From 4bc4a47ce3c633d7c24775c4637aa308bec6ae44 Mon Sep 17 00:00:00 2001 From: teamjorge <18563449+teamjorge@users.noreply.github.com> Date: Wed, 21 Aug 2024 20:00:33 +0200 Subject: [PATCH] Feature refactor processor and parser (#6) --- README.md | 6 +- examples/README.md | 18 ++++ examples/common.go | 42 +++++++++ examples/track_temp/README.md | 17 ++++ examples/track_temp/main.go | 110 ++++------------------ examples/track_temp/processors.go | 69 ++++++++++++++ headers/var.go | 4 +- parser.go | 37 +------- parser_test.go | 72 ++------------- processor.go | 80 ++++++++++++++-- processor_test.go | 146 +++++++++++++++++++++++++++--- stub.go | 10 ++ stub_test.go | 28 ++++++ tick.go | 42 +++++++++ tick_test.go | 36 ++++++++ utilities/sets.go | 14 +++ utilities/sets_test.go | 32 +++++++ value.go | 2 +- 18 files changed, 547 insertions(+), 218 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/common.go create mode 100644 examples/track_temp/README.md create mode 100644 examples/track_temp/processors.go create mode 100644 tick.go create mode 100644 tick_test.go create mode 100644 utilities/sets.go create mode 100644 utilities/sets_test.go diff --git a/README.md b/README.md index 292e0e0..c28e3c9 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,11 @@ This package will not parse real-time telemetry as that requires opening a memor * Quick parsing of file metadata. * Grouping of *ibt* files into the sessions where they originate from. * Great test coverage and code documentation. -* Freedom to use it your own way. Most of what is needed are public functions/methods. +* Freedom to use it your own way. Most functions/methods has been made public. ## Examples -Please see the `examples` folder for detailed usage instructions. +Please see the [`examples`](https://github.com/teamjorge/ibt/tree/main/examples) folder for detailed usage instructions. To try the examples locally, please clone to repository: @@ -49,6 +49,6 @@ go run examples/track_temp/main.go # Or to run it with your own telemetry files -go run examples/track_temp/main.go /path/to/telem/files/*ibt +go run examples/track_temp/main.go /path/to/telem/files/*.ibt ``` diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..e543fdc --- /dev/null +++ b/examples/README.md @@ -0,0 +1,18 @@ +# examples + +All examples allow you to run with the supplied `ibt` file or with your own. + +For example: + + +```shell +go run examples/[Example Folder]/main.go + +# Or + +go run examples/[Example Folder]/main.go /path/to/telem/files/*.ibt +``` + +Available examples: + +* [track temperature](./track_temp/README.md) - diff --git a/examples/common.go b/examples/common.go new file mode 100644 index 0000000..5ee23de --- /dev/null +++ b/examples/common.go @@ -0,0 +1,42 @@ +package examples + +import ( + "flag" + "fmt" + "path/filepath" + + "github.com/teamjorge/ibt" +) + +// Use the default testing file if no ibt files were provided +// Provided files can use wildcards, for example ./telemetry/*.ibt +func getExampleFilePattern() string { + flag.Parse() + + var filePattern string + if flag.Arg(0) == "" { + filePattern = ".testing/valid_test_file.ibt" + } else { + filePattern = flag.Arg(0) + } + + return filePattern +} + +func ParseExampleStubs() (ibt.StubGroup, error) { + filePattern := getExampleFilePattern() + + // Find all files for parsing in case it had included a wildcard + files, err := filepath.Glob(filePattern) + if err != nil { + return nil, fmt.Errorf("could not glob the given input files: %v", err) + } + + // Parse the files into stubs + stubs, err := ibt.ParseStubs(files...) + if err != nil { + return nil, fmt.Errorf("failed to parse stubs for %v. error - %v", files, err) + } + + return stubs, nil +} diff --git a/examples/track_temp/README.md b/examples/track_temp/README.md new file mode 100644 index 0000000..ab4e6fb --- /dev/null +++ b/examples/track_temp/README.md @@ -0,0 +1,17 @@ +# track_temp + +The `track_temp` example shows a simple processor summarising the track temperature on each lap of the provided `ibt` files. + +## Running + +From the root of the repository: + +```shell +go run examples/track_temp/main.go + +# Or with your own files + +go run examples/track_temp/main.go /path/to/telem/files/*.ibt +``` + +Using the included `ibt` file will yield only a single lap and it's value. However, if you have telemetry consisting of a few laps and/or files from a longer session, you should have a nicely summarised per-lap output. \ No newline at end of file diff --git a/examples/track_temp/main.go b/examples/track_temp/main.go index 281ef66..1eb7e7e 100644 --- a/examples/track_temp/main.go +++ b/examples/track_temp/main.go @@ -1,113 +1,35 @@ package main import ( - "flag" - "fmt" + "context" "log" - "os" - "path/filepath" - "sort" "github.com/teamjorge/ibt" - "github.com/teamjorge/ibt/headers" - "github.com/teamjorge/ibt/utilities" - "golang.org/x/exp/maps" + "github.com/teamjorge/ibt/examples" ) func main() { - flag.Parse() - - // Use the default testing file if no ibt files were provided - // Provided files can use wildcards, for example ./telemetry/*.ibt - var filePattern string - if flag.Arg(0) == "" { - filePattern = ".testing/valid_test_file.ibt" - } else { - filePattern = flag.Arg(0) - } - - // Find all files for parsing in case it had included a wildcard - files, err := filepath.Glob(filePattern) - if err != nil { - log.Fatalf("could not glob the given input files: %v", err) - } - // Parse the files into stubs - stubs, err := ibt.ParseStubs(files...) + stubs, err := examples.ParseExampleStubs() if err != nil { - log.Fatalf("failed to parse stubs for %v. error - %v", files, err) + log.Fatal(err) } - // Group the stubs into iRacing sessions - stubGroups := stubs.Group() - - for _, stubGroup := range stubGroups { - for _, stub := range stubGroup { - stubFile, err := os.Open(stub.Filename()) - if err != nil { - log.Fatalf("failed to open stub file %s for reading: %v", stub.Filename(), err) - } + // We group the stubs by iRacing session. This allows us to summarise results for + // an entire session, instead of just a single ibt file. + groups := stubs.Group() - // Create the instance(s) of your processor(s) - processor := NewTrackTempProcessor() + for groupIdx, group := range groups { + // Create the instance(s) of your processor(s) for this group + processor := newTrackTempProcessor() - // Process the available telemetry for the ibt file. This is currently only utilising the Track Temp processor, - // but can include as many as you want. - if err := ibt.Process(stubFile, *stub.Headers(), processor); err != nil { - log.Fatalf("failed to process telemetry for stub %s: %v", stub.Filename(), err) - } - - // Print the summarised track temperature - processor.Print() + // Process the available telemetry for the ibt file. This is currently only utilising the Track Temp processor, + // but can include as many as you want. + if err := ibt.Process(context.Background(), group, processor); err != nil { + log.Fatalf("failed to process telemetry for group %d: %v", groupIdx, err) } - } - -} - -type TrackTempProcessor struct { - tempMap map[int]float32 -} - -func NewTrackTempProcessor() *TrackTempProcessor { - t := new(TrackTempProcessor) - - t.tempMap = make(map[int]float32) - - return t -} - -// Display name of the processor -func (t *TrackTempProcessor) Name() string { return "Track Temp" } - -// Method used for processing every tick of telemetry -func (t *TrackTempProcessor) Process(input map[string]headers.VarHeader, hasNext bool, session *headers.Session) error { - TrackTempProcessor := input["TrackTempCrew"].Value.(float32) - lap := input["Lap"].Value.(int) - - t.tempMap[lap] = TrackTempProcessor - - return nil -} - -// Utility function for create a result that can be joined with other processors. -// -// This will convert the results to map[int]interface{}, where the keys will refer to laps. -// Result is not yet required by any interfaces, but is useful when using multiple processors -// that summarise telemetry based by lap. -func (t *TrackTempProcessor) Result() map[int]interface{} { - return utilities.CreateGenericMap(t.tempMap) -} - -// Columns required for the processor -func (t *TrackTempProcessor) Whitelist() []string { return []string{"Lap", "TrackTempCrew"} } - -// Print the summarised Track Temperature -func (t *TrackTempProcessor) Print() { - fmt.Println("Track Temp:") - laps := maps.Keys(t.tempMap) - sort.Ints(laps) - for _, lap := range laps { - fmt.Printf("%03d - %.3f\n", lap, t.tempMap[lap]) + // Print the summarised track temperature + processor.Print() } } diff --git a/examples/track_temp/processors.go b/examples/track_temp/processors.go new file mode 100644 index 0000000..c12ad34 --- /dev/null +++ b/examples/track_temp/processors.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" + "sort" + + "github.com/teamjorge/ibt" + "github.com/teamjorge/ibt/headers" + "github.com/teamjorge/ibt/utilities" + "golang.org/x/exp/maps" +) + +// TrackTempProcessors tracks the track temperature for each lap of the ibt file +type trackTempProcessor struct { + tempMap map[int]float32 +} + +// NewTrackTempProcessor creates and initialises a new trackTempProcessor +func newTrackTempProcessor() *trackTempProcessor { + t := new(trackTempProcessor) + + // tempMap will store a temperature value against a lap number + t.tempMap = make(map[int]float32) + + return t +} + +// Display name of the processor +func (t *trackTempProcessor) Name() string { return "Track Temp" } + +// Method used for processing every tick of telemetry +func (t *trackTempProcessor) Process(input ibt.Tick, hasNext bool, session *headers.Session) error { + trackTemp, err := ibt.GetVariableValue[float32](input, "TrackTempCrew") + if err != nil { + return err + } + + lap, err := ibt.GetVariableValue[int](input, "Lap") + if err != nil { + return err + } + + t.tempMap[lap] = trackTemp + + return nil +} + +// Utility function for create a result that can be joined with other processors. +// +// This will convert the results to map[int]interface{}, where the keys will refer to laps. +// Result is not yet required by any interfaces, but is useful when using multiple processors +// that summarise telemetry based by lap. +func (t *trackTempProcessor) Result() map[int]interface{} { + return utilities.CreateGenericMap(t.tempMap) +} + +// Columns required for the processor +func (t *trackTempProcessor) Whitelist() []string { return []string{"Lap", "TrackTempCrew"} } + +// Print the summarised Track Temperature +func (t *trackTempProcessor) Print() { + fmt.Println("Track Temp:") + laps := maps.Keys(t.tempMap) + sort.Ints(laps) + + for _, lap := range laps { + fmt.Printf("%03d - %.3f\n", lap, t.tempMap[lap]) + } +} diff --git a/headers/var.go b/headers/var.go index f0de60c..4eda7f3 100644 --- a/headers/var.go +++ b/headers/var.go @@ -17,10 +17,10 @@ type VarHeader struct { // Rtype is the variable value type. // // Possible values: - // 0: String + // 0: Uint8 // 1: Boolean // 2: Int - // 3: Byte + // 3: String // 4: Float32 // 5: Float64 Rtype int `json:"rtype,omitempty"` diff --git a/parser.go b/parser.go index 14c9255..9aaa787 100644 --- a/parser.go +++ b/parser.go @@ -2,7 +2,6 @@ package ibt import ( "github.com/teamjorge/ibt/headers" - "golang.org/x/exp/maps" ) // Parser is used to iterate and process telemetry variables for a given ibt file and it's headers. @@ -27,7 +26,7 @@ func NewParser(reader headers.Reader, header headers.Header, whitelist ...string p := new(Parser) p.reader = reader - p.whitelist = computeVars(header.VarHeader(), whitelist...) + p.whitelist = whitelist p.length = header.TelemetryHeader().BufLen p.bufferOffset = header.TelemetryHeader().BufOffset @@ -43,7 +42,7 @@ func NewParser(reader headers.Reader, header headers.Header, whitelist ...string // a nil and false will be returned. // // Should expected variable values be missing, please ensure that they are added to the Parser whitelist. -func (p *Parser) Next() (map[string]headers.VarHeader, bool) { +func (p *Parser) Next() (Tick, bool) { start := p.bufferOffset + (p.current * p.length) currentBuf := p.read(start) if currentBuf == nil { @@ -54,13 +53,12 @@ func (p *Parser) Next() (map[string]headers.VarHeader, bool) { nextStart := p.bufferOffset + ((p.current + 1) * p.length) nextBuf := p.read(nextStart) - newVars := make(map[string]headers.VarHeader) + newVars := make(Tick) for _, variable := range p.whitelist { item := p.varHeader[variable] val := readVarValue(currentBuf, item) - item.Value = val - newVars[variable] = item + newVars[variable] = val } p.current++ @@ -79,30 +77,3 @@ func (p *Parser) read(start int) []byte { return buf } - -// compareVars will retrieve vars when * is used and ensure a unique list -// -// Variables that are not found in the VarHeader will automatically be excluded. -func computeVars(vars map[string]headers.VarHeader, whitelist ...string) []string { - if len(whitelist) == 0 { - return headers.AvailableVars(vars) - } - - for _, col := range whitelist { - if col == "*" { - return headers.AvailableVars(vars) - } - } - - // de-duplicate the columns - varMap := make(map[string]struct{}) - - for _, col := range whitelist { - // ensure it's a valid column - if _, ok := vars[col]; ok { - varMap[col] = struct{}{} - } - } - - return maps.Keys(varMap) -} diff --git a/parser_test.go b/parser_test.go index 3a01eb0..23d5896 100644 --- a/parser_test.go +++ b/parser_test.go @@ -49,19 +49,6 @@ func TestParser(t *testing.T) { t.Errorf("expected varHeader to be of length %d, actual: %d", 276, len(p.varHeader)) } }) - - t.Run("test NewParser wildcard or null whitelist", func(t *testing.T) { - p := NewParser(f, testHeaders, "*") - - if len(p.whitelist) != 276 { - t.Errorf("expected whitelist to be of length %d, actual: %d", 278, len(p.whitelist)) - } - - p = NewParser(f, testHeaders) - if len(p.whitelist) != 276 { - t.Errorf("expected whitelist to be of length %d, actual: %d", 278, len(p.whitelist)) - } - }) } func TestParserNext(t *testing.T) { @@ -89,8 +76,8 @@ func TestParserNext(t *testing.T) { for idx, expectedValue := range expectedValues { vars, next := p.Next() - if vars["LapCurrentLapTime"].Value != expectedValue { - t.Errorf("expected LapCurrentLapTime value to equal %f, got %f", expectedValue, vars["LapCurrentLapTime"].Value) + if vars["LapCurrentLapTime"] != expectedValue { + t.Errorf("expected LapCurrentLapTime value to equal %f, got %f", expectedValue, vars["LapCurrentLapTime"]) } if !next { t.Errorf("expected additional var values to be available after iteration %d", idx) @@ -105,8 +92,8 @@ func TestParserNext(t *testing.T) { p.current = 388 vars, next := p.Next() - if vars["LapCurrentLapTime"].Value != expectedValue1 { - t.Errorf("expected LapCurrentLapTime value to equal %f, got %f", expectedValue1, vars["LapCurrentLapTime"].Value) + if vars["LapCurrentLapTime"] != expectedValue1 { + t.Errorf("expected LapCurrentLapTime value to equal %f, got %f", expectedValue1, vars["LapCurrentLapTime"]) } if !next { t.Error("expected additional var values to be available after iteration") @@ -114,8 +101,8 @@ func TestParserNext(t *testing.T) { expectedValue2 := float32(44.145233) vars, next = p.Next() - if vars["LapCurrentLapTime"].Value != expectedValue2 { - t.Errorf("expected LapCurrentLapTime value to equal %f, got %f", expectedValue2, vars["LapCurrentLapTime"].Value) + if vars["LapCurrentLapTime"] != expectedValue2 { + t.Errorf("expected LapCurrentLapTime value to equal %f, got %f", expectedValue2, vars["LapCurrentLapTime"]) } if next { t.Error("expected no more var values to be available after iteration") @@ -170,50 +157,3 @@ func TestParserRead(t *testing.T) { }) } - -func TestCompareVars(t *testing.T) { - t.Run("computeVars() explicit columns", func(t *testing.T) { - vars := map[string]headers.VarHeader{ - "var1": {}, - "var2": {}, - "var3": {}, - "var4": {}, - } - - receivedVars := computeVars(vars, "var3", "test", "var4") - sort.Strings(receivedVars) - - if receivedVars[0] != "var3" && receivedVars[1] != "var4" { - t.Errorf("expected vars to equal [%s, %s]. received: %v", "var3", "var4", receivedVars) - } - }) - - t.Run("computeVars() empty", func(t *testing.T) { - vars := map[string]headers.VarHeader{ - "var1": {}, - "var2": {}, - } - - receivedVars := computeVars(vars) - sort.Strings(receivedVars) - - if receivedVars[0] != "var1" && receivedVars[1] != "var2" { - t.Errorf("expected vars to equal [%s, %s]. received: %v", "var1", "var2", receivedVars) - } - }) - - t.Run("computeVars() wildcard", func(t *testing.T) { - vars := map[string]headers.VarHeader{ - "var1": {}, - "var2": {}, - "var3": {}, - } - - receivedVars := computeVars(vars, "*", "test", "*") - sort.Strings(receivedVars) - - if receivedVars[0] != "var1" && receivedVars[1] != "var2" && receivedVars[2] != "var3" { - t.Errorf("expected vars to equal [%s, %s, %s]. received: %v", "var1", "var2", "var3", receivedVars) - } - }) -} diff --git a/processor.go b/processor.go index 53bba58..4fb8a66 100644 --- a/processor.go +++ b/processor.go @@ -1,27 +1,54 @@ package ibt import ( + "context" + "errors" + "sort" + "github.com/teamjorge/ibt/headers" + "github.com/teamjorge/ibt/utilities" ) type Processor interface { - Process(input map[string]headers.VarHeader, hasNext bool, session *headers.Session) error + Process(input Tick, hasNext bool, session *headers.Session) error Whitelist() []string } -func Process(reader headers.Reader, header headers.Header, processors ...Processor) error { - whitelist := make([]string, 0) +func Process(ctx context.Context, stubs StubGroup, processors ...Processor) error { + sort.Sort(stubs) - for _, proc := range processors { - whitelist = append(whitelist, proc.Whitelist()...) + for _, stub := range stubs { + if err := process(ctx, stub, processors...); err != nil { + return err + } } - parser := NewParser(reader, header, whitelist...) + return nil +} + +func process(ctx context.Context, stub Stub, processors ...Processor) error { + reader, err := stub.Open() + if err != nil { + return err + } + defer reader.Close() + + header := stub.header + + whitelist := buildWhitelist(header.VarHeader(), processors...) + + parser := NewParser(reader, header, whitelist...) for { + select { + case <-ctx.Done(): + return errors.New("context cancelled") + default: + } + tick, hasNext := parser.Next() for _, proc := range processors { - if err := proc.Process(tick, hasNext, header.SessionInfo()); err != nil { + if err := proc.Process(tick.Filter(proc.Whitelist()...), hasNext, header.SessionInfo()); err != nil { return err } } @@ -33,3 +60,42 @@ func Process(reader headers.Reader, header headers.Header, processors ...Process return nil } + +// getcinoketeWhitelist compiles the whitelists from all processors and removes overlap +func buildWhitelist(vars map[string]headers.VarHeader, processors ...Processor) []string { + whitelist := make([]string, 0) + + for _, proc := range processors { + whitelist = append(whitelist, parseAndValidateWhitelist(vars, proc)...) + } + + return utilities.GetDistinct(whitelist) +} + +// parseWhitelist will retrieve vars when * is used and ensure a unique list +// +// Variables that are not found in the VarHeader will automatically be excluded. +func parseAndValidateWhitelist(vars map[string]headers.VarHeader, processor Processor) []string { + whitelist := processor.Whitelist() + + if len(whitelist) == 0 { + return headers.AvailableVars(vars) + } + + for _, col := range whitelist { + if col == "*" { + return headers.AvailableVars(vars) + } + } + + columns := make([]string, 0) + + // Ensure only valid columns are added + for _, col := range whitelist { + if _, ok := vars[col]; ok { + columns = append(columns, col) + } + } + + return columns +} diff --git a/processor_test.go b/processor_test.go index eea57cb..6234900 100644 --- a/processor_test.go +++ b/processor_test.go @@ -1,30 +1,33 @@ package ibt import ( + "context" "errors" "os" + "sort" "testing" "github.com/teamjorge/ibt/headers" ) type testProcessor struct { - results []map[string]headers.VarHeader - session *headers.Session + results []Tick + session *headers.Session + whitelist []string } -func (t *testProcessor) Process(input map[string]headers.VarHeader, hasNext bool, session *headers.Session) error { +func (t *testProcessor) Process(input Tick, hasNext bool, session *headers.Session) error { t.results = append(t.results, input) t.session = session return nil } -func (t *testProcessor) Whitelist() []string { return []string{"LapCurrentLapTime"} } +func (t *testProcessor) Whitelist() []string { return t.whitelist } type testErrorProcessor struct{} -func (t *testErrorProcessor) Process(input map[string]headers.VarHeader, hasNext bool, session *headers.Session) error { +func (t *testErrorProcessor) Process(input Tick, hasNext bool, session *headers.Session) error { return errors.New("unit test error") } @@ -44,29 +47,148 @@ func TestProcess(t *testing.T) { return } - t.Run("test process normal processor", func(t *testing.T) { - proc := testProcessor{} + stubs := StubGroup{ + {filepath: ".testing/valid_test_file.ibt", header: testHeaders}, + } + + t.Run("test Process() normal processor", func(t *testing.T) { + proc := testProcessor{whitelist: []string{"LapCurrentLapTime"}} - if err := Process(f, testHeaders, &proc); err != nil { + if err := Process(context.Background(), stubs, &proc); err != nil { t.Errorf("expected Process() to run without err. received error: %v", err) } - valueToCheck := proc.results[0]["LapCurrentLapTime"].Value.(float32) + valueToCheck := proc.results[0]["LapCurrentLapTime"].(float32) if valueToCheck != 37.678566 { t.Errorf("expected value to check to be %f. got %f", 37.678566, valueToCheck) } - valueToCheck = proc.results[69]["LapCurrentLapTime"].Value.(float32) + valueToCheck = proc.results[69]["LapCurrentLapTime"].(float32) if valueToCheck != 38.828568 { t.Errorf("expected value to check to be %f. got %f", 38.828568, valueToCheck) } }) - t.Run("test process err processor", func(t *testing.T) { + t.Run("test Process() err processor", func(t *testing.T) { proc := testErrorProcessor{} - if err := Process(f, testHeaders, &proc); err == nil { + if err := Process(context.Background(), stubs, &proc); err == nil { t.Error("expected Process() to return an error") } }) + + t.Run("test process() invalid file", func(t *testing.T) { + proc := testProcessor{whitelist: []string{"LapCurrentLapTime"}} + + invalidStub := Stub{ + filepath: "disappear_here", + } + + if err := process(context.Background(), invalidStub, &proc); err == nil { + t.Errorf("expected Process() to exit with a file error") + } + }) + + t.Run("test process() invalid file", func(t *testing.T) { + proc := testProcessor{whitelist: []string{"LapCurrentLapTime"}} + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + if err := process(ctx, stubs[0], &proc); err == nil { + t.Errorf("expected process() to exit with a context done error") + } + }) +} + +func TestWhitelistParsing(t *testing.T) { + f, err := os.Open(".testing/valid_test_file.ibt") + if err != nil { + t.Errorf("failed to open testing file - %v", err) + return + } + defer f.Close() + + testHeaders, err := headers.ParseHeaders(f) + if err != nil { + t.Errorf("failed to parse header for testing file - %v", err) + return + } + + varHeader := testHeaders.VarHeader() + + t.Run("test parseAndValidateWhitelist empty", func(t *testing.T) { + proc := testProcessor{whitelist: []string{}} + + cols := parseAndValidateWhitelist(varHeader, &proc) + + if len(cols) != 276 { + t.Errorf("expected %d columns to be in whitelist when returning an empty Whitelist() value. found %d", 276, len(cols)) + } + }) + + t.Run("test parseAndValidateWhitelist *", func(t *testing.T) { + proc := testProcessor{whitelist: []string{"something", "*"}} + + cols := parseAndValidateWhitelist(varHeader, &proc) + + if len(cols) != 276 { + t.Errorf("expected %d columns to be in whitelist when returning an empty Whitelist() value. found %d", 276, len(cols)) + } + }) + + t.Run("test parseAndValidateWhitelist valid and invalid columns", func(t *testing.T) { + proc := testProcessor{whitelist: []string{"something", "Speed", "is", "Gear", "wrong"}} + + cols := parseAndValidateWhitelist(varHeader, &proc) + + if len(cols) != 2 { + t.Errorf("expected %d columns to be in whitelist when returning an empty Whitelist() value. found %d", 2, len(cols)) + } + + sort.Strings(cols) + + if cols[0] != "Gear" || cols[1] != "Speed" { + t.Errorf("expected columns to be %v. received %v", []string{"Gear", "Speed"}, cols) + } + }) + + t.Run("test buildWhitelist with 1 * and 2 normal", func(t *testing.T) { + proc1 := testProcessor{whitelist: []string{"something", "Speed", "is", "Gear", "wrong"}} + proc2 := testProcessor{whitelist: []string{"*"}} + + cols := buildWhitelist(varHeader, []Processor{&proc1, &proc2}...) + + if len(cols) != 276 { + t.Errorf("expected %d columns to be in whitelist when returning an empty Whitelist() value. found %d", 276, len(cols)) + } + }) + + t.Run("test buildWhitelist with 1 empty and 2 normal", func(t *testing.T) { + proc1 := testProcessor{whitelist: []string{"something", "Speed", "is", "Gear", "wrong"}} + proc2 := testProcessor{whitelist: nil} + + cols := buildWhitelist(varHeader, []Processor{&proc1, &proc2}...) + + if len(cols) != 276 { + t.Errorf("expected %d columns to be in whitelist when returning an empty Whitelist() value. found %d", 276, len(cols)) + } + }) + + t.Run("test buildWhitelist with duplicate and invalid columns", func(t *testing.T) { + proc1 := testProcessor{whitelist: []string{"something", "Speed", "is", "Gear", "wrong"}} + proc2 := testProcessor{whitelist: []string{"BrakeRaw", "Speed", "ThrottleRaw", "Gear", "wrong"}} + + cols := buildWhitelist(varHeader, []Processor{&proc1, &proc2}...) + + if len(cols) != 4 { + t.Errorf("expected %d columns to be in whitelist when returning an empty Whitelist() value. found %d", 4, len(cols)) + } + + sort.Strings(cols) + + if cols[0] != "BrakeRaw" || cols[1] != "Gear" || cols[2] != "Speed" || cols[3] != "ThrottleRaw" { + t.Errorf("expected columns to be %v. received %v", []string{"BrakeRaw", "Gear", "Speed", "ThrottleRaw"}, cols) + } + }) } diff --git a/stub.go b/stub.go index c3976db..44b942f 100644 --- a/stub.go +++ b/stub.go @@ -18,6 +18,16 @@ type Stub struct { header headers.Header } +// Open the underlying ibt file for reading +func (stub *Stub) Open() (headers.Reader, error) { + reader, err := os.Open(stub.Filename()) + if err != nil { + return nil, fmt.Errorf("failed to open stub file %s for reading: %v", stub.Filename(), err) + } + + return reader, nil +} + // Filename where the stub originated from func (stub *Stub) Filename() string { return stub.filepath } diff --git a/stub_test.go b/stub_test.go index 1da5328..8ae9d19 100644 --- a/stub_test.go +++ b/stub_test.go @@ -1,6 +1,7 @@ package ibt import ( + "bytes" "os" "reflect" "sort" @@ -66,6 +67,33 @@ func TestStubs(t *testing.T) { t.Errorf("expected driver idx to be 15, but got %d", driverIdxStub.DriverIdx()) } }) + + t.Run("stubs Open() valid file", func(t *testing.T) { + stub := Stub{filepath: ".testing/valid_test_file.ibt"} + + f, err := stub.Open() + if err != nil { + t.Errorf("did not expect an error when opening file %s. received: %v", ".testing/valid_test_file.ibt", err) + } + + buf := make([]byte, 2) + if _, err := f.Read(buf); err != nil { + t.Errorf("did not expect an error when reading 2 bytes from file %s. received: %v", ".testing/valid_test_file.ibt", err) + } + + if !bytes.Equal(buf, []byte{0x2, 0x0}) { + t.Errorf("expected buf to be %v. received %v", []byte{0x2, 0x0}, buf) + } + }) + + t.Run("stubs Open() invalid file", func(t *testing.T) { + stub := Stub{filepath: ".testing/disappear_here.ibt"} + + _, err := stub.Open() + if err == nil { + t.Errorf("expected an error when opening a non-existent file %s", ".testing/disappear_here.ibt") + } + }) } func TestParseStubs(t *testing.T) { diff --git a/tick.go b/tick.go new file mode 100644 index 0000000..3625f96 --- /dev/null +++ b/tick.go @@ -0,0 +1,42 @@ +package ibt + +import ( + "fmt" + "reflect" +) + +// Tick is a single instance of telemetry data +type Tick map[string]interface{} + +// TickValueType is an interface containing all possible types for the value of a telemetry variable +type TickValueType interface { + uint8 | []uint8 | bool | []bool | int | []int | string | []string | float32 | []float32 | float64 | []float64 +} + +// Filter the tick for only the given whitelisted fields +func (t Tick) Filter(whitelist ...string) Tick { + partialTick := make(Tick) + + for _, field := range whitelist { + partialTick[field] = t[field] + } + + return partialTick +} + +// GetVariableValue will retrieve and type assert the given variable. +func GetVariableValue[T TickValueType](variables Tick, key string) (T, error) { + var def T + + rawValue, ok := variables[key] + if !ok { + return def, fmt.Errorf("key %s not found in telemetry variables", key) + } + + value, ok := rawValue.(T) + if !ok { + return def, fmt.Errorf("value of %s was %s not %s", key, reflect.TypeOf(rawValue).String(), reflect.TypeOf(def).String()) + } + + return value, nil +} diff --git a/tick_test.go b/tick_test.go new file mode 100644 index 0000000..f3b9682 --- /dev/null +++ b/tick_test.go @@ -0,0 +1,36 @@ +package ibt + +import "testing" + +func TestGetVariableValue(t *testing.T) { + testTick := Tick{ + "Speed": float32(103.23), + "Gear": 5, + "Flag": "0x3421", + } + + t.Run("test normal scenario", func(t *testing.T) { + value, err := GetVariableValue[int](testTick, "Gear") + if err != nil { + t.Errorf("expected err to be nil but received: %v", err) + } + + if value != 5 { + t.Errorf("expected return Gear value to be %d. received: %d", 5, value) + } + }) + + t.Run("test missing key", func(t *testing.T) { + _, err := GetVariableValue[int](testTick, "NotFound") + if err == nil { + t.Errorf("expected an error to occur when retrieving value for key %s", "NotFound") + } + }) + + t.Run("test missing key", func(t *testing.T) { + _, err := GetVariableValue[int](testTick, "Speed") + if err == nil { + t.Errorf("expected an error to occur when retrieving value for key %s with type int", "Speed") + } + }) +} diff --git a/utilities/sets.go b/utilities/sets.go new file mode 100644 index 0000000..284ebff --- /dev/null +++ b/utilities/sets.go @@ -0,0 +1,14 @@ +package utilities + +import "golang.org/x/exp/maps" + +// GetDistinct values of a slice +func GetDistinct[K comparable](slice []K) []K { + unique := map[K]struct{}{} + + for _, item := range slice { + unique[item] = struct{}{} + } + + return maps.Keys(unique) +} diff --git a/utilities/sets_test.go b/utilities/sets_test.go new file mode 100644 index 0000000..e7d38cd --- /dev/null +++ b/utilities/sets_test.go @@ -0,0 +1,32 @@ +package utilities + +import ( + "sort" + "testing" +) + +func TestDistinct(t *testing.T) { + t.Run("test distinct normal", func(t *testing.T) { + items := []int{5, 1, 5, 5, 3, 1, 4} + + distinctItems := GetDistinct(items) + if len(distinctItems) != 4 { + t.Errorf("expected items to have len %d. received %d", 4, len(distinctItems)) + } + + sort.Ints(distinctItems) + + if distinctItems[0] != 1 && distinctItems[1] != 3 && distinctItems[2] != 4 && distinctItems[3] != 5 { + t.Errorf("expected items to be %v. received: %v", []int{1, 3, 4, 5}, distinctItems) + } + }) + + t.Run("test distinct empty", func(t *testing.T) { + var items []int = nil + + distinctItems := GetDistinct(items) + if len(distinctItems) != 0 { + t.Errorf("expected items to have len %d. received %d", 0, len(distinctItems)) + } + }) +} diff --git a/value.go b/value.go index c881bac..50489bd 100644 --- a/value.go +++ b/value.go @@ -63,7 +63,7 @@ func readVarValue(buf []byte, vh headers.VarHeader) interface{} { switch vh.Rtype { case 0: rbuf = buf[offset : offset+1] - value = rbuf[0] + value = uint8(rbuf[0]) case 1: rbuf = buf[offset : offset+1] value = int(rbuf[0]) > 0