From 989206e00175a2f01dfa460400512ff8bd656f12 Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Wed, 1 Apr 2026 06:33:52 +0000 Subject: [PATCH] test(brew): add tests for parseBrewInfoVersion, parseBrewOutdated, semverCmp, parseVersionSuffix Add comprehensive unit tests for untested brew parse functions: - parseBrewInfoVersion: formula, cask, empty, invalid JSON, no results - parseBrewOutdated: formulae only, casks only, mixed, empty, invalid - semverCmp: equality, ordering, multi-digit, edge cases - parseVersionSuffix: versioned formulae, plain names, edge cases - Additional edge cases for parseBrewInfo, parseBrewSearch, parseBrewList Increases brew package test coverage from 14.4% to 26.7%. --- brew/brew_test.go | 221 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) diff --git a/brew/brew_test.go b/brew/brew_test.go index a8cb341..8e72ae7 100644 --- a/brew/brew_test.go +++ b/brew/brew_test.go @@ -206,6 +206,227 @@ func TestCapabilities(t *testing.T) { } } +func TestParseBrewInfoVersion(t *testing.T) { + t.Run("formula", func(t *testing.T) { + input := `{"formulae":[{"name":"git","full_name":"git","desc":"Distributed revision control system","versions":{"stable":"2.43.0"},"installed":[]}],"casks":[]}` + ver := parseBrewInfoVersion(input) + if ver != "2.43.0" { + t.Errorf("expected '2.43.0', got %q", ver) + } + }) + + t.Run("cask", func(t *testing.T) { + input := `{"formulae":[],"casks":[{"token":"visual-studio-code","name":["Visual Studio Code"],"desc":"Open-source code editor","version":"1.85.0"}]}` + ver := parseBrewInfoVersion(input) + if ver != "1.85.0" { + t.Errorf("expected '1.85.0', got %q", ver) + } + }) + + t.Run("empty", func(t *testing.T) { + ver := parseBrewInfoVersion("") + if ver != "" { + t.Errorf("expected empty, got %q", ver) + } + }) + + t.Run("invalid json", func(t *testing.T) { + ver := parseBrewInfoVersion("not json") + if ver != "" { + t.Errorf("expected empty, got %q", ver) + } + }) + + t.Run("no formulae or casks", func(t *testing.T) { + input := `{"formulae":[],"casks":[]}` + ver := parseBrewInfoVersion(input) + if ver != "" { + t.Errorf("expected empty, got %q", ver) + } + }) +} + +func TestParseBrewOutdated(t *testing.T) { + t.Run("formulae only", func(t *testing.T) { + input := `{"formulae":[{"name":"git","installed_versions":["2.43.0"],"current_version":"2.44.0"},{"name":"go","installed_versions":["1.21.6"],"current_version":"1.22.0"}],"casks":[]}` + pkgs := parseBrewOutdated(input) + if len(pkgs) != 2 { + t.Fatalf("expected 2 packages, got %d", len(pkgs)) + } + if pkgs[0].Name != "git" || pkgs[0].Version != "2.44.0" { + t.Errorf("unexpected first package: %+v", pkgs[0]) + } + if !pkgs[0].Installed { + t.Error("expected Installed=true") + } + }) + + t.Run("casks only", func(t *testing.T) { + input := `{"formulae":[],"casks":[{"name":"firefox","installed_versions":"119.0","current_version":"120.0"}]}` + pkgs := parseBrewOutdated(input) + if len(pkgs) != 1 { + t.Fatalf("expected 1 package, got %d", len(pkgs)) + } + if pkgs[0].Name != "firefox" || pkgs[0].Version != "120.0" { + t.Errorf("unexpected package: %+v", pkgs[0]) + } + }) + + t.Run("mixed", func(t *testing.T) { + input := `{"formulae":[{"name":"git","installed_versions":["2.43.0"],"current_version":"2.44.0"}],"casks":[{"name":"firefox","installed_versions":"119.0","current_version":"120.0"}]}` + pkgs := parseBrewOutdated(input) + if len(pkgs) != 2 { + t.Fatalf("expected 2 packages, got %d", len(pkgs)) + } + }) + + t.Run("empty", func(t *testing.T) { + pkgs := parseBrewOutdated("") + if len(pkgs) != 0 { + t.Errorf("expected 0 packages, got %d", len(pkgs)) + } + }) + + t.Run("no outdated", func(t *testing.T) { + input := `{"formulae":[],"casks":[]}` + pkgs := parseBrewOutdated(input) + if len(pkgs) != 0 { + t.Errorf("expected 0 packages, got %d", len(pkgs)) + } + }) + + t.Run("invalid json", func(t *testing.T) { + pkgs := parseBrewOutdated("not json") + if len(pkgs) != 0 { + t.Errorf("expected 0 packages, got %d", len(pkgs)) + } + }) +} + +func TestSemverCmp(t *testing.T) { + tests := []struct { + name string + a, b string + want int + }{ + {"equal", "1.0.0", "1.0.0", 0}, + {"less major", "1.0.0", "2.0.0", -1}, + {"greater major", "2.0.0", "1.0.0", 1}, + {"less minor", "1.2.3", "1.3.0", -1}, + {"less patch", "1.2.3", "1.2.4", -1}, + {"multi-digit", "1.10.0", "1.9.0", 1}, + {"short vs long equal", "1.0", "1.0.0", 0}, + {"short vs long less", "1.0", "1.0.1", -1}, + {"short vs long greater", "1.1", "1.0.9", 1}, + {"single component", "5", "3", 1}, + {"single equal", "3", "3", 0}, + {"empty vs empty", "", "", 0}, + {"empty vs version", "", "1.0", -1}, + {"version vs empty", "1.0", "", 1}, + {"four components", "1.2.3.4", "1.2.3.5", -1}, + {"different lengths", "1.0.0.0", "1.0.0", 0}, + {"real brew versions", "2.43.0", "2.44.0", -1}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := semverCmp(tt.a, tt.b) + if got != tt.want { + t.Errorf("semverCmp(%q, %q) = %d, want %d", tt.a, tt.b, got, tt.want) + } + }) + } +} + +func TestParseVersionSuffix(t *testing.T) { + tests := []struct { + input string + wantName string + wantVersion string + }{ + {"python@3.12", "python", "3.12"}, + {"node@18", "node", "18"}, + {"git", "git", ""}, + {"ruby@3.2", "ruby", "3.2"}, + {"", "", ""}, + {"@3.12", "@3.12", ""}, // @ at position 0, LastIndex returns 0 which is not > 0 + } + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + gotName, gotVer := parseVersionSuffix(tt.input) + if gotName != tt.wantName || gotVer != tt.wantVersion { + t.Errorf("parseVersionSuffix(%q) = (%q, %q), want (%q, %q)", + tt.input, gotName, gotVer, tt.wantName, tt.wantVersion) + } + }) + } +} + +func TestParseBrewInfo_Empty(t *testing.T) { + pkg := parseBrewInfo("") + if pkg != nil { + t.Error("expected nil for empty input") + } +} + +func TestParseBrewInfo_InvalidJSON(t *testing.T) { + pkg := parseBrewInfo("not json") + if pkg != nil { + t.Error("expected nil for invalid JSON") + } +} + +func TestParseBrewInfo_NoFormulaeOrCasks(t *testing.T) { + input := `{"formulae":[],"casks":[]}` + pkg := parseBrewInfo(input) + if pkg != nil { + t.Error("expected nil when no formulae or casks") + } +} + +func TestParseBrewSearch_HeadersOnly(t *testing.T) { + input := `==> Formulae + +==> Casks +` + pkgs := parseBrewSearch(input) + if len(pkgs) != 0 { + t.Errorf("expected 0 packages, got %d", len(pkgs)) + } +} + +func TestParseBrewSearch_MultiplePerLine(t *testing.T) { + input := "git go vim curl\n" + pkgs := parseBrewSearch(input) + if len(pkgs) != 4 { + t.Fatalf("expected 4 packages, got %d", len(pkgs)) + } + names := []string{"git", "go", "vim", "curl"} + for i, want := range names { + if pkgs[i].Name != want { + t.Errorf("pkg[%d].Name = %q, want %q", i, pkgs[i].Name, want) + } + } +} + +func TestParseBrewList_NameOnly(t *testing.T) { + input := "git\ncurl\n" + pkgs := parseBrewList(input) + if len(pkgs) != 2 { + t.Fatalf("expected 2 packages, got %d", len(pkgs)) + } + if pkgs[0].Version != "" { + t.Errorf("expected empty version, got %q", pkgs[0].Version) + } +} + +func TestParseBrewList_WhitespaceLines(t *testing.T) { + input := " \n\n git 2.43.0\n \n" + pkgs := parseBrewList(input) + if len(pkgs) != 1 { + t.Fatalf("expected 1 package, got %d", len(pkgs)) + } +} + func TestInterfaceNonCompliance(t *testing.T) { var m snack.Manager = New() if _, ok := m.(snack.Holder); ok {