package main import ( "bytes" "encoding/json" "errors" "reflect" "strings" "testing" "netdome.biz/paul/qmk-rgb/internal/via" ) type infoKey struct { channel via.Channel param uint8 } // DetectChannels lets the info command's target seam reuse this fake. func (f *fakeInfoProtocol) DetectChannels() ([]via.Channel, error) { return impact80Channels(), nil } type fakeInfoProtocol struct { values map[infoKey][]byte errors map[infoKey]error calls []infoKey } func (f *fakeInfoProtocol) GetValue(channel via.Channel, param uint8) ([]byte, error) { key := infoKey{channel: channel, param: param} f.calls = append(f.calls, key) if err := f.errors[key]; err != nil { return nil, err } return append([]byte(nil), f.values[key]...), nil } func (f *fakeInfoProtocol) SetValue(via.Channel, uint8, uint8) error { return errors.New("unexpected SetValue call") } func (f *fakeInfoProtocol) SetColor(via.Channel, uint8, uint8) error { return errors.New("unexpected SetColor call") } func (f *fakeInfoProtocol) Close() error { return nil } func TestInfoSuccessfulRecord(t *testing.T) { t.Cleanup(vendoredDefinitions(t)) protocol := &fakeInfoProtocol{ values: map[infoKey][]byte{ {channel: 3, param: 1}: {160}, {channel: 3, param: 2}: {17}, {channel: 3, param: 3}: {2}, {channel: 3, param: 4}: {85, 255}, {channel: 2, param: 1}: {80}, {channel: 2, param: 2}: {4}, {channel: 2, param: 3}: {1}, {channel: 2, param: 4}: {0, 128}, }, errors: make(map[infoKey]error), } zones := []via.Channel{via.ChannelRgbMatrix, via.ChannelRgblight} got, err := readInfo(protocol, zones, impact80Display(), impact80Catalog(t)) if err != nil { t.Fatalf("readInfo() unexpected error: %v", err) } want := infoOutput{ Enabled: true, Mode: "rainbow_moving_chevron", Brightness: 160, Speed: 2, Zones: []zoneInfo{ { Zone: "backlight", Channel: 3, Enabled: true, Effect: "rainbow_moving_chevron", EffectID: 17, Brightness: 160, Speed: 2, Color: infoColor{Hue: 85, Saturation: 255}, }, { Zone: "logo", Channel: 2, Enabled: true, Effect: "breathe", EffectID: 4, Brightness: 80, Speed: 1, Color: infoColor{Hue: 0, Saturation: 128}, }, }, } if !reflect.DeepEqual(got, want) { t.Fatalf("readInfo() = %+v, want %+v", got, want) } data, err := json.Marshal(got) if err != nil { t.Fatalf("json.Marshal() error = %v", err) } wantJSON := `{"enabled":true,"mode":"rainbow_moving_chevron","brightness":160,"speed":2,"zones":[{"zone":"backlight","channel":3,"enabled":true,"effect":"rainbow_moving_chevron","effectId":17,"brightness":160,"speed":2,"color":{"hue":85,"saturation":255}},{"zone":"logo","channel":2,"enabled":true,"effect":"breathe","effectId":4,"brightness":80,"speed":1,"color":{"hue":0,"saturation":128}}]}` if string(data) != wantJSON { t.Errorf("JSON = %s, want %s", data, wantJSON) } } func TestInfoErrorRecord(t *testing.T) { t.Cleanup(vendoredDefinitions(t)) protocol := &fakeInfoProtocol{ values: map[infoKey][]byte{ {channel: 2, param: 1}: {120}, {channel: 2, param: 2}: {4}, {channel: 2, param: 3}: {3}, {channel: 2, param: 4}: {10, 20}, {channel: 3, param: 1}: {90}, {channel: 3, param: 2}: {5}, {channel: 3, param: 3}: {4}, {channel: 3, param: 4}: {30, 40}, }, errors: map[infoKey]error{ {channel: 4, param: 1}: errors.New("side unavailable"), }, } zones := []via.Channel{via.ChannelRgblight, via.ChannelAudio, via.ChannelRgbMatrix} got, err := readInfo(protocol, zones, impact80Display(), impact80Catalog(t)) if err == nil { t.Fatal("readInfo() expected error, got nil") } want := infoOutput{ Enabled: true, Mode: "breathe", Brightness: 120, Speed: 3, Zones: []zoneInfo{ { Zone: "logo", Channel: 2, Enabled: true, Effect: "breathe", EffectID: 4, Brightness: 120, Speed: 3, Color: infoColor{Hue: 10, Saturation: 20}, }, { Zone: "side", Channel: 4, Error: "side unavailable", }, { // The vendor spells this channel's breathing effect differently // from the other two channels': "breathing" here and "breathe" // there. Both are the vendor's own, and the tool reports each // channel as its definition writes it. Zone: "backlight", Channel: 3, Enabled: true, Effect: "breathing", EffectID: 5, Brightness: 90, Speed: 4, Color: infoColor{Hue: 30, Saturation: 40}, }, }, } if !reflect.DeepEqual(got, want) { t.Fatalf("readInfo() = %+v, want %+v", got, want) } data, err := json.Marshal(got) if err != nil { t.Fatalf("json.Marshal() error = %v", err) } var envelope struct { Zones []map[string]json.RawMessage `json:"zones"` } if err := json.Unmarshal(data, &envelope); err != nil { t.Fatalf("json.Unmarshal() error = %v", err) } if len(envelope.Zones) != 3 { t.Fatalf("zones = %d, want 3", len(envelope.Zones)) } if _, ok := envelope.Zones[1]["error"]; !ok { t.Errorf("error zone JSON = %v, want error key", envelope.Zones[1]) } } func TestInfoCommandPrintsJSONBeforeReturningError(t *testing.T) { withJSON(t) protocol := &fakeInfoProtocol{ values: map[infoKey][]byte{ {channel: 2, param: 1}: {120}, {channel: 2, param: 2}: {4}, {channel: 2, param: 3}: {3}, {channel: 2, param: 4}: {10, 20}, {channel: 3, param: 1}: {90}, {channel: 3, param: 2}: {5}, {channel: 3, param: 3}: {4}, {channel: 3, param: 4}: {30, 40}, }, errors: map[infoKey]error{ {channel: 4, param: 1}: errors.New("side unavailable"), }, } t.Cleanup(stubOpenTarget(t, protocol, impact80Display(), impact80Channels(), 0x36B0, 0x309F)) var stdout bytes.Buffer var stderr bytes.Buffer cmd := NewInfoCmd() cmd.SetOut(&stdout) cmd.SetErr(&stderr) cmd.SilenceErrors = true cmd.SilenceUsage = true cmd.SetArgs(nil) err := cmd.Execute() if err == nil { t.Fatal("Execute() expected query error, got nil") } if !strings.Contains(stdout.String(), `"zones"`) || !strings.Contains(stdout.String(), `"error"`) { t.Errorf("stdout = %q, want JSON containing zones and error", stdout.String()) } if stderr.Len() != 0 { t.Errorf("stderr = %q, want no Cobra usage/error noise", stderr.String()) } }