Initial commit

Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
This commit is contained in:
Thomas Klaehn
2026-02-08 07:28:32 +00:00
commit 88b74b7dbc
7 changed files with 1567 additions and 0 deletions

246
fitparser/decoder_test.go Normal file
View File

@@ -0,0 +1,246 @@
package fitparser
import (
"fmt"
"os"
"testing"
)
func TestIsFIT(t *testing.T) {
tests := []struct {
name string
data []byte
expected bool
}{
{
name: "Valid FIT signature",
data: []byte{0x0E, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, '.', 'F', 'I', 'T', 0x00, 0x00},
expected: true,
},
{
name: "Invalid signature",
data: []byte{0x0E, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 'N', 'O', 'T', 'F', 0x00, 0x00},
expected: false,
},
{
name: "Too short",
data: []byte{0x0E, 0x10},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsFIT(tt.data)
if result != tt.expected {
t.Errorf("IsFIT() = %v, want %v", result, tt.expected)
}
})
}
}
func TestCalculateCRC(t *testing.T) {
// Test with known CRC values
data := []byte{0x0E, 0x10, 0xD9, 0x07, 0x00, 0x00, 0x00, 0x00, '.', 'F', 'I', 'T'}
crc := CalculateCRC(data)
// CRC should be non-zero for this data
if crc == 0 {
t.Errorf("CalculateCRC() returned 0, expected non-zero value")
}
// Test that same data produces same CRC
crc2 := CalculateCRC(data)
if crc != crc2 {
t.Errorf("CalculateCRC() not deterministic: got %d and %d", crc, crc2)
}
}
func TestParseHeader(t *testing.T) {
tests := []struct {
name string
data []byte
wantErr bool
}{
{
name: "Valid 12-byte header",
data: []byte{0x0C, 0x10, 0xD9, 0x07, 0x10, 0x00, 0x00, 0x00, '.', 'F', 'I', 'T'},
wantErr: false,
},
{
name: "Too short",
data: []byte{0x0C, 0x10},
wantErr: true,
},
{
name: "Invalid signature",
data: []byte{0x0C, 0x10, 0xD9, 0x07, 0x10, 0x00, 0x00, 0x00, 'N', 'O', 'P', 'E'},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
header, err := ParseHeader(tt.data)
if (err != nil) != tt.wantErr {
t.Errorf("ParseHeader() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && header == nil {
t.Errorf("ParseHeader() returned nil header without error")
}
})
}
}
func TestDecodeActivityFile(t *testing.T) {
// Test with a real FIT file from the SDK examples
filePath := "../example/testdata/Activity.fit"
data, err := os.ReadFile(filePath)
if err != nil {
t.Fatalf("Error reading test file %s: %v", filePath, err)
}
decoder, err := NewDecoder(data)
if err != nil {
t.Fatalf("NewDecoder() error = %v", err)
}
messages, err := decoder.Decode()
if err != nil {
t.Fatalf("Decode() error = %v", err)
}
if len(messages) == 0 {
t.Error("Decode() returned no messages")
}
// Print some statistics
t.Logf("Successfully decoded %d messages", len(messages))
// Count message types
msgCounts := make(map[uint16]int)
for _, msg := range messages {
msgCounts[msg.Num]++
}
t.Logf("Message type breakdown:")
for mesgNum, count := range msgCounts {
t.Logf(" %s (%d): %d messages", GetMessageName(mesgNum), mesgNum, count)
}
// Verify we have essential messages
if msgCounts[MesgNumFileId] == 0 {
t.Error("Expected at least one FileId message")
}
if msgCounts[MesgNumRecord] == 0 {
t.Error("Expected at least one Record message")
}
}
func TestDecodeSettingsFile(t *testing.T) {
filePath := "../example/testdata/Settings.fit"
data, err := os.ReadFile(filePath)
if err != nil {
t.Fatalf("Error reading test file %s: %v", filePath, err)
}
decoder, err := NewDecoder(data)
if err != nil {
t.Fatalf("NewDecoder() error = %v", err)
}
messages, err := decoder.Decode()
if err != nil {
t.Fatalf("Decode() error = %v", err)
}
if len(messages) == 0 {
t.Error("Decode() returned no messages")
}
t.Logf("Successfully decoded %d messages from Settings.fit", len(messages))
}
func TestCheckIntegrity(t *testing.T) {
filePath := "../example/testdata/Activity.fit"
data, err := os.ReadFile(filePath)
if err != nil {
t.Fatalf("Error reading test file %s: %v", filePath, err)
}
err = CheckIntegrity(data)
if err != nil {
t.Errorf("CheckIntegrity() error = %v", err)
}
}
func TestConvertSemicirclesToDegrees(t *testing.T) {
tests := []struct {
semicircles int32
expected float64
}{
{0, 0.0},
{2147483647, 180.0}, // Max positive (approximately)
{-2147483648, -180.0}, // Max negative (approximately)
{1073741824, 90.0}, // Quarter circle
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%d_semicircles", tt.semicircles), func(t *testing.T) {
result := ConvertSemicirclesToDegrees(tt.semicircles)
// Allow small floating point error
if result < tt.expected-0.1 || result > tt.expected+0.1 {
t.Errorf("ConvertSemicirclesToDegrees(%d) = %f, want %f", tt.semicircles, result, tt.expected)
}
})
}
}
func BenchmarkDecode(b *testing.B) {
filePath := "../example/testdata/Activity.fit"
data, err := os.ReadFile(filePath)
if err != nil {
b.Fatalf("Error reading test file %s: %v", filePath, err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
decoder, err := NewDecoder(data)
if err != nil {
b.Fatalf("NewDecoder() error = %v", err)
}
_, err = decoder.Decode()
if err != nil {
b.Fatalf("Decode() error = %v", err)
}
}
}
func BenchmarkDecodeNoCRC(b *testing.B) {
filePath := "../example/testdata/Activity.fit"
data, err := os.ReadFile(filePath)
if err != nil {
b.Fatalf("Error reading test file %s: %v", filePath, err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
decoder, err := NewDecoder(data)
if err != nil {
b.Fatalf("NewDecoder() error = %v", err)
}
decoder.EnableCRCCheck(false)
_, err = decoder.Decode()
if err != nil {
b.Fatalf("Decode() error = %v", err)
}
}
}