package main import ( "bytes" "path/filepath" "reflect" "strings" "testing" intrgb "netdome.biz/paul/qmk-rgb/internal/rgb" "netdome.biz/paul/qmk-rgb/internal/via" ) // impact80Catalog is the catalog of the board these cases describe, which comes // from the vendored definition file: that file is where every effect name now // comes from. func impact80Catalog(t *testing.T) *intrgb.Catalog { t.Helper() def, err := intrgb.LoadDefinition(filepath.Join("..", "..", "definitions", "impact80.json")) if err != nil { t.Fatalf("LoadDefinition(definitions/impact80.json) error = %v", err) } return def.Catalog } // A name the board has but a named channel does not is refused, however many // channels can do it. `effect all` asks for every channel, so a name only one of // them has is a request the tool cannot carry out rather than one channel written // and reported as done. func TestResolveEffectTargetsRefusesANameNotEveryNamedChannelHas(t *testing.T) { targets, skipped, err := resolveEffectTargets(impact80Catalog(t), "rainbow_moving_chevron", impact80Channels(), true) if err == nil { t.Fatal("resolveEffectTargets() expected error, got nil") } if len(targets) != 0 { t.Errorf("resolveEffectTargets() targets = %v, want none", targets) } if len(skipped) != 0 { t.Errorf("resolveEffectTargets() skipped = %v, want none", skipped) } } // Without a channel named, a name the board does not have everywhere is skipped // with a warning, which is the shape `load` uses when it applies a whole profile. func TestResolveEffectTargetsSkipsWhenNoChannelWasNamed(t *testing.T) { targets, skipped, err := resolveEffectTargets(impact80Catalog(t), "rainbow_moving_chevron", impact80Channels(), false) if err != nil { t.Fatalf("resolveEffectTargets() unexpected error: %v", err) } wantTargets := []intrgb.EffectTarget{{Channel: via.ChannelRgbMatrix, ID: 17}} if !reflect.DeepEqual(targets, wantTargets) { t.Errorf("resolveEffectTargets() targets = %v, want %v", targets, wantTargets) } wantSkipped := []string{"rgblight", "audio"} if !reflect.DeepEqual(skipped, wantSkipped) { t.Errorf("resolveEffectTargets() skipped = %v, want %v", skipped, wantSkipped) } } func TestResolveEffectTargetsBreathing(t *testing.T) { targets, skipped, err := resolveEffectTargets(impact80Catalog(t), "breathing", impact80Channels(), true) if err != nil { t.Fatalf("resolveEffectTargets() unexpected error: %v", err) } wantTargets := []intrgb.EffectTarget{ {Channel: via.ChannelRgblight, ID: 4}, {Channel: via.ChannelRgbMatrix, ID: 5}, {Channel: via.ChannelAudio, ID: 4}, } if !reflect.DeepEqual(targets, wantTargets) { t.Errorf("resolveEffectTargets() targets = %v, want %v", targets, wantTargets) } if len(skipped) != 0 { t.Errorf("resolveEffectTargets() skipped = %v, want none", skipped) } } func TestResolveEffectTargetsExplicitUnsupported(t *testing.T) { cases := []struct { name string zones []via.Channel }{ {"rainbow_moving_chevron", []via.Channel{via.ChannelRgblight}}, {"rainbow_wave", []via.Channel{via.ChannelRgbMatrix}}, {"splash", []via.Channel{via.ChannelAudio}}, } for _, tc := range cases { t.Run(tc.name+string(tc.zones[0]), func(t *testing.T) { targets, skipped, err := resolveEffectTargets(impact80Catalog(t), tc.name, tc.zones, true) if err == nil { t.Fatal("resolveEffectTargets() expected error, got nil") } if len(targets) != 0 { t.Errorf("resolveEffectTargets() targets = %v, want none", targets) } if len(skipped) != 0 { t.Errorf("resolveEffectTargets() skipped = %v, want none", skipped) } }) } } func TestResolveEffectTargetsUnknownName(t *testing.T) { targets, skipped, err := resolveEffectTargets(impact80Catalog(t), "not_an_effect", impact80Channels(), true) if err == nil { t.Fatal("resolveEffectTargets() expected error, got nil") } if len(targets) != 0 { t.Errorf("resolveEffectTargets() targets = %v, want none", targets) } if len(skipped) != 0 { t.Errorf("resolveEffectTargets() skipped = %v, want none", skipped) } } func TestResolveEffectTargetsStaticCompatibility(t *testing.T) { targets, skipped, err := resolveEffectTargets(impact80Catalog(t), "static", impact80Channels(), true) if err != nil { t.Fatalf("resolveEffectTargets() unexpected error: %v", err) } wantTargets := []intrgb.EffectTarget{ {Channel: via.ChannelRgblight, ID: 5}, {Channel: via.ChannelRgbMatrix, ID: 1}, {Channel: via.ChannelAudio, ID: 5}, } if !reflect.DeepEqual(targets, wantTargets) { t.Errorf("resolveEffectTargets() targets = %v, want %v", targets, wantTargets) } if len(skipped) != 0 { t.Errorf("resolveEffectTargets() skipped = %v, want none", skipped) } } func executeEffectCommand(t *testing.T, protocol rgbProtocol, zone string, args ...string) (string, string, error) { t.Helper() t.Cleanup(stubOpenTarget(t, protocol, impact80Display(), impact80Channels(), 0x36B0, 0x309F)) var stdout bytes.Buffer var stderr bytes.Buffer cmd := NewEffectCmd() cmd.SetOut(&stdout) cmd.SetErr(&stderr) cmd.SilenceErrors = true cmd.SilenceUsage = true // The zone is the first argument, so the effect name follows it, which is // the spelling a user would type. cmd.SetArgs(append([]string{zone}, args...)) err := cmd.Execute() return stdout.String(), stderr.String(), err } // `effect all breathing` reaches every channel, because the name means the same // index on all three subsystems of this board. func TestEffectAllWritesEveryChannelTheNameIsOn(t *testing.T) { t.Cleanup(vendoredDefinitions(t)) protocol := &fakeZoneProtocol{failAt: -1} stdout, _, err := executeEffectCommand(t, protocol, "all", "breathing") if err != nil { t.Fatalf("Execute() unexpected error: %v", err) } if !strings.Contains(stdout, `Effect set to "breathing"`) { t.Errorf("stdout = %q, want success output", stdout) } if len(protocol.reports) != 3 { t.Fatalf("reports = %v, want one per channel", protocol.reports) } want := []commandReport{ {channel: 2, param: 2, value: 4}, {channel: 3, param: 2, value: 5}, {channel: 4, param: 2, value: 4}, } if !reflect.DeepEqual(protocol.reports, want) { t.Errorf("reports = %v, want %v", protocol.reports, want) } } // Several zones in one argument reach exactly those channels and no others. func TestEffectWritesEveryChannelOfAZoneList(t *testing.T) { t.Cleanup(vendoredDefinitions(t)) protocol := &fakeZoneProtocol{failAt: -1} _, _, err := executeEffectCommand(t, protocol, "side,logo", "breathing") if err != nil { t.Fatalf("Execute() unexpected error: %v", err) } want := []commandReport{ {channel: 2, param: 2, value: 4}, {channel: 4, param: 2, value: 4}, } if !reflect.DeepEqual(protocol.reports, want) { t.Errorf("reports = %v, want %v", protocol.reports, want) } } func TestEffectCommandWritesNothingForAnUnsupportedEffect(t *testing.T) { protocol := &fakeZoneProtocol{failAt: -1} stdout, _, err := executeEffectCommand(t, protocol, "logo", "rainbow_moving_chevron") if err == nil { t.Fatal("Execute() expected error, got nil") } if len(protocol.reports) != 0 { t.Errorf("reports = %v, want no writes before rejection", protocol.reports) } if stdout != "" { t.Errorf("stdout = %q, want no success output", stdout) } } func TestEffectCommandSuppressesSuccessAfterLaterWriteFailure(t *testing.T) { t.Cleanup(vendoredDefinitions(t)) protocol := &fakeZoneProtocol{failAt: 1} stdout, _, err := executeEffectCommand(t, protocol, "all", "breathing") if err == nil { t.Fatal("Execute() expected write error, got nil") } if len(protocol.reports) != 1 { t.Fatalf("reports = %v, want one report before failure", protocol.reports) } if strings.Contains(stdout, "Effect set") { t.Errorf("stdout = %q, want success output suppressed", stdout) } }