From 8fc2103a0e512202b8c7caf638792f33905978cb Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:28:00 -0700 Subject: [PATCH 01/23] use testify in the astgen tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/astgen/builders_test.go | 60 ++++++++++++-------------------- internal/astgen/http_test.go | 27 ++++++-------- internal/astgen/nearest_test.go | 7 ++-- internal/astgen/suggest_test.go | 4 +-- 4 files changed, 38 insertions(+), 60 deletions(-) diff --git a/internal/astgen/builders_test.go b/internal/astgen/builders_test.go index 6e582c8f..eb99d875 100644 --- a/internal/astgen/builders_test.go +++ b/internal/astgen/builders_test.go @@ -9,6 +9,9 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/astgen" "github.com/typelate/muxt/internal/source" ) @@ -56,12 +59,8 @@ func TestConvertToString(t *testing.T) { t.Run(tt.name, func(t *testing.T) { im := imports{} got, err := astgen.ConvertToString(im, ast.NewIdent("v"), source.NewType(types.Typ[tt.kind])) - if err != nil { - t.Fatal(err) - } - if s := astgen.Format(got); s != tt.want { - t.Errorf("ConvertToString(%s) = %q, want %q", tt.name, s, tt.want) - } + require.NoError(t, err) + assert.Equal(t, tt.want, astgen.Format(got), "ConvertToString(%s)", tt.name) }) } @@ -75,9 +74,7 @@ func TestConvertToString(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { _, err := astgen.ConvertToString(imports{}, ast.NewIdent("v"), source.NewType(tt.tp)) - if err == nil || err.Error() != tt.want { - t.Errorf("ConvertToString(%s) error = %v, want %q", tt.name, err, tt.want) - } + assert.EqualError(t, err, tt.want, "ConvertToString(%s) error", tt.name) }) } } @@ -98,12 +95,8 @@ func TestStrconvCalls(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { im := imports{} - if got := astgen.Format(tt.call(im)); got != tt.want { - t.Errorf("%s call = %q, want %q", tt.name, got, tt.want) - } - if im["strconv"] != "strconv" { - t.Errorf("%s did not register the strconv import: %v", tt.name, im) - } + assert.Equal(t, tt.want, astgen.Format(tt.call(im)), "%s call", tt.name) + assert.Equal(t, "strconv", im["strconv"], "%s should register the strconv import: %v", tt.name, im) }) } } @@ -155,17 +148,11 @@ func TestBuilders(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { im := imports{} - if got := astgen.Format(tt.node(im)); got != tt.want { - t.Errorf("%s = %q, want %q", tt.name, got, tt.want) - } + assert.Equal(t, tt.want, astgen.Format(tt.node(im)), tt.name) for _, pkg := range tt.imports { - if _, ok := im[pkg]; !ok { - t.Errorf("%s did not register the %s import: %v", tt.name, pkg, im) - } - } - if len(im) != len(tt.imports) { - t.Errorf("%s registered imports %v, want %v", tt.name, im, tt.imports) + assert.Contains(t, im, pkg, "%s should register the %s import", tt.name, pkg) } + assert.Len(t, im, len(tt.imports), "%s registered imports %v, want %v", tt.name, im, tt.imports) }) } } @@ -173,9 +160,7 @@ func TestBuilders(t *testing.T) { func TestGetBufferFromPool(t *testing.T) { stmts := astgen.GetBufferFromPool(imports{}, "pool", "buf") want := "{\n\tbuf := pool.Get().(*bytes.Buffer)\n\tbuf.Reset()\n\tdefer pool.Put(buf)\n}" - if got := astgen.Format(&ast.BlockStmt{List: stmts}); got != want { - t.Errorf("GetBufferFromPool = %q, want %q", got, want) - } + assert.Equal(t, want, astgen.Format(&ast.BlockStmt{List: stmts}), "GetBufferFromPool") } func TestFindFieldWithName(t *testing.T) { @@ -193,22 +178,21 @@ func TestFindFieldWithName(t *testing.T) { {name: "c", wantType: "string", wantOK: true}, {name: "d"}, } { - field, ok := astgen.FindFieldWithName(list, tt.name) - if ok != tt.wantOK { - t.Errorf("FindFieldWithName(%q) found = %v, want %v", tt.name, ok, tt.wantOK) - continue - } - if ok && astgen.Format(field.Type) != tt.wantType { - t.Errorf("FindFieldWithName(%q).Type = %s, want %s", tt.name, astgen.Format(field.Type), tt.wantType) - } + t.Run(tt.name, func(t *testing.T) { + field, ok := astgen.FindFieldWithName(list, tt.name) + if !assert.Equal(t, tt.wantOK, ok, "FindFieldWithName(%q) found", tt.name) { + return + } + if ok { + assert.Equal(t, tt.wantType, astgen.Format(field.Type), "FindFieldWithName(%q).Type", tt.name) + } + }) } } func TestFormatReportsUnparsableNodes(t *testing.T) { got := astgen.Format(&ast.BasicLit{Kind: token.INT, Value: "not a number"}) - if !strings.HasPrefix(got, "formatting error:") { - t.Errorf("Format(bad literal) = %q, want it to report a formatting error", got) - } + assert.True(t, strings.HasPrefix(got, "formatting error:"), "Format(bad literal) = %q, want it to report a formatting error", got) } func TestTypeFormatter(t *testing.T) { diff --git a/internal/astgen/http_test.go b/internal/astgen/http_test.go index 3adc74c8..16b160c9 100644 --- a/internal/astgen/http_test.go +++ b/internal/astgen/http_test.go @@ -1,6 +1,11 @@ package astgen -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) // TestHTTPStatusName states which names resolve to a status code. A // template name may write the constant with or without its package @@ -21,12 +26,8 @@ func TestHTTPStatusName(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { got, err := HTTPStatusName(tt.name) - if err != nil { - t.Fatalf("HTTPStatusName(%q) = %v", tt.name, err) - } - if got != tt.want { - t.Errorf("HTTPStatusName(%q) = %d, want %d", tt.name, got, tt.want) - } + require.NoError(t, err, "HTTPStatusName(%q)", tt.name) + assert.Equal(t, tt.want, got, "HTTPStatusName(%q)", tt.name) }) } } @@ -46,15 +47,9 @@ func TestHTTPStatusNameRejectsWhatIsNotAConstant(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { code, err := HTTPStatusName(tt.name) - if err == nil { - t.Fatalf("HTTPStatusName(%q) = %d, want an error", tt.name, code) - } - if err.Error() != tt.want { - t.Errorf("HTTPStatusName(%q) = %q, want %q", tt.name, err, tt.want) - } - if code != 0 { - t.Errorf("HTTPStatusName(%q) = %d, want no code beside the error", tt.name, code) - } + require.Error(t, err, "HTTPStatusName(%q) = %d, want an error", tt.name, code) + assert.Equal(t, tt.want, err.Error(), "HTTPStatusName(%q) error", tt.name) + assert.Zero(t, code, "HTTPStatusName(%q) code beside the error", tt.name) }) } } diff --git a/internal/astgen/nearest_test.go b/internal/astgen/nearest_test.go index 8ac0fd5d..2be001bd 100644 --- a/internal/astgen/nearest_test.go +++ b/internal/astgen/nearest_test.go @@ -3,6 +3,8 @@ package astgen_test import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/typelate/muxt/internal/astgen" ) @@ -24,9 +26,8 @@ func TestNearestStringExactAndTiedMatches(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { got, found := astgen.NearestString(tt.target, tt.candidates) - if found != tt.found || got != tt.want { - t.Errorf("NearestString(%q, %q) = %q, %v; want %q, %v", tt.target, tt.candidates, got, found, tt.want, tt.found) - } + assert.Equal(t, tt.found, found, "NearestString(%q, %q) found", tt.target, tt.candidates) + assert.Equal(t, tt.want, got, "NearestString(%q, %q)", tt.target, tt.candidates) }) } } diff --git a/internal/astgen/suggest_test.go b/internal/astgen/suggest_test.go index 09777688..a35bc10d 100644 --- a/internal/astgen/suggest_test.go +++ b/internal/astgen/suggest_test.go @@ -60,9 +60,7 @@ func TestNearestString(t *testing.T) { t.Run(tt.Name, func(t *testing.T) { suggestion, found := astgen.NearestString(tt.Target, tt.Candidates) assert.Equal(t, tt.Found, found, "NearestString(%q, %v)", tt.Target, tt.Candidates) - if tt.Found { - assert.Equal(t, tt.Suggestion, suggestion) - } + assert.Equal(t, tt.Suggestion, suggestion, "NearestString(%q, %v) suggestion", tt.Target, tt.Candidates) }) } } From cef497162bb571760711200cce5f4d19be97c073 Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:28:41 -0700 Subject: [PATCH 02/23] split TestTypeFormatter into one-behavior subtests Each subtest builds its own formatter, since Qualifier memoizes the name it assigned per import path. The memoization sequence stays in one subtest. Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/astgen/builders_test.go | 111 +++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 35 deletions(-) diff --git a/internal/astgen/builders_test.go b/internal/astgen/builders_test.go index eb99d875..33ff9682 100644 --- a/internal/astgen/builders_test.go +++ b/internal/astgen/builders_test.go @@ -195,45 +195,86 @@ func TestFormatReportsUnparsableNodes(t *testing.T) { assert.True(t, strings.HasPrefix(got, "formatting error:"), "Format(bad literal) = %q, want it to report a formatting error", got) } -func TestTypeFormatter(t *testing.T) { - server := types.NewPackage("example.com/server", "server") - other := types.NewPackage("example.com/other", "other") - colliding := types.NewPackage("example.com/x/other", "other") +func newPackage(path, name string) *types.Package { + return types.NewPackage(path, name) +} - tf := astgen.NewTypeFormatter(server.Path()) - tf.Idents = []string{"other"} +// TestTypeFormatter states how a generated file names the packages its types +// come from. Qualifier remembers the name it gave each import path, so the +// subtests that depend on that build their own formatter and say so. +func TestTypeFormatter(t *testing.T) { + const outputPath = "example.com/server" - if got := tf.Qualifier(nil); got != "" { - t.Errorf("Qualifier(nil) = %q, want empty", got) - } - if got := tf.Qualifier(server); got != "" { - t.Errorf("Qualifier(output package) = %q, want empty", got) - } - if got := tf.Qualifier(other); got != "other1" { - t.Errorf("Qualifier(other) = %q, want other1 because the file declares other", got) + // newFormatter writes for the output package with the identifiers the + // file already declares. + newFormatter := func(idents ...string) *astgen.TypeFormatter { + tf := astgen.NewTypeFormatter(outputPath) + tf.Idents = idents + return tf } - if got := tf.Qualifier(other); got != "other1" { - t.Errorf("Qualifier(other) again = %q, want the name it was given", got) - } - tf.Idents = []string{"third", "third1", "third2"} - if got := tf.Qualifier(types.NewPackage("example.com/third", "third")); got != "third3" { - t.Errorf("Qualifier(third) = %q, want third3, the first name the file does not declare", got) - } - tf.Idents = nil - if got := tf.Qualifier(colliding); got != "other" { - t.Errorf("Qualifier(colliding) = %q, want other", got) + // collectedFormatter has already been asked about three import paths, + // two of which share the name other. + collectedFormatter := func() *astgen.TypeFormatter { + tf := newFormatter() + tf.Imports = map[string]string{ + "example.com/x/other": "other", + "example.com/third": "third3", + "example.com/other": "other1", + } + return tf } - want := "import (\n\tother1 \"example.com/other\"\n\tthird3 \"example.com/third\"\n\t\"example.com/x/other\"\n)" - if got := astgen.Format(tf.GenDecl()); got != want { - t.Errorf("GenDecl = %q, want %q", got, want) - } + t.Run("a nil package is qualified by the empty string", func(t *testing.T) { + tf := newFormatter("server") + assert.Empty(t, tf.Qualifier(nil), "Qualifier(nil)") + }) - encoded, err := json.Marshal(tf, json.Deterministic(true)) - if err != nil { - t.Fatal(err) - } - if want := `{"example.com/other":"other1","example.com/third":"third3","example.com/x/other":"other"}`; string(encoded) != want { - t.Errorf("json.Marshal(TypeFormatter) = %s, want %s", encoded, want) - } + t.Run("the output package is qualified by the empty string", func(t *testing.T) { + tf := newFormatter("server") + assert.Empty(t, tf.Qualifier(newPackage(outputPath, "server")), "Qualifier(output package)") + }) + + t.Run("a name the file declares gets the next free numeric suffix", func(t *testing.T) { + tf := newFormatter("other") + assert.Equal(t, "other1", tf.Qualifier(newPackage("example.com/other", "other")), "Qualifier(other) because the file declares other") + }) + + t.Run("a package keeps the name it was given", func(t *testing.T) { + other := newPackage("example.com/other", "other") + tf := newFormatter("other") + require.Equal(t, "other1", tf.Qualifier(other), "Qualifier(other)") + assert.Equal(t, "other1", tf.Qualifier(other), "Qualifier(other) again with the same declared names") + + tf.Idents = nil + assert.Equal(t, "other1", tf.Qualifier(other), "Qualifier(other) after the file stops declaring other") + }) + + t.Run("the suffix skips every declared name", func(t *testing.T) { + tf := newFormatter("third", "third1", "third2") + assert.Equal(t, "third3", tf.Qualifier(newPackage("example.com/third", "third")), "Qualifier(third), the first name the file does not declare") + }) + + t.Run("two packages with one name are told apart by import path", func(t *testing.T) { + tf := newFormatter("other") + require.Equal(t, "other1", tf.Qualifier(newPackage("example.com/other", "other")), "Qualifier(example.com/other)") + + tf.Idents = nil + assert.Equal(t, "other", tf.Qualifier(newPackage("example.com/x/other", "other")), "Qualifier(example.com/x/other)") + assert.Equal(t, map[string]string{ + "example.com/other": "other1", + "example.com/x/other": "other", + }, tf.Imports, "each import path keeps its own name") + }) + + t.Run("GenDecl lists imports sorted by path and omits an alias equal to the last path element", func(t *testing.T) { + want := "import (\n\tother1 \"example.com/other\"\n\tthird3 \"example.com/third\"\n\t\"example.com/x/other\"\n)" + assert.Equal(t, want, astgen.Format(collectedFormatter().GenDecl()), "GenDecl") + }) + + t.Run("JSON marshals the path to name map with deterministic key order", func(t *testing.T) { + encoded, err := json.Marshal(collectedFormatter(), json.Deterministic(true)) + require.NoError(t, err) + want := `{"example.com/other":"other1","example.com/third":"third3","example.com/x/other":"other"}` + assert.Equal(t, want, string(encoded), "json.Marshal(TypeFormatter)") + }) } From 2fd138457a639c8f737d22983ca2b588d3a70e93 Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:29:00 -0700 Subject: [PATCH 03/23] use testify in the asteval test Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/asteval/reflect_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/asteval/reflect_test.go b/internal/asteval/reflect_test.go index 1d498175..91781243 100644 --- a/internal/asteval/reflect_test.go +++ b/internal/asteval/reflect_test.go @@ -4,8 +4,9 @@ import ( "go/token" "go/types" "strconv" - "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestCheckParses(t *testing.T) { @@ -52,12 +53,11 @@ func TestCheckParses(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { err := CheckParses(tt.val, tt.tp) - switch { - case tt.wantErr == "" && err != nil: - t.Errorf("CheckParses(%q, %s) = %v, want no error", tt.val, tt.tp, err) - case tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)): - t.Errorf("CheckParses(%q, %s) = %v, want error containing %q", tt.val, tt.tp, err, tt.wantErr) + if tt.wantErr == "" { + assert.NoError(t, err, "CheckParses(%q, %s)", tt.val, tt.tp) + return } + assert.ErrorContains(t, err, tt.wantErr, "CheckParses(%q, %s)", tt.val, tt.tp) }) } } From 8823c41cc95001d8c106b09502ce82ef7faa81e2 Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:29:17 -0700 Subject: [PATCH 04/23] name the header Args cases and version failures Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/header/header_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/header/header_test.go b/internal/header/header_test.go index 58496545..511ebdb4 100644 --- a/internal/header/header_test.go +++ b/internal/header/header_test.go @@ -64,8 +64,10 @@ func TestHeaderArgs(t *testing.T) { {command: "", want: []string{}}, {command: "--a=b --c", want: []string{"--a=b", "--c"}}, } { - got := header.Header{Command: tt.command}.Args() - assert.Equal(t, tt.want, got, "Header{Command: %q}.Args()", tt.command) + t.Run(tt.command, func(t *testing.T) { + got := header.Header{Command: tt.command}.Args() + assert.Equal(t, tt.want, got, "Header{Command: %q}.Args()", tt.command) + }) } } @@ -86,7 +88,7 @@ func TestRoundTrip(t *testing.T) { lines := splitLines(text) got, ok := header.Parse(lines[0], lines[1]) require.True(t, ok, "Parse of Format(%q, %q) = %q", tt.args, tt.version, text) - assert.Equal(t, tt.version, got.Version) + assert.Equal(t, tt.version, got.Version, "version of Format(%q, %q)", tt.args, tt.version) want := tt.args if want == nil { want = []string{} From 1a4c2916c0a715c92dd5a33f8448a56bcd74eaed Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:30:09 -0700 Subject: [PATCH 05/23] use testify in the generate file and import tests TestImports compared got and want the wrong way round; the expected value now comes first. Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/generate/file_import_test.go | 49 +++++++++++++++----------- internal/generate/file_test.go | 24 ++++++------- internal/generate/format_test.go | 11 +++--- internal/generate/marshal_json_test.go | 23 ++++++------ 4 files changed, 57 insertions(+), 50 deletions(-) diff --git a/internal/generate/file_import_test.go b/internal/generate/file_import_test.go index 078b8a4b..a7987175 100644 --- a/internal/generate/file_import_test.go +++ b/internal/generate/file_import_test.go @@ -3,30 +3,37 @@ package generate import ( "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestFileImport(t *testing.T) { - file := scalarTestFile(t) + t.Run("a path is named for its last element and keeps that name", func(t *testing.T) { + file := scalarTestFile(t) + assert.Equal(t, "http", file.Import("", "net/http"), "Import(net/http)") + assert.Equal(t, "http", file.Import("", "net/http"), "Import(net/http) again") + }) + + t.Run("a name another path holds gets a 12 character hash and keeps it", func(t *testing.T) { + file := scalarTestFile(t) + file.Import("", "net/http") + + aliased := file.Import("", "example.com/other/http") + assert.True(t, strings.HasPrefix(aliased, "http"), "Import(example.com/other/http) = %q, want it to start with http", aliased) + assert.NotEqual(t, "http", aliased, "Import(example.com/other/http)") + assert.Len(t, aliased, len("http")+12, "Import(example.com/other/http) = %q, want http followed by a 12 character hash", aliased) + assert.Equal(t, aliased, file.Import("", "example.com/other/http"), "Import(example.com/other/http) again") + }) - if got := file.Import("", "net/http"); got != "http" { - t.Errorf("Import(net/http) = %q, want http", got) - } - if got := file.Import("", "net/http"); got != "http" { - t.Errorf("Import(net/http) again = %q, want the same name", got) - } - aliased := file.Import("", "example.com/other/http") - if !strings.HasPrefix(aliased, "http") || aliased == "http" || len(aliased) != len("http")+12 { - t.Errorf("Import(example.com/other/http) = %q, want http followed by a 12 character hash", aliased) - } - if got := file.Import("", "example.com/other/http"); got != aliased { - t.Errorf("Import(example.com/other/http) again = %q, want %q", got, aliased) - } + t.Run("ImportSpecs lists the paths sorted", func(t *testing.T) { + file := scalarTestFile(t) + file.Import("", "net/http") + file.Import("", "example.com/other/http") - var paths []string - for _, spec := range file.ImportSpecs() { - paths = append(paths, spec.Path.Value) - } - if want := `"example.com/other/http" "net/http"`; strings.Join(paths, " ") != want { - t.Errorf("ImportSpecs paths = %v, want %s", paths, want) - } + var paths []string + for _, spec := range file.ImportSpecs() { + paths = append(paths, spec.Path.Value) + } + assert.Equal(t, []string{`"example.com/other/http"`, `"net/http"`}, paths, "ImportSpecs paths") + }) } diff --git a/internal/generate/file_test.go b/internal/generate/file_test.go index 5f3ef353..cb2105df 100644 --- a/internal/generate/file_test.go +++ b/internal/generate/file_test.go @@ -30,35 +30,35 @@ func TestImports(t *testing.T) { t.Run("initial add", func(t *testing.T) { file := outputFile() assert.Equal(t, "http", file.Import("http", "net/http")) - assert.Equal(t, genDecl(file), `import "net/http"`) + assert.Equal(t, `import "net/http"`, genDecl(file)) }) t.Run("initial with pkg ident", func(t *testing.T) { file := outputFile() assert.Equal(t, "p", file.Import("p", "net/http")) - assert.Equal(t, genDecl(file), `import p "net/http"`) + assert.Equal(t, `import p "net/http"`, genDecl(file)) }) t.Run("initial with empty ident", func(t *testing.T) { file := outputFile() assert.Equal(t, "http", file.Import("", "net/http")) - assert.Equal(t, genDecl(file), `import "net/http"`) + assert.Equal(t, `import "net/http"`, genDecl(file)) }) - t.Run("initial with empty ident", func(t *testing.T) { + t.Run("imports are listed sorted by path", func(t *testing.T) { file := outputFile() _ = file.Import("", "net/http") _ = file.Import("", "html/template") - assert.Equal(t, genDecl(file), `import ( + assert.Equal(t, `import ( "html/template" "net/http" -)`) +)`, genDecl(file)) }) t.Run("it respects order", func(t *testing.T) { file := outputFile() _ = file.Import("", "html/template") _ = file.Import("", "net/http") - assert.Equal(t, genDecl(file), `import ( + assert.Equal(t, `import ( "html/template" "net/http" -)`) +)`, genDecl(file)) }) t.Run("it returns the registered identifier", func(t *testing.T) { file := outputFile() @@ -78,8 +78,8 @@ func TestHTTPStatusCode(t *testing.T) { exp := astgen.HTTPStatusCode(file, 600) require.NotNil(t, exp) lit, ok := exp.(*ast.BasicLit) - require.True(t, ok) - require.Equal(t, token.INT, lit.Kind) - require.Equal(t, "600", lit.Value) - require.Empty(t, file.ImportSpecs(), "it should not add the import if it is not needed") + require.True(t, ok, "HTTPStatusCode(600) is a %T, want a literal", exp) + assert.Equal(t, token.INT, lit.Kind) + assert.Equal(t, "600", lit.Value) + assert.Empty(t, file.ImportSpecs(), "it should not add the import if it is not needed") } diff --git a/internal/generate/format_test.go b/internal/generate/format_test.go index 3f1e9471..5c674252 100644 --- a/internal/generate/format_test.go +++ b/internal/generate/format_test.go @@ -5,6 +5,9 @@ import ( "go/token" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/astgen" ) @@ -62,12 +65,8 @@ func TestFormatFileImports(t *testing.T) { f.Decls = []ast.Decl{decl} } got, err := formatFile("routes.go", f) - if err != nil { - t.Fatal(err) - } - if got != tt.want { - t.Errorf("formatFile =\n%s\nwant\n%s", got, tt.want) - } + require.NoError(t, err) + assert.Equal(t, tt.want, got, "formatFile") }) } } diff --git a/internal/generate/marshal_json_test.go b/internal/generate/marshal_json_test.go index 1ab8de0e..1858e5ad 100644 --- a/internal/generate/marshal_json_test.go +++ b/internal/generate/marshal_json_test.go @@ -1,22 +1,23 @@ package generate -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestMarshalJSONHandlerFuncRejectsTheExecuteCallback(t *testing.T) { pkg, defs := routesTestDefinitions(t, map[string]string{ "a.gohtml": `{{define "GET /e marshalJSON(Run(execute))"}}{{end}}{{define "GET /b marshalJSON(B())"}}{{end}}`, }) for _, def := range defs { - _, err := marshalJSONHandlerFunc(newFile(pkg), testConfig(), def, "td", "RoutesReceiver", "buf", "statusCode") - switch def.RawPattern() { - case "GET /e": - if want := "marshalJSON does not support the execute callback"; err == nil || err.Error() != want { - t.Errorf("marshalJSONHandlerFunc(%s) error = %v, want %q", def.RawPattern(), err, want) - } - default: - if err != nil { - t.Errorf("marshalJSONHandlerFunc(%s) = %v, want no error", def.RawPattern(), err) + t.Run(def.RawPattern(), func(t *testing.T) { + _, err := marshalJSONHandlerFunc(newFile(pkg), testConfig(), def, "td", "RoutesReceiver", "buf", "statusCode") + if def.RawPattern() == "GET /e" { + assert.EqualError(t, err, "marshalJSON does not support the execute callback", "marshalJSONHandlerFunc(%s)", def.RawPattern()) + return } - } + assert.NoError(t, err, "marshalJSONHandlerFunc(%s)", def.RawPattern()) + }) } } From 29b5050273d160d7c1bf9ef2a1eab2302728ee7b Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:30:52 -0700 Subject: [PATCH 06/23] use testify in the generate parse tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/generate/parse_arguments_test.go | 36 +++++++---------------- internal/generate/parse_form_test.go | 16 ++++------ internal/generate/parse_scalar_test.go | 15 ++++------ 3 files changed, 23 insertions(+), 44 deletions(-) diff --git a/internal/generate/parse_arguments_test.go b/internal/generate/parse_arguments_test.go index 4981ba54..b7d0c311 100644 --- a/internal/generate/parse_arguments_test.go +++ b/internal/generate/parse_arguments_test.go @@ -3,9 +3,11 @@ package generate import ( "go/ast" "go/token" - "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/astgen" "github.com/typelate/muxt/internal/muxt" ) @@ -31,9 +33,7 @@ func TestAppendParseArgumentStatementsRejectsUnresolvedCalls(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { _, err := appendParseArgumentStatements(nil, file, tt.args, "", testConfig(), tt.call, nil, nil) - if err == nil || err.Error() != tt.want { - t.Errorf("appendParseArgumentStatements error = %v, want %q", err, tt.want) - } + assert.EqualError(t, err, tt.want, "appendParseArgumentStatements error") }) } } @@ -41,9 +41,7 @@ func TestAppendParseArgumentStatementsRejectsUnresolvedCalls(t *testing.T) { func TestAppendParseArgumentStatementsRejectsUnexpectedArgumentExpressions(t *testing.T) { call := &ast.CallExpr{Fun: ast.NewIdent("F"), Args: []ast.Expr{&ast.BasicLit{Kind: token.INT, Value: "1"}}} _, err := appendParseArgumentStatements(nil, scalarTestFile(t), []muxt.Argument{{}}, "", testConfig(), call, nil, nil) - if want := "unsupported argument 1 in call to F"; err == nil || err.Error() != want { - t.Errorf("appendParseArgumentStatements error = %v, want %q", err, want) - } + assert.EqualError(t, err, "unsupported argument 1 in call to F", "appendParseArgumentStatements error") } func TestAppendParseArgumentStatementsNumbersNestedCallResults(t *testing.T) { @@ -51,22 +49,16 @@ func TestAppendParseArgumentStatementsNumbersNestedCallResults(t *testing.T) { "a.gohtml": `{{define "GET /two Two(B(), B())"}}{{end}}`, }) handler, err := callHandlerFunc(newFile(pkg), testConfig(), defs[0], "RoutesReceiver") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) got := astgen.Format(handler.Body) for _, want := range []string{"result0 := receiver.B()", "result1 := receiver.B()", "receiver.Two(result0, result1)"} { - if !strings.Contains(got, want) { - t.Errorf("handler does not contain %q:\n%s", want, got) - } + assert.Contains(t, got, want, "handler body") } } func TestMismatchedArgumentError(t *testing.T) { err := mismatchedArgumentError(scalarTestFile(t), muxt.Argument{Identifier: "fooMessage"}) - if err == nil || err.Error() != "failed to determine type for fooMessage" { - t.Errorf("mismatchedArgumentError = %v, want failed to determine type for fooMessage", err) - } + assert.EqualError(t, err, "failed to determine type for fooMessage", "mismatchedArgumentError") } func TestRequestArgumentSource(t *testing.T) { @@ -84,17 +76,11 @@ func TestRequestArgumentSource(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := requestArgumentSource(tt.argument) if tt.wantErr != "" { - if err == nil || err.Error() != tt.wantErr { - t.Errorf("requestArgumentSource(%s) error = %v, want %q", tt.name, err, tt.wantErr) - } + assert.EqualError(t, err, tt.wantErr, "requestArgumentSource(%s) error", tt.name) return } - if err != nil { - t.Fatal(err) - } - if s := astgen.Format(got); s != tt.want { - t.Errorf("requestArgumentSource(%s) = %q, want %q", tt.name, s, tt.want) - } + require.NoError(t, err) + assert.Equal(t, tt.want, astgen.Format(got), "requestArgumentSource(%s)", tt.name) }) } } diff --git a/internal/generate/parse_form_test.go b/internal/generate/parse_form_test.go index ba8d0057..04a5c108 100644 --- a/internal/generate/parse_form_test.go +++ b/internal/generate/parse_form_test.go @@ -3,9 +3,11 @@ package generate import ( "go/ast" "go/types" - "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/astgen" "github.com/typelate/muxt/internal/source" ) @@ -25,9 +27,7 @@ func TestCallParseMultipartFormMaxMemory(t *testing.T) { config := testConfig() config.MultipartMaxMemory = tt.maxMemory got := astgen.Format(callParseMultipartForm(scalarTestFile(t), config, &ast.BlockStmt{})) - if !strings.Contains(got, tt.want) { - t.Errorf("callParseMultipartForm(%d) = %s, want it to call %s", tt.maxMemory, got, tt.want) - } + assert.Contains(t, got, tt.want, "callParseMultipartForm(%d)", tt.maxMemory) }) } } @@ -45,12 +45,8 @@ func TestTypedVar(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { decl, err := typedVar(scalarTestFile(t), "form", tp, tt.value) - if err != nil { - t.Fatal(err) - } - if got := astgen.Format(decl); got != tt.want { - t.Errorf("typedVar(form, %s) = %q, want %q", tt.name, got, tt.want) - } + require.NoError(t, err) + assert.Equal(t, tt.want, astgen.Format(decl), "typedVar(form, %s)", tt.name) }) } } diff --git a/internal/generate/parse_scalar_test.go b/internal/generate/parse_scalar_test.go index 09f3a460..0600b2a8 100644 --- a/internal/generate/parse_scalar_test.go +++ b/internal/generate/parse_scalar_test.go @@ -7,6 +7,9 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/astgen" "github.com/typelate/muxt/internal/fake" "github.com/typelate/muxt/internal/muxt" @@ -198,15 +201,11 @@ out = tmp`, tmp: "tmp", str: ast.NewIdent("str"), typ: tt.typ(t), method: tt.method, validations: tt.validations, assign: assign, errBlock: errBlock(), }.statements(file) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) got := strings.TrimSpace(astgen.Format(&ast.BlockStmt{List: stmts})) got = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(got, "{"), "}")) got = strings.ReplaceAll(got, "\n\t", "\n") - if got != tt.want { - t.Errorf("scalarParse(%s).statements =\n%s\nwant\n%s", tt.name, got, tt.want) - } + assert.Equal(t, tt.want, got, "scalarParse(%s).statements", tt.name) }) } @@ -215,8 +214,6 @@ out = tmp`, tmp: "tmp", str: ast.NewIdent("str"), typ: basic(types.Complex128), method: muxt.UnmarshalUnsupported, assign: assign, errBlock: errBlock(), }.statements(scalarTestFile(t)) - if err == nil || !strings.Contains(err.Error(), "unsupported type: complex128") { - t.Errorf("error = %v, want unsupported type: complex128", err) - } + assert.ErrorContains(t, err, "unsupported type: complex128") }) } From 6afde43d0693eeda559605997a6117cd4637ed9b Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:31:29 -0700 Subject: [PATCH 07/23] use testify in the generate route and template data tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/generate/routes_test.go | 32 ++++++------------- internal/generate/template_data_test.go | 10 +++--- internal/generate/template_route_path_test.go | 19 ++++------- 3 files changed, 21 insertions(+), 40 deletions(-) diff --git a/internal/generate/routes_test.go b/internal/generate/routes_test.go index 9104bfc7..f3a6596b 100644 --- a/internal/generate/routes_test.go +++ b/internal/generate/routes_test.go @@ -3,9 +3,11 @@ package generate import ( "io" "log" - "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/astgen" "github.com/typelate/muxt/internal/fake" "github.com/typelate/muxt/internal/muxt" @@ -28,30 +30,20 @@ func (T) Title(id int) string { return "" } `, "T", `{{define "GET /article/{id} Article(id, Title(id))"}}{{end}}`) defs, err := muxt.ResolveDefinitions(pkg, receiver, fake.NewChecker().Fake()) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) groups, err := groupTemplates(config, defs) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) file := newFile(pkg) def := groups.all[0] var handlers []string for range 2 { handler, err := callHandlerFunc(file, config, def, config.ReceiverInterface) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) handlers = append(handlers, astgen.Format(handler)) } - if handlers[0] != handlers[1] { - t.Errorf("the second handler differs from the first:\n%s\nsecond:\n%s", handlers[0], handlers[1]) - } - if got := astgen.Format(def.CallExpression()); got != "Article(id, Title(id))" { - t.Errorf("the route's call is %s after generation, want it as written", got) - } + assert.Equal(t, handlers[0], handlers[1], "the second handler differs from the first") + assert.Equal(t, "Article(id, Title(id))", astgen.Format(def.CallExpression()), "the route's call after generation, want it as written") } // TestHandlerGenerationRejectsAnArgumentWithNoRequestValue generates an sse @@ -68,11 +60,7 @@ func (T) Stream(string) {} `, "T", `{{define "GET /x sse(Stream(fooMessage))"}}{{end}}{{define "fooMessage"}}{{end}}`) defs, err := muxt.ResolveDefinitions(pkg, receiver, fake.NewChecker().Fake()) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) _, err = TemplateRoutesFiles(".", config, pkg, defs, log.New(io.Discard, "", 0)) - if err == nil || !strings.Contains(err.Error(), "failed to determine type for fooMessage") { - t.Errorf("got error %v, want it to say it failed to determine type for fooMessage", err) - } + assert.ErrorContains(t, err, "failed to determine type for fooMessage") } diff --git a/internal/generate/template_data_test.go b/internal/generate/template_data_test.go index 1e9012a5..49c5151e 100644 --- a/internal/generate/template_data_test.go +++ b/internal/generate/template_data_test.go @@ -5,6 +5,8 @@ import ( "go/token" "testing" + "github.com/stretchr/testify/assert" + "github.com/typelate/muxt/internal/astgen" ) @@ -86,9 +88,7 @@ func TestTemplateDataMethods(t *testing.T) { }, } { t.Run(tt.name, func(t *testing.T) { - if got := astgen.Format(tt.decl); got != tt.want { - t.Errorf("%s =\n%s\nwant\n%s", tt.name, got, tt.want) - } + assert.Equal(t, tt.want, astgen.Format(tt.decl), tt.name) }) } } @@ -96,7 +96,5 @@ func TestTemplateDataMethods(t *testing.T) { func TestHTMXRequestHeaderBoolMethod(t *testing.T) { decl := htmxRequestHeaderBoolMethod("TemplateData", "Flag", "X-Flag", token.NEQ, "yes") want := "func (data *TemplateData[R, T]) Flag() bool {\n\treturn data.Request().Header.Get(\"X-Flag\") != \"yes\"\n}" - if got := astgen.Format(decl); got != want { - t.Errorf("htmxRequestHeaderBoolMethod = %s, want %s", got, want) - } + assert.Equal(t, want, astgen.Format(decl), "htmxRequestHeaderBoolMethod") } diff --git a/internal/generate/template_route_path_test.go b/internal/generate/template_route_path_test.go index 90d02d23..799ab34e 100644 --- a/internal/generate/template_route_path_test.go +++ b/internal/generate/template_route_path_test.go @@ -5,6 +5,9 @@ import ( "html/template" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/astgen" "github.com/typelate/muxt/internal/fake" "github.com/typelate/muxt/internal/muxt" @@ -153,21 +156,13 @@ func TestRoutePathFunc(t *testing.T) { id := fake.Lookup(t, pkg, "ID") checker := fake.StandInChecker(t, pkg).ParsesFromText(id).FormatsAsText(id).Fake() defs, err := muxt.ResolveDefinitions(src, fake.Lookup(t, pkg, "T").(*types.Named), checker) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) config := testConfig() config.PathPrefix = tt.pathPrefix decl, escapers, err := routePathFunc(newFile(src), config, &defs[0]) - if err != nil { - t.Fatal(err) - } - if got := astgen.Format(decl); got != tt.want { - t.Errorf("routePathFunc(%q) =\n%s\nwant\n%s", tt.pattern, got, tt.want) - } - if escapers != tt.escapers { - t.Errorf("routePathFunc(%q) escapers = %+v, want %+v", tt.pattern, escapers, tt.escapers) - } + require.NoError(t, err) + assert.Equal(t, tt.want, astgen.Format(decl), "routePathFunc(%q)", tt.pattern) + assert.Equal(t, tt.escapers, escapers, "routePathFunc(%q) escapers", tt.pattern) }) } } From 71ad59399d82418708057abc03fcc74181b687e6 Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:32:40 -0700 Subject: [PATCH 08/23] use testify in the generate route function and snapshot tests The snapshot mismatch message had a %s with no argument; it now names the want/ file. Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/generate/routes_functions_test.go | 81 +++++++--------------- internal/generate/snapshot_test.go | 42 ++++------- 2 files changed, 39 insertions(+), 84 deletions(-) diff --git a/internal/generate/routes_functions_test.go b/internal/generate/routes_functions_test.go index 09991f74..a2f8de08 100644 --- a/internal/generate/routes_functions_test.go +++ b/internal/generate/routes_functions_test.go @@ -11,6 +11,9 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/astgen" "github.com/typelate/muxt/internal/fake" "github.com/typelate/muxt/internal/muxt" @@ -53,9 +56,7 @@ func routesTestDefinitions(t *testing.T, files map[string]string) (source.Packag } receiver := fake.Lookup(t, pkg, "T").(*types.Named) defs, err := muxt.ResolveDefinitions(src, receiver, fake.StandInChecker(t, pkg).Fake()) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) return src, defs } @@ -66,21 +67,14 @@ func TestGroupTemplates(t *testing.T) { "c d.gohtml": `{{define "GET /c B()"}}{{end}}`, }) groups, err := groupTemplates(testConfig(), defs) - if err != nil { - t.Fatal(err) - } - if got, want := slices.Sorted(maps.Keys(groups.byFile)), []string{"a.gohtml", "b.gohtml"}; !slices.Equal(got, want) { - t.Errorf("groups.byFile keys = %q, want %q", got, want) - } - if got := len(groups.byFile["b.gohtml"]); got != 2 { - t.Errorf("len(groups.byFile[b.gohtml]) = %d, want 2", got) - } - if len(groups.noFile) != 1 || groups.noFile[0].RawPattern() != "GET /c" { - t.Errorf("groups.noFile = %v, want the route of the file with a space in its name", groups.noFile) - } - if len(groups.all) != len(defs) || len(groups.all) != 4 { - t.Errorf("len(groups.all) = %d, want 4", len(groups.all)) + require.NoError(t, err) + assert.Equal(t, []string{"a.gohtml", "b.gohtml"}, slices.Sorted(maps.Keys(groups.byFile)), "groups.byFile keys") + assert.Len(t, groups.byFile["b.gohtml"], 2, "groups.byFile[b.gohtml]") + if assert.Len(t, groups.noFile, 1, "groups.noFile, want the route of the file with a space in its name") { + assert.Equal(t, "GET /c", groups.noFile[0].RawPattern(), "groups.noFile[0]") } + assert.Len(t, groups.all, len(defs), "groups.all, want every definition") + assert.Len(t, groups.all, 4, "groups.all") } func TestLogResolutionNotes(t *testing.T) { @@ -115,14 +109,10 @@ func TestLogResolutionNotes(t *testing.T) { var buf bytes.Buffer logResolutionNotes(defs, tt.config(testConfig()), log.New(&buf, "", 0)) for _, want := range tt.contains { - if !strings.Contains(buf.String(), want) { - t.Errorf("log = %q, want it to contain %q", buf.String(), want) - } + assert.Contains(t, buf.String(), want, "log") } for _, unwanted := range tt.excludes { - if strings.Contains(buf.String(), unwanted) { - t.Errorf("log = %q, want it not to contain %q", buf.String(), unwanted) - } + assert.NotContains(t, buf.String(), unwanted, "log") } }) } @@ -139,20 +129,14 @@ func TestCollectReceiverMethods(t *testing.T) { "a.gohtml": `{{define "GET /a/{id} A(id)"}}{{end}}{{define "GET /b B()"}}{{end}}{{define "GET /n Nested(B())"}}{{end}}{{define "GET /plain"}}{{end}}`, }) receiverInterface := &ast.InterfaceType{Methods: new(ast.FieldList)} - if err := collectReceiverMethods(defs, newFile(pkg), receiverInterface); err != nil { - t.Fatal(err) - } + require.NoError(t, collectReceiverMethods(defs, newFile(pkg), receiverInterface)) var names []string for _, field := range receiverInterface.Methods.List { names = append(names, field.Names[0].Name) } slices.Sort(names) - if want := []string{"A", "B", "Nested"}; !slices.Equal(names, want) { - t.Errorf("collectReceiverMethods interface methods = %q, want %q (each method once, nested calls included)", names, want) - } - if got := astgen.Format(receiverInterface); !strings.Contains(got, "Nested(string) string") { - t.Errorf("receiver interface = %s, want it to declare Nested(string) string", got) - } + assert.Equal(t, []string{"A", "B", "Nested"}, names, "collectReceiverMethods interface methods, each method once, nested calls included") + assert.Contains(t, astgen.Format(receiverInterface), "Nested(string) string", "receiver interface") } func TestGeneratePerFileRouteFunction(t *testing.T) { @@ -162,9 +146,7 @@ func TestGeneratePerFileRouteFunction(t *testing.T) { t.Run("source file is required", func(t *testing.T) { _, err := generatePerFileRouteFunction("", defs, newFile(pkg), "aRoutes", "aReceiver", log.New(&bytes.Buffer{}, "", 0), testConfig(), &ast.InterfaceType{Methods: new(ast.FieldList)}) - if err == nil || err.Error() != "sourceFile cannot be empty" { - t.Errorf("error = %v, want sourceFile cannot be empty", err) - } + assert.EqualError(t, err, "sourceFile cannot be empty") }) for _, tt := range []struct { @@ -193,34 +175,23 @@ func TestGeneratePerFileRouteFunction(t *testing.T) { var buf bytes.Buffer iface := &ast.InterfaceType{Methods: new(ast.FieldList)} decl, err := generatePerFileRouteFunction("a.gohtml", defs, newFile(pkg), "aRoutes", "aReceiver", log.New(&buf, "", 0), config, iface) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) got := astgen.Format(decl) - if first, _, _ := strings.Cut(got, "\n"); first != tt.signature { - t.Errorf("signature = %q, want %q", first, tt.signature) - } - if !strings.Contains(got, "bytesBufferPool") { - t.Errorf("function has no buffer pool declaration:\n%s", got) - } + first, _, _ := strings.Cut(got, "\n") + assert.Equal(t, tt.signature, first, "signature") + assert.Contains(t, got, "bytesBufferPool", "function should declare a buffer pool") for _, want := range []string{"generating handler for pattern GET /a/{id} in a.gohtml", "generating handler for pattern GET /plain in a.gohtml"} { - if !strings.Contains(buf.String(), want) { - t.Errorf("log = %q, want it to contain %q", buf.String(), want) - } + assert.Contains(t, buf.String(), want, "log") } - if len(iface.Methods.List) != 1 || iface.Methods.List[0].Names[0].Name != "A" { - t.Errorf("receiver interface = %s, want it to declare A", astgen.Format(iface)) + if assert.Len(t, iface.Methods.List, 1, "receiver interface = %s, want it to declare A", astgen.Format(iface)) { + assert.Equal(t, "A", iface.Methods.List[0].Names[0].Name, "receiver interface method") } }) } t.Run("no routes has no buffer pool", func(t *testing.T) { decl, err := generatePerFileRouteFunction("a.gohtml", nil, newFile(pkg), "aRoutes", "aReceiver", nil, testConfig(), &ast.InterfaceType{Methods: new(ast.FieldList)}) - if err != nil { - t.Fatal(err) - } - if got := astgen.Format(decl); strings.Contains(got, "bytesBufferPool") { - t.Errorf("function with no routes declares a buffer pool:\n%s", got) - } + require.NoError(t, err) + assert.NotContains(t, astgen.Format(decl), "bytesBufferPool", "function with no routes should not declare a buffer pool") }) } diff --git a/internal/generate/snapshot_test.go b/internal/generate/snapshot_test.go index 2e9a9320..c4a4b712 100644 --- a/internal/generate/snapshot_test.go +++ b/internal/generate/snapshot_test.go @@ -18,6 +18,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/tools/txtar" "github.com/typelate/muxt/internal/configjson" @@ -75,16 +76,11 @@ var templates = template.Must(template.ParseFS(templateFiles, "*.gohtml")) // integration suite's job, under cmd/muxt/testdata. func TestSnapshots(t *testing.T) { const command = "generate" - if stray, _ := filepath.Glob(filepath.Join("testdata", "*.txtar")); len(stray) > 0 { - t.Fatalf("%s is not in a command's directory; generate's archives are in testdata/%s", stray[0], command) - } + stray, _ := filepath.Glob(filepath.Join("testdata", "*.txtar")) + require.Empty(t, stray, "these are not in a command's directory; generate's archives are in testdata/%s", command) archives, err := filepath.Glob(filepath.Join("testdata", command, "*.txtar")) - if err != nil { - t.Fatal(err) - } - if len(archives) == 0 { - t.Fatalf("no archives in testdata/%s", command) - } + require.NoError(t, err) + require.NotEmpty(t, archives, "no archives in testdata/%s", command) t.Run(command, func(t *testing.T) { for _, archivePath := range archives { runSnapshot(t, archivePath) @@ -98,9 +94,7 @@ func runSnapshot(t *testing.T, archivePath string) { t.Helper() t.Run(strings.TrimSuffix(filepath.Base(archivePath), ".txtar"), func(t *testing.T) { archive, err := txtar.ParseFile(archivePath) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) got := snapshot(t, configuration(t, archive), archive) if *update { writeSnapshot(t, archivePath, archive, got) @@ -113,7 +107,7 @@ func runSnapshot(t *testing.T, archivePath string) { } } for _, name := range sortedKeys(got, want) { - assert.Equal(t, want[name], got[name], "want/%s differs (run go test -run TestSnapshots -update to rewrite)") + assert.Equal(t, want[name], got[name], "want/%s differs (run go test -run TestSnapshots -update to rewrite)", name) } }) } @@ -127,12 +121,10 @@ func configuration(t *testing.T, archive *txtar.Archive) generate.RoutesFileConf continue } var config generate.RoutesFileConfiguration - if err := json.Unmarshal(file.Data, &config, configjson.Options()); err != nil { - t.Fatalf("config.json: %v", err) - } + require.NoError(t, json.Unmarshal(file.Data, &config, configjson.Options()), "config.json") return config } - t.Fatal("the archive has no config.json") + require.FailNow(t, "the archive has no config.json") return generate.RoutesFileConfiguration{} } @@ -176,9 +168,7 @@ func snapshot(t *testing.T, config generate.RoutesFileConfiguration, archive *tx generated, err := generate.TemplateRoutesFiles(dir, config, pkg, defs, log.New(&logs, "", 0)) for _, file := range generated { got[relative(file.Path)] = file.Content - for _, name := range unusedImports(t, file.Content) { - t.Errorf("%s imports %s without using it", relative(file.Path), name) - } + assert.Empty(t, unusedImports(t, file.Content), "%s imports these without using them", relative(file.Path)) } if logs.Len() > 0 { got["log.txt"] = relative(logs.String()) @@ -198,9 +188,7 @@ func writeSnapshot(t *testing.T, archivePath string, archive *txtar.Archive, got files = append(files, txtar.File{Name: "want/" + name, Data: []byte(got[name])}) } archive.Files = files - if err := os.WriteFile(archivePath, txtar.Format(archive), 0o644); err != nil { - t.Fatal(err) - } + require.NoError(t, os.WriteFile(archivePath, txtar.Format(archive), 0o644)) } func sortedKeys(ms ...map[string]string) []string { @@ -218,9 +206,7 @@ func sortedKeys(ms ...map[string]string) []string { func unusedImports(t *testing.T, content string) []string { t.Helper() file, err := parser.ParseFile(token.NewFileSet(), "generated.go", content, 0) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) referenced := make(map[string]bool) ast.Inspect(file, func(node ast.Node) bool { if sel, ok := node.(*ast.SelectorExpr); ok { @@ -235,9 +221,7 @@ func unusedImports(t *testing.T, content string) []string { var unused []string for _, spec := range file.Imports { importPath, err := strconv.Unquote(spec.Path.Value) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) name := path.Base(importPath) if spec.Name != nil { name = spec.Name.Name From 309087cc53faf6e5f3334b20095c03a4a764b395 Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:27:37 -0700 Subject: [PATCH 09/23] use testify in the boolean, delimiters, error and estimate tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/mutation/boolean_test.go | 103 +++++++++++++-------------- internal/mutation/delimiters_test.go | 38 ++++------ internal/mutation/errors_test.go | 17 +++-- internal/mutation/estimate_test.go | 14 ++-- 4 files changed, 80 insertions(+), 92 deletions(-) diff --git a/internal/mutation/boolean_test.go b/internal/mutation/boolean_test.go index 50cfc60c..81951836 100644 --- a/internal/mutation/boolean_test.go +++ b/internal/mutation/boolean_test.go @@ -5,9 +5,24 @@ import ( "testing" "text/template/parse" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/asteval" ) +// ifPipe parses `{{if pipeline}}x{{end}}` and returns the template text and +// the if's pipeline. +func ifPipe(t *testing.T, pipeline string) (string, *parse.PipeNode) { + t.Helper() + text := `{{if ` + pipeline + `}}x{{end}}` + trees, err := asteval.ParseTrees("t", text, "", "", nil) + require.NoError(t, err) + node, ok := trees["t"].Root.Nodes[0].(*parse.IfNode) + require.Truef(t, ok, "first node is %T, want an if", trees["t"].Root.Nodes[0]) + return text, node.Pipe +} + // TestSimplifyReducesConditions states the laws the decision simplifier // applies. // @@ -127,28 +142,13 @@ func TestSimplifyReducesConditions(t *testing.T) { }, } { t.Run(tt.name, func(t *testing.T) { - text := `{{if ` + tt.pipeline + `}}x{{end}}` - trees, err := asteval.ParseTrees("t", text, "", "", nil) - if err != nil { - t.Fatal(err) - } - node, ok := trees["t"].Root.Nodes[0].(*parse.IfNode) - if !ok { - t.Fatalf("first node is %T, want an if", trees["t"].Root.Nodes[0]) - } - built, ok := decision(text, node.Pipe) - if !ok { - t.Fatalf("decision(%q) could not be modelled", tt.pipeline) - } + text, pipe := ifPipe(t, tt.pipeline) + built, ok := decision(text, pipe) + require.Truef(t, ok, "decision(%q) could not be modelled", tt.pipeline) simplified := built.simplify() - if got := simplified.canonical(); got != tt.canonical { - t.Errorf("simplify(%q).canonical() = %q, want %q", tt.pipeline, got, tt.canonical) - } - got := simplified.conditions() - if strings.Join(got, ",") != strings.Join(tt.conditions, ",") { - t.Errorf("simplify(%q).conditions() = %v, want %v", tt.pipeline, got, tt.conditions) - } + assert.Equal(t, tt.canonical, simplified.canonical(), "simplify(%q).canonical()", tt.pipeline) + assert.Equal(t, strings.Join(tt.conditions, ","), strings.Join(simplified.conditions(), ","), "simplify(%q).conditions()", tt.pipeline) }) } } @@ -172,15 +172,9 @@ func TestDecisionRefusesWhatItCannotModel(t *testing.T) { `and .A (eq .B 1)`, } { t.Run(pipeline, func(t *testing.T) { - text := `{{if ` + pipeline + `}}x{{end}}` - trees, err := asteval.ParseTrees("t", text, "", "", nil) - if err != nil { - t.Fatal(err) - } - node := trees["t"].Root.Nodes[0].(*parse.IfNode) - if _, ok := decision(text, node.Pipe); ok { - t.Errorf("decision(%q) was modelled, want it declined so the general operand combinations apply instead", pipeline) - } + text, pipe := ifPipe(t, pipeline) + _, ok := decision(text, pipe) + assert.False(t, ok, "decision(%q) was modelled, want it declined so the general operand combinations apply instead", pipeline) }) } } @@ -190,9 +184,10 @@ func TestDecisionCommandRefusesACommandWithoutArguments(t *testing.T) { "nil": nil, "empty": {}, } { - if node, ok := decisionCommand("", command); ok { - t.Errorf("decisionCommand(%s command) = %v, want it declined", name, node) - } + t.Run(name, func(t *testing.T) { + node, ok := decisionCommand("", command) + assert.False(t, ok, "decisionCommand(%s command) = %v, want it declined", name, node) + }) } } @@ -210,8 +205,9 @@ func TestLogicalKind(t *testing.T) { } { t.Run(tt.function, func(t *testing.T) { kind, ok := logicalKind(tt.function) - if ok != tt.ok || (ok && kind != tt.kind) { - t.Errorf("logicalKind(%q) = %v, %t, want %v, %t", tt.function, kind, ok, tt.kind, tt.ok) + assert.Equal(t, tt.ok, ok, "logicalKind(%q) ok", tt.function) + if ok { + assert.Equal(t, tt.kind, kind, "logicalKind(%q)", tt.function) } }) } @@ -236,11 +232,9 @@ func TestDecisionCallArity(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { node, ok := decisionCall(tt.kind, "", tt.args) - if ok != tt.ok { - t.Fatalf("decisionCall(%v, %d args) ok = %t, want %t", tt.kind, len(tt.args), ok, tt.ok) - } - if ok && len(node.kids) != len(tt.args) { - t.Errorf("decisionCall(%v, %d args) has %d operands", tt.kind, len(tt.args), len(node.kids)) + require.Equal(t, tt.ok, ok, "decisionCall(%v, %d args) ok", tt.kind, len(tt.args)) + if ok { + assert.Len(t, node.kids, len(tt.args), "decisionCall(%v, %d args) operands", tt.kind, len(tt.args)) } }) } @@ -270,17 +264,15 @@ func TestSimplifyHelpers(t *testing.T) { {name: "double", kid: not(a), want: ".A"}, {name: "condition", kid: a, want: "not(.A)"}, } { - if got := negate(tt.kid).canonical(); got != tt.want { - t.Errorf("negate(%s) = %q, want %q", tt.name, got, tt.want) - } + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, negate(tt.kid).canonical(), "negate(%s)", tt.name) + }) } }) t.Run("flatten", func(t *testing.T) { got := canonicals(flatten(boolAnd, []*boolNode{a, junction(boolAnd, b, a), junction(boolOr, a, b), not(not(b))})) - if want := ".A .B .A or(.A,.B) .B"; got != want { - t.Errorf("flatten(and, ...) = %q, want %q", got, want) - } + assert.Equal(t, ".A .B .A or(.A,.B) .B", got, "flatten(and, ...)") }) t.Run("foldConstants", func(t *testing.T) { @@ -296,18 +288,19 @@ func TestSimplifyHelpers(t *testing.T) { {name: "or drops false", kids: []*boolNode{constant(false), a}, zero: true, want: ".A"}, {name: "or decided by true", kids: []*boolNode{a, constant(true)}, zero: true, wantDecided: true}, } { - kept, decided := foldConstants(tt.kids, tt.zero) - if decided != tt.wantDecided || (!decided && canonicals(kept) != tt.want) { - t.Errorf("foldConstants(%s) = %q, %t, want %q, %t", tt.name, canonicals(kept), decided, tt.want, tt.wantDecided) - } + t.Run(tt.name, func(t *testing.T) { + kept, decided := foldConstants(tt.kids, tt.zero) + assert.Equal(t, tt.wantDecided, decided, "foldConstants(%s) decided", tt.name) + if !decided { + assert.Equal(t, tt.want, canonicals(kept), "foldConstants(%s) kept", tt.name) + } + }) } }) t.Run("distinct", func(t *testing.T) { got := canonicals(distinct([]*boolNode{a, b, a, junction(boolAnd, b, a), junction(boolAnd, a, b)})) - if want := ".A .B and(.A,.B)"; got != want { - t.Errorf("distinct = %q, want %q", got, want) - } + assert.Equal(t, ".A .B and(.A,.B)", got, "distinct") }) t.Run("hasComplement", func(t *testing.T) { @@ -321,9 +314,9 @@ func TestSimplifyHelpers(t *testing.T) { {name: "not last", kids: []*boolNode{a, b, not(a)}, want: true}, {name: "different negation", kids: []*boolNode{a, not(b)}, want: false}, } { - if got := hasComplement(tt.kids); got != tt.want { - t.Errorf("hasComplement(%s) = %t, want %t", tt.name, got, tt.want) - } + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, hasComplement(tt.kids), "hasComplement(%s)", tt.name) + }) } }) } diff --git a/internal/mutation/delimiters_test.go b/internal/mutation/delimiters_test.go index c42a7d2a..f38a8e51 100644 --- a/internal/mutation/delimiters_test.go +++ b/internal/mutation/delimiters_test.go @@ -5,6 +5,9 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/source" ) @@ -62,12 +65,9 @@ func TestDelimitersReadsThemOffTheEndClause(t *testing.T) { } left, right, ok := delimiters(text, definition) - if ok != tt.ok { - t.Fatalf("delimiters(%q) ok = %t, want %t", tt.end, ok, tt.ok) - } - if left != tt.left || right != tt.right { - t.Errorf("delimiters(%q) = %q, %q, want %q, %q", tt.end, left, right, tt.left, tt.right) - } + require.Equal(t, tt.ok, ok, "delimiters(%q) ok", tt.end) + assert.Equal(t, tt.left, left, "delimiters(%q) left", tt.end) + assert.Equal(t, tt.right, right, "delimiters(%q) right", tt.end) }) } } @@ -86,9 +86,8 @@ func TestDelimitersDeclinesWhatItCannotRead(t *testing.T) { Name: "t.gohtml", End: source.Span{Position: token.Position{Filename: "t.gohtml", Offset: len(text)}}, } - if _, _, ok := delimiters(text, definition); ok { - t.Error("delimiters accepted a definition with no end clause to read") - } + _, _, ok := delimiters(text, definition) + assert.False(t, ok, "delimiters accepted a definition with no end clause to read") }) t.Run("a span outside the text", func(t *testing.T) { @@ -100,9 +99,8 @@ func TestDelimitersDeclinesWhatItCannotRead(t *testing.T) { Length: 99, }, } - if _, _, ok := delimiters(text, definition); ok { - t.Error("delimiters read past the end of the text") - } + _, _, ok := delimiters(text, definition) + assert.False(t, ok, "delimiters read past the end of the text") }) } @@ -124,21 +122,13 @@ func TestDelimitersAgreeWithTheScanner(t *testing.T) { } left, right, ok := delimiters(text, definition) - if !ok { - t.Fatal("delimiters could not read the pair off the end clause") - } + require.True(t, ok, "delimiters could not read the pair off the end clause") found := regions(text, left, right) - if len(found) != 3 { - t.Fatalf("regions = %d, want the define, the action and the end", len(found)) - } - if got := text[found[1].start:found[1].end]; got != "[[.Name]]" { - t.Errorf("second region = %q, want %q", got, "[[.Name]]") - } + require.Len(t, found, 3, "regions: the define, the action and the end") + assert.Equal(t, "[[.Name]]", text[found[1].start:found[1].end], "second region") // The defaults find nothing here, which is the failure this whole // derivation exists to avoid. - if none := regions(text, "", ""); len(none) != 0 { - t.Errorf("scanning with the default delimiters found %d regions, want none", len(none)) - } + assert.Empty(t, regions(text, "", ""), "scanning with the default delimiters") } diff --git a/internal/mutation/errors_test.go b/internal/mutation/errors_test.go index 75792c56..d9470a5b 100644 --- a/internal/mutation/errors_test.go +++ b/internal/mutation/errors_test.go @@ -1,33 +1,42 @@ package mutation -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) // TestErrorMessages states what each error says, which is all a reader of // a failed run has to go on. func TestErrorMessages(t *testing.T) { for _, tt := range []struct { + name string err error want string }{ { + name: "baseline failed", err: &BaselineFailedError{Output: "--- FAIL: TestIndex (0.00s)\nFAIL\n"}, want: "baseline tests failed before mutation; fix them first:\n--- FAIL: TestIndex (0.00s)\nFAIL", }, { + name: "no call sites", err: &NoCallSitesError{Variables: []string{"templates", "pages"}}, want: "no templates, pages.ExecuteTemplate calls found: mutation testing needs a call site to know the type of dot", }, { + name: "no mutations", err: &NoMutationsError{Templates: 3}, want: "no mutations available: the 3 template(s) reached hold no dynamic or control flow actions, so a run would report every mutant killed without testing anything", }, { + name: "unreadable template", err: &UnreadableTemplateError{Template: "page", Path: "page.gohtml"}, want: `template "page" in page.gohtml holds actions the template set can see and this run cannot: it was read with the wrong delimiters`, }, } { - if got := tt.err.Error(); got != tt.want { - t.Errorf("Error() = %q, want %q", got, tt.want) - } + t.Run(tt.name, func(t *testing.T) { + assert.EqualError(t, tt.err, tt.want) + }) } } diff --git a/internal/mutation/estimate_test.go b/internal/mutation/estimate_test.go index 1e765494..c7203e97 100644 --- a/internal/mutation/estimate_test.go +++ b/internal/mutation/estimate_test.go @@ -3,6 +3,8 @@ package mutation import ( "testing" "time" + + "github.com/stretchr/testify/assert" ) func TestEstimateTotal(t *testing.T) { @@ -21,9 +23,7 @@ func TestEstimateTotal(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { e := estimate{perMutant: time.Second, remaining: tt.remaining, workers: tt.workers} - if got := e.total(); got != tt.want { - t.Errorf("total() = %v, want %v", got, tt.want) - } + assert.Equal(t, tt.want, e.total(), "total()") }) } } @@ -32,10 +32,6 @@ func TestEstimateObserve(t *testing.T) { e := estimate{perMutant: 10 * time.Second, remaining: 1, workers: 1} e.observe(2 * time.Second) e.observe(4 * time.Second) - if e.perMutant != 3*time.Second { - t.Errorf("perMutant = %v, want the average of what was observed, 3s", e.perMutant) - } - if e.remaining != 0 { - t.Errorf("remaining = %d, want it to stop at 0", e.remaining) - } + assert.Equal(t, 3*time.Second, e.perMutant, "perMutant is the average of what was observed") + assert.Equal(t, 0, e.remaining, "remaining stops at 0") } From 50e9556cce250d1d84b24826003ca63f17f829d9 Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:29:52 -0700 Subject: [PATCH 10/23] use testify in the plan, enumerate, mutant and fixture tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/mutation/enumerate_test.go | 63 ++++++---------- internal/mutation/fixtures_test.go | 41 ++++------- internal/mutation/mutant_test.go | 43 +++++------ internal/mutation/plan_test.go | 107 +++++++++++----------------- 4 files changed, 93 insertions(+), 161 deletions(-) diff --git a/internal/mutation/enumerate_test.go b/internal/mutation/enumerate_test.go index f65c3ff7..1c2f1e71 100644 --- a/internal/mutation/enumerate_test.go +++ b/internal/mutation/enumerate_test.go @@ -1,14 +1,15 @@ package mutation import ( - "fmt" "go/types" "regexp" - "slices" "strconv" "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/asteval" ) @@ -43,9 +44,7 @@ func enumerate(t *testing.T, body string, dot types.Type, maxCases int) ([]Mutan t.Helper() text := `{{define "t"}}` + body + `{{end}}` trees, err := asteval.ParseTrees("t.gohtml", text, "", "", nil) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) src := newFileSource("t.gohtml", "t.gohtml", text, "", "") sc := scope{template: "t", dataType: dot, treeLocation: treeLocation{src: src, tree: trees["t"]}} return mutantsInScope(sc, nil, newValues(1), maxCases) @@ -111,12 +110,8 @@ func TestMutantsInScope(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { mutants, notes := enumerate(t, tt.body, page, DefaultMaxCases) - if got := variants(mutants); !slices.Equal(got, tt.want) { - t.Errorf("mutants:\n got %q\nwant %q", got, tt.want) - } - if len(notes) != 0 { - t.Errorf("notes = %v, want none", notes) - } + assert.Equal(t, tt.want, variants(mutants), "mutants") + assert.Empty(t, notes, "notes") }) } } @@ -126,51 +121,39 @@ func TestMutantsInScope(t *testing.T) { // says so where the action is. func TestMutantsInScopeHoldsBackAnActionOverBudget(t *testing.T) { mutants, notes := enumerate(t, `{{printf "%s %d" .Name .Count}}`, dataType(t, pageSource, "Page"), 2) - if got, want := variants(mutants), []variant{{OperatorActionZero, `""`}}; !slices.Equal(got, want) { - t.Errorf("mutants %q, want %q", got, want) - } - if len(notes) != 1 { - t.Fatalf("notes = %v, want one", notes) - } + assert.Equal(t, []variant{{OperatorActionZero, `""`}}, variants(mutants), "mutants") + require.Len(t, notes, 1) note := notes[0] - if note.line != 1 || note.column != 15 || note.template != "t" { - t.Errorf("note at %s %d:%d, want t 1:15", note.template, note.line, note.column) - } - if got, want := note.reason(), "2 operands need 3 cases, over --max-cases=2"; got != want { - t.Errorf("reason = %q, want %q", got, want) - } + assert.Equal(t, "t", note.template, "note template") + assert.Equal(t, 1, note.line, "note line") + assert.Equal(t, 15, note.column, "note column") + assert.Equal(t, "2 operands need 3 cases, over --max-cases=2", note.reason(), "reason") } // TestValuesDraw states what a substituted value looks like: a literal of // the operand's own type where there is one, and a quoted word otherwise. func TestValuesDraw(t *testing.T) { word := regexp.MustCompile(`^"[a-z]{4}"$`) - isWord := func(s string) error { - if !word.MatchString(s) { - return fmt.Errorf("not a quoted four letter word") - } - return nil - } + isWord := func(s string) bool { return word.MatchString(s) } for _, tt := range []struct { name string typ types.Type - valid func(string) error + valid func(string) bool }{ {name: "a string", typ: types.Typ[types.String], valid: isWord}, {name: "a safe string", typ: safeHTML(), valid: isWord}, {name: "no type", typ: nil, valid: isWord}, {name: "a struct", typ: types.NewStruct(nil, nil), valid: isWord}, {name: "a complex number", typ: types.Typ[types.Complex128], valid: isWord}, - {name: "a bool", typ: types.Typ[types.Bool], valid: func(s string) error { _, err := strconv.ParseBool(s); return err }}, - {name: "an int", typ: types.Typ[types.Int], valid: func(s string) error { _, err := strconv.Atoi(s); return err }}, - {name: "a float", typ: types.Typ[types.Float64], valid: func(s string) error { _, err := strconv.ParseFloat(s, 64); return err }}, + {name: "a bool", typ: types.Typ[types.Bool], valid: func(s string) bool { _, err := strconv.ParseBool(s); return err == nil }}, + {name: "an int", typ: types.Typ[types.Int], valid: func(s string) bool { _, err := strconv.Atoi(s); return err == nil }}, + {name: "a float", typ: types.Typ[types.Float64], valid: func(s string) bool { _, err := strconv.ParseFloat(s, 64); return err == nil }}, } { t.Run(tt.name, func(t *testing.T) { v := newValues(1) for range 20 { - if drawn := v.draw(tt.typ); tt.valid(drawn) != nil { - t.Fatalf("draw = %q, which is not a literal of %v", drawn, tt.typ) - } + drawn := v.draw(tt.typ) + require.True(t, tt.valid(drawn), "draw = %q, which is not a literal of %v", drawn, tt.typ) } }) } @@ -187,10 +170,6 @@ func TestValuesAreSeeded(t *testing.T) { } return out } - if !slices.Equal(draws(7), draws(7)) { - t.Error("the same seed drew different values") - } - if slices.Equal(draws(7), draws(8)) { - t.Error("different seeds drew the same values") - } + assert.Equal(t, draws(7), draws(7), "the same seed draws the same values") + assert.NotEqual(t, draws(7), draws(8), "different seeds draw different values") } diff --git a/internal/mutation/fixtures_test.go b/internal/mutation/fixtures_test.go index e1c306ef..59759bd8 100644 --- a/internal/mutation/fixtures_test.go +++ b/internal/mutation/fixtures_test.go @@ -13,6 +13,8 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/asteval" ) @@ -46,12 +48,8 @@ func writeFiles(t *testing.T, dir string, files map[string]string) { t.Helper() for name, content := range files { path := filepath.Join(dir, filepath.FromSlash(name)) - if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(path, []byte(content), 0o600); err != nil { - t.Fatal(err) - } + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o700)) + require.NoError(t, os.WriteFile(path, []byte(content), 0o600)) } } @@ -165,9 +163,8 @@ func (r *repo) git(args ...string) { cmd := exec.Command("git", args...) cmd.Dir = r.dir cmd.Env = r.env - if out, err := cmd.CombinedOutput(); err != nil { - r.t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out) - } + out, err := cmd.CombinedOutput() + require.NoError(r.t, err, "git %s\n%s", strings.Join(args, " "), out) } func (r *repo) write(files map[string]string) { @@ -191,9 +188,8 @@ func exitStatusOne(t *testing.T) error { } err := exec.Command("false").Run() exitErr, ok := errors.AsType[*exec.ExitError](err) - if !ok || exitErr.ExitCode() != 1 { - t.Fatalf("false = %v, want exit status 1", err) - } + require.True(t, ok, "false = %v, want exit status 1", err) + require.Equal(t, 1, exitErr.ExitCode(), "false = %v", err) return err } @@ -205,13 +201,9 @@ func exitStatusOne(t *testing.T) error { func scopeOf(t *testing.T, name, text string, dot types.Type) scope { t.Helper() trees, err := asteval.ParseTrees(name, text, "", "", nil) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) tree, ok := trees[name] - if !ok { - t.Fatalf("the text does not hold %q", name) - } + require.True(t, ok, "the text does not hold %q", name) file := name + ".gohtml" return scope{ template: name, @@ -245,9 +237,8 @@ func reportText(t *testing.T, p *plan) string { report := p.report() report.DryRun, report.Verbose = true, true var out strings.Builder - if _, err := report.WriteTo(&out); err != nil { - t.Fatal(err) - } + _, err := report.WriteTo(&out) + require.NoError(t, err) return out.String() } @@ -289,13 +280,9 @@ func dataType(t *testing.T, src, name string) types.Type { t.Helper() fset := token.NewFileSet() file, err := parser.ParseFile(fset, "data.go", "package data\n"+src, 0) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) pkg, err := new(types.Config).Check("example.com/data", fset, []*ast.File{file}, nil) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) return pkg.Scope().Lookup(name).Type() } diff --git a/internal/mutation/mutant_test.go b/internal/mutation/mutant_test.go index de388ae5..a47723df 100644 --- a/internal/mutation/mutant_test.go +++ b/internal/mutation/mutant_test.go @@ -1,10 +1,14 @@ package mutation import ( + "strconv" "strings" "testing" "text/template/parse" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/asteval" ) @@ -24,9 +28,7 @@ func TestValueStart(t *testing.T) { } { t.Run(tt.text, func(t *testing.T) { trees, err := asteval.ParseTrees("t", tt.text, "", "", nil) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) nodes := trees["t"].Root.Nodes var pipe *parse.PipeNode switch node := nodes[len(nodes)-1].(type) { @@ -39,13 +41,9 @@ func TestValueStart(t *testing.T) { } src := newFileSource("t.gohtml", "t.gohtml", tt.text, "", "") _, r, ok := regionAt(src.regions, int(pipe.Position())) - if !ok { - t.Fatalf("no region holds the pipeline at %d", pipe.Position()) - } + require.True(t, ok, "no region holds the pipeline at %d", pipe.Position()) start := valueStart(pipe) - if got := tt.text[start:r.innerEnd]; got != tt.want { - t.Errorf("value = %q, want %q", got, tt.want) - } + assert.Equal(t, tt.want, tt.text[start:r.innerEnd], "value") }) } } @@ -58,9 +56,7 @@ func TestMutantsAreReportedWhereTheFileHoldsThem(t *testing.T) { const goFile = "package p\n\nvar t = `x\n {{.A}}`\n" start, end := strings.Index(goFile, "`"), strings.LastIndex(goFile, "`")+1 literal, err := newLiteralSource("p.go", "p.go", "t", goFile, "", "", start, end) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) for _, tt := range []struct { name string src *templateSource @@ -73,9 +69,9 @@ func TestMutantsAreReportedWhereTheFileHoldsThem(t *testing.T) { e := &enumerator{src: tt.src, template: "t"} r := tt.src.regions[0] e.appendEdits(r, OperatorActionEmpty, []edit{{start: r.start, end: r.end}}, "") - if m := e.mutants[0]; m.Line != tt.line || m.Column != tt.column { - t.Errorf("mutant at %d:%d, want %d:%d", m.Line, m.Column, tt.line, tt.column) - } + m := e.mutants[0] + assert.Equal(t, tt.line, m.Line, "mutant line") + assert.Equal(t, tt.column, m.Column, "mutant column") }) } } @@ -93,9 +89,11 @@ func TestLineIndex(t *testing.T) { {offset: 7, line: 4, column: 1}, {offset: 8, line: 4, column: 2}, } { - if line, column := lines.at(tt.offset); line != tt.line || column != tt.column { - t.Errorf("at(%d) = %d:%d, want %d:%d", tt.offset, line, column, tt.line, tt.column) - } + t.Run(strconv.Itoa(tt.offset), func(t *testing.T) { + line, column := lines.at(tt.offset) + assert.Equal(t, tt.line, line, "at(%d) line", tt.offset) + assert.Equal(t, tt.column, column, "at(%d) column", tt.offset) + }) } } @@ -136,13 +134,8 @@ func TestConstructDropKeepsTheElseBranch(t *testing.T) { src := newFileSource("t.gohtml", "t.gohtml", tt.text, tt.left, tt.right) e := &enumerator{src: src, template: "t"} e.addConstructDrop(action{region: src.regions[0], index: 0}, OperatorWithEmpty) - out := e.mutants - if len(out) != 1 { - t.Fatalf("mutants = %d, want 1", len(out)) - } - if got := src.mutatedText(out[0].edits); got != tt.want { - t.Errorf("dropping the with leaves %q, want %q", got, tt.want) - } + require.Len(t, e.mutants, 1) + assert.Equal(t, tt.want, src.mutatedText(e.mutants[0].edits), "text left by dropping the with") }) } } diff --git a/internal/mutation/plan_test.go b/internal/mutation/plan_test.go index 2c05fc83..741da1a2 100644 --- a/internal/mutation/plan_test.go +++ b/internal/mutation/plan_test.go @@ -6,11 +6,14 @@ import ( "go/types" "path/filepath" "regexp" - "slices" + "strconv" "strings" "testing" "text/template/parse" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/asteval" "github.com/typelate/muxt/internal/source" ) @@ -90,9 +93,7 @@ func TestSelectorSkipsWhatARevisionAlreadyHeld(t *testing.T) { if tt.wantMutated { wantMutate = []string{tt.scope.template} } - if got := names(chosen.mutate); !slices.Equal(got, wantMutate) { - t.Errorf("mutate %q, want %q", got, wantMutate) - } + assert.Equal(t, wantMutate, names(chosen.mutate), "mutate") var wantUnchanged []string if tt.wantUnchanged != "" { @@ -102,9 +103,7 @@ func TestSelectorSkipsWhatARevisionAlreadyHeld(t *testing.T) { for _, u := range chosen.unchanged { got = append(got, u.Template+" "+u.DataType) } - if !slices.Equal(got, wantUnchanged) { - t.Errorf("unchanged %q, want %q", got, wantUnchanged) - } + assert.Equal(t, wantUnchanged, got, "unchanged") }) } } @@ -117,13 +116,10 @@ func TestSelectorMutatesATemplateOnceAcrossAWholeRun(t *testing.T) { page := scopeOf(t, "page", `{{.}}`, str) sel := newSelector(nil) - if got := names(sel.choose([]scope{page}, nil).mutate); !slices.Equal(got, []string{"page"}) { - t.Fatalf("the first traversal chose %q, want the page", got) - } + require.Equal(t, []string{"page"}, names(sel.choose([]scope{page}, nil).mutate), "the first traversal chose the page") second := sel.choose([]scope{page}, nil) - if len(second.mutate) != 0 || len(second.unchanged) != 0 { - t.Errorf("the second traversal chose %v and left %v, want neither", names(second.mutate), second.unchanged) - } + assert.Empty(t, second.mutate, "the second traversal chose nothing") + assert.Empty(t, second.unchanged, "the second traversal left nothing") } // TestSelectorHonoursTheTemplatePattern states that --template-pattern @@ -138,12 +134,8 @@ func TestSelectorHonoursTheTemplatePattern(t *testing.T) { scopeOf(t, "page", `{{.}}`, str), scopeOf(t, "footer", `
{{.}}
`, str), }, nil) - if got := names(chosen.mutate); !slices.Equal(got, []string{"page"}) { - t.Errorf("mutate %q, want only the page", got) - } - if len(chosen.unchanged) != 0 { - t.Errorf("unchanged %v, want none: the pattern is not a comparison", chosen.unchanged) - } + assert.Equal(t, []string{"page"}, names(chosen.mutate), "mutate only the page") + assert.Empty(t, chosen.unchanged, "the pattern is not a comparison") } // TestSelectorReportsTrims states what a trim says and when it is worth @@ -158,21 +150,15 @@ func TestSelectorReportsTrims(t *testing.T) { trimOf("row", str, "page.go", "page.go"), trimOf("row", str, "page.go", "page.go"), }) - if len(chosen.trimmed) != 1 { - t.Fatalf("trimmed %v, want one", chosen.trimmed) - } + require.Len(t, chosen.trimmed, 1) want := TrimmedTemplate{CallSite: "page.go:1:1", Template: "row", DataType: "string", FirstSeenAt: "page.go:1:1"} - if chosen.trimmed[0] != want { - t.Errorf("trimmed %+v, want %+v", chosen.trimmed[0], want) - } + assert.Equal(t, want, chosen.trimmed[0]) }) t.Run("a repeat of a template left alone is not reported", func(t *testing.T) { before := scopesOf([]scope{page}) chosen := newSelector(before).choose([]scope{page}, []trim{trimOf("page", str, "page.go", "page.go")}) - if len(chosen.trimmed) != 0 { - t.Errorf("trimmed %v, want none: the page was mutated nowhere", chosen.trimmed) - } + assert.Empty(t, chosen.trimmed, "the page was mutated nowhere") }) } @@ -182,9 +168,8 @@ func TestSelectorReportsTrims(t *testing.T) { func TestPlanReportCounts(t *testing.T) { p := &plan{mutants: make([]Mutant, 5), runnableN: 3, overBudget: 2} report := p.report() - if report.Total != 7 || report.Skipped != 4 { - t.Errorf("total %d, skipped %d, want 7 and 4", report.Total, report.Skipped) - } + assert.Equal(t, 7, report.Total, "total") + assert.Equal(t, 4, report.Skipped, "skipped") } func TestIndexTreesKeepsTheFirstNonEmptyTree(t *testing.T) { @@ -219,12 +204,8 @@ func TestIndexTreesKeepsTheFirstNonEmptyTree(t *testing.T) { src.rootName = "page" } index, err := indexTrees(tt.sources, nil) - if err != nil { - t.Fatal(err) - } - if got := index["page"].src.path; got != tt.want { - t.Errorf("indexTrees(...)[page] comes from %q, want %q", got, tt.want) - } + require.NoError(t, err) + assert.Equal(t, tt.want, index["page"].src.path, "indexTrees(...)[page] source") }) } } @@ -232,17 +213,14 @@ func TestIndexTreesKeepsTheFirstNonEmptyTree(t *testing.T) { func TestIndexTreesNamesTheSourceThatDoesNotParse(t *testing.T) { src := newFileSource("bad.gohtml", "bad.gohtml", "{{if}}", "", "") _, err := indexTrees([]*templateSource{src}, nil) - if err == nil || !strings.HasPrefix(err.Error(), "bad.gohtml: ") { - t.Errorf("indexTrees(bad source) error = %v, want one naming bad.gohtml", err) - } + require.Error(t, err, "indexTrees(bad source)") + assert.True(t, strings.HasPrefix(err.Error(), "bad.gohtml: "), "indexTrees(bad source) error = %v, want one naming bad.gohtml", err) } func TestVerifyReadable(t *testing.T) { treeOf := func(text string) *parse.Tree { trees, err := asteval.ParseTrees("page", text, "", "", nil) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) return trees["page"] } definition := func(name, text string) source.Definition { @@ -308,12 +286,13 @@ func TestVerifyReadable(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { got := verifyReadable(tt.defs, tt.index, filepath.Join("/work", "app")) - if (got != nil) != tt.want { - t.Fatalf("verifyReadable(...) = %v, want error %t", got, tt.want) - } - if got != nil && (got.Path != tt.wantPath || got.Template != tt.wantAbout) { - t.Errorf("verifyReadable(...) = template %q at %q, want %q at %q", got.Template, got.Path, tt.wantAbout, tt.wantPath) + if !tt.want { + require.Nil(t, got, "verifyReadable(...)") + return } + require.NotNil(t, got, "verifyReadable(...)") + assert.Equal(t, tt.wantPath, got.Path, "verifyReadable(...) path") + assert.Equal(t, tt.wantAbout, got.Template, "verifyReadable(...) template") }) } } @@ -344,14 +323,14 @@ func TestPlanValidate(t *testing.T) { err := tt.plan.validate([]string{"templates"}) noCalls, isNoCalls := errors.AsType[*NoCallSitesError](err) noMutations, isNoMutations := errors.AsType[*NoMutationsError](err) - if isNoCalls != tt.wantNoCalls || isNoMutations != tt.wantNoMutant || (err != nil) != (tt.wantNoCalls || tt.wantNoMutant) { - t.Fatalf("validate() = %v, want no call sites %t, no mutations %t", err, tt.wantNoCalls, tt.wantNoMutant) - } - if isNoCalls && !slices.Equal(noCalls.Variables, []string{"templates"}) { - t.Errorf("validate() names variables %v, want [templates]", noCalls.Variables) + require.Equal(t, tt.wantNoCalls, isNoCalls, "validate() = %v: no call sites", err) + require.Equal(t, tt.wantNoMutant, isNoMutations, "validate() = %v: no mutations", err) + require.Equal(t, tt.wantNoCalls || tt.wantNoMutant, err != nil, "validate() = %v: any error", err) + if isNoCalls { + assert.Equal(t, []string{"templates"}, noCalls.Variables, "validate() variables") } - if isNoMutations && noMutations.Templates != tt.plan.templates { - t.Errorf("validate() counts %d templates, want %d", noMutations.Templates, tt.plan.templates) + if isNoMutations { + assert.Equal(t, tt.plan.templates, noMutations.Templates, "validate() templates") } }) } @@ -364,9 +343,9 @@ func TestConfigurationMaxCases(t *testing.T) { {configured: 1, want: 1}, {configured: 20, want: 20}, } { - if got := (Configuration{MaxCases: tt.configured}).maxCases(); got != tt.want { - t.Errorf("Configuration{MaxCases: %d}.maxCases() = %d, want %d", tt.configured, got, tt.want) - } + t.Run(strconv.Itoa(tt.configured), func(t *testing.T) { + assert.Equal(t, tt.want, (Configuration{MaxCases: tt.configured}).maxCases(), "Configuration{MaxCases: %d}.maxCases()", tt.configured) + }) } } @@ -382,9 +361,7 @@ func TestTemplateFilter(t *testing.T) { {name: "other name", pattern: regexp.MustCompile(`^page$`), in: "footer", want: false}, } { t.Run(tt.name, func(t *testing.T) { - if got := templateFilter(tt.pattern)(tt.in); got != tt.want { - t.Errorf("templateFilter(%v)(%q) = %t, want %t", tt.pattern, tt.in, got, tt.want) - } + assert.Equal(t, tt.want, templateFilter(tt.pattern)(tt.in), "templateFilter(%v)(%q)", tt.pattern, tt.in) }) } } @@ -400,9 +377,7 @@ func TestHasRoot(t *testing.T) { {name: "tree with root", tree: &parse.Tree{Root: &parse.ListNode{}}, want: true}, } { t.Run(tt.name, func(t *testing.T) { - if got := hasRoot(tt.tree); got != tt.want { - t.Errorf("hasRoot(%s) = %t, want %t", tt.name, got, tt.want) - } + assert.Equal(t, tt.want, hasRoot(tt.tree), "hasRoot(%s)", tt.name) }) } } @@ -417,9 +392,7 @@ func TestRelativePath(t *testing.T) { {name: "no relative path", dir: "relative", file: filepath.Join(dir, "a.gohtml"), want: filepath.Join(dir, "a.gohtml")}, } { t.Run(tt.name, func(t *testing.T) { - if got := relativePath(tt.dir, tt.file); got != tt.want { - t.Errorf("relativePath(%q, %q) = %q, want %q", tt.dir, tt.file, got, tt.want) - } + assert.Equal(t, tt.want, relativePath(tt.dir, tt.file), "relativePath(%q, %q)", tt.dir, tt.file) }) } } From f37b00a77a10b20bbf8d0020dbc2412d66549578 Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:31:19 -0700 Subject: [PATCH 11/23] use testify in the diff and go test tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/mutation/diff_test.go | 140 +++++++++++-------------------- internal/mutation/gotest_test.go | 33 ++++---- 2 files changed, 69 insertions(+), 104 deletions(-) diff --git a/internal/mutation/diff_test.go b/internal/mutation/diff_test.go index b230f27f..98f28235 100644 --- a/internal/mutation/diff_test.go +++ b/internal/mutation/diff_test.go @@ -3,17 +3,18 @@ package mutation import ( "archive/tar" "bytes" - "errors" "fmt" "go/types" "io" "os" "path/filepath" "runtime" - "slices" "strings" "testing" "testing/iotest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // TestRevisionChanged states which scopes a --diff run mutates: one whose @@ -35,9 +36,7 @@ func TestRevisionChanged(t *testing.T) { {name: "a type of dot not reached with before", text: `{{.}}`, dot: types.Typ[types.Int], want: true}, } { t.Run(tt.name, func(t *testing.T) { - if got := before.changed(scopeOf(t, "t", tt.text, tt.dot)); got != tt.want { - t.Errorf("changed = %t, want %t", got, tt.want) - } + assert.Equal(t, tt.want, before.changed(scopeOf(t, "t", tt.text, tt.dot)), "changed") }) } } @@ -49,16 +48,11 @@ func tarOf(t *testing.T, entries ...tarEntry) *bytes.Buffer { w := tar.NewWriter(&buf) for _, e := range entries { e.header.Size = int64(len(e.body)) - if err := w.WriteHeader(&e.header); err != nil { - t.Fatal(err) - } - if _, err := w.Write([]byte(e.body)); err != nil { - t.Fatal(err) - } - } - if err := w.Close(); err != nil { - t.Fatal(err) + require.NoError(t, w.WriteHeader(&e.header)) + _, err := w.Write([]byte(e.body)) + require.NoError(t, err) } + require.NoError(t, w.Close()) return &buf } @@ -77,16 +71,10 @@ func TestExtractWritesTheTree(t *testing.T) { tarEntry{header: tar.Header{Typeflag: tar.TypeReg, Name: "sub/page.gohtml", Mode: 0o644}, body: `{{.}}`}, ) dir := t.TempDir() - if err := extract(archive, dir); err != nil { - t.Fatalf("extract = %v", err) - } + require.NoError(t, extract(archive, dir)) got, err := os.ReadFile(filepath.Join(dir, "sub", "page.gohtml")) - if err != nil { - t.Fatal(err) - } - if string(got) != `{{.}}` { - t.Errorf("sub/page.gohtml = %q, want %q", got, `{{.}}`) - } + require.NoError(t, err) + assert.Equal(t, `{{.}}`, string(got), "sub/page.gohtml") } // TestExtractWritesASymlink states that a symlink in the tree is made as @@ -100,16 +88,16 @@ func TestExtractWritesASymlink(t *testing.T) { tarEntry{header: tar.Header{Typeflag: tar.TypeSymlink, Name: "link/page.gohtml", Linkname: "../sub/page.gohtml"}}, ) dir := t.TempDir() - if err := extract(archive, dir); err != nil { - t.Fatalf("extract = %v", err) - } + require.NoError(t, extract(archive, dir)) link := filepath.Join(dir, "link", "page.gohtml") - if target, err := os.Readlink(link); err != nil || target != "../sub/page.gohtml" { - t.Errorf("Readlink = %q, %v, want %q", target, err, "../sub/page.gohtml") - } - if got, err := os.ReadFile(link); err != nil || string(got) != `{{.}}` { - t.Errorf("reading through the link = %q, %v, want %q", got, err, `{{.}}`) - } + + target, err := os.Readlink(link) + assert.NoError(t, err, "Readlink") + assert.Equal(t, "../sub/page.gohtml", target, "Readlink") + + got, err := os.ReadFile(link) + assert.NoError(t, err, "reading through the link") + assert.Equal(t, `{{.}}`, string(got), "reading through the link") } // TestExtractRefusesALinkOutOfTheTree states that a symlink pointing @@ -117,14 +105,12 @@ func TestExtractWritesASymlink(t *testing.T) { // hold. func TestExtractRefusesALinkOutOfTheTree(t *testing.T) { for _, target := range []string{"../../escape", "/etc/hosts"} { - archive := tarOf(t, tarEntry{header: tar.Header{Typeflag: tar.TypeSymlink, Name: "link/escape", Linkname: target}}) - dir := t.TempDir() - if err := extract(archive, dir); err == nil { - t.Errorf("extract of a link to %q = nil, want an error", target) - } - if _, err := os.Lstat(filepath.Join(dir, "link", "escape")); err == nil { - t.Errorf("extract made the link to %q", target) - } + t.Run(target, func(t *testing.T) { + archive := tarOf(t, tarEntry{header: tar.Header{Typeflag: tar.TypeSymlink, Name: "link/escape", Linkname: target}}) + dir := t.TempDir() + assert.Error(t, extract(archive, dir), "extract of a link to %q", target) + assert.NoFileExists(t, filepath.Join(dir, "link", "escape"), "extract made the link to %q", target) + }) } } @@ -143,8 +129,10 @@ func TestCheckSymlink(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { err := checkSymlink(tt.entry, tt.target) - if (err != nil) != tt.wantErr { - t.Errorf("checkSymlink(%q, %q) = %v, want error %t", tt.entry, tt.target, err, tt.wantErr) + if tt.wantErr { + assert.Error(t, err, "checkSymlink(%q, %q)", tt.entry, tt.target) + } else { + assert.NoError(t, err, "checkSymlink(%q, %q)", tt.entry, tt.target) } }) } @@ -155,23 +143,20 @@ func TestCheckSymlink(t *testing.T) { // truncated copy would be compared as though it had changed. func TestWriteArchivedReportsAReadError(t *testing.T) { path := filepath.Join(t.TempDir(), "page.gohtml") - if err := writeArchived(path, iotest.ErrReader(io.ErrUnexpectedEOF), 0o644); !errors.Is(err, io.ErrUnexpectedEOF) { - t.Errorf("writeArchived = %v, want %v", err, io.ErrUnexpectedEOF) - } + err := writeArchived(path, iotest.ErrReader(io.ErrUnexpectedEOF), 0o644) + assert.ErrorIs(t, err, io.ErrUnexpectedEOF, "writeArchived") } // TestExtractRefusesAnEntryOutsideTheTree states that an entry naming a // path outside the directory is refused rather than written. func TestExtractRefusesAnEntryOutsideTheTree(t *testing.T) { for _, name := range []string{"../escape.txt", "/escape.txt"} { - dir := t.TempDir() - archive := tarOf(t, tarEntry{header: tar.Header{Typeflag: tar.TypeReg, Name: name, Mode: 0o644}, body: "x"}) - if err := extract(archive, filepath.Join(dir, "tree")); err == nil { - t.Errorf("extract(%q) = nil, want an error", name) - } - if _, err := os.Stat(filepath.Join(dir, "escape.txt")); err == nil { - t.Errorf("extract(%q) wrote outside the tree", name) - } + t.Run(name, func(t *testing.T) { + dir := t.TempDir() + archive := tarOf(t, tarEntry{header: tar.Header{Typeflag: tar.TypeReg, Name: name, Mode: 0o644}, body: "x"}) + assert.Error(t, extract(archive, filepath.Join(dir, "tree")), "extract(%q)", name) + assert.NoFileExists(t, filepath.Join(dir, "escape.txt"), "extract(%q) wrote outside the tree", name) + }) } } @@ -276,26 +261,14 @@ func TestNewPlanWithDiff(t *testing.T) { } p, err := newPlan(config, web) - if err != nil { - t.Fatal(err) - } - if p.diffError != "" { - t.Fatalf("the templates at HEAD could not be read: %s", p.diffError) - } - if got := mutatedTemplates(p); !slices.Equal(got, tt.mutated) { - t.Errorf("mutated %q, want %q", got, tt.mutated) - } - if got := unchangedTemplates(p); !slices.Equal(got, tt.unchanged) { - t.Errorf("unchanged %q, want %q", got, tt.unchanged) - } - if len(p.trimmed) != tt.trimmed { - t.Errorf("trimmed %v, want %d", p.trimmed, tt.trimmed) - } + require.NoError(t, err) + require.Empty(t, p.diffError, "the templates at HEAD could not be read") + assert.Equal(t, tt.mutated, mutatedTemplates(p), "mutated") + assert.Equal(t, tt.unchanged, unchangedTemplates(p), "unchanged") + assert.Len(t, p.trimmed, tt.trimmed, "trimmed") text := reportText(t, p) for _, want := range tt.reportSays { - if !strings.Contains(text, want) { - t.Errorf("report does not say %q:\n%s", want, text) - } + assert.Contains(t, text, want, "report") } }) } @@ -313,18 +286,10 @@ func TestNewPlanWithDiffAtARevisionItCannotRead(t *testing.T) { config.Diff = "empty" p, err := newPlan(config, web) - if err != nil { - t.Fatal(err) - } - if p.diffError == "" { - t.Error("diffError is empty, want why the templates could not be read") - } - if got, want := mutatedTemplates(p), []string{"page server.Summary", "name string", "count float64"}; !slices.Equal(got, want) { - t.Errorf("mutated %q, want every template", got) - } - if text := reportText(t, p); !strings.Contains(text, "every template counts as changed: the templates at empty could not be read (") { - t.Errorf("report does not say why everything was mutated:\n%s", text) - } + require.NoError(t, err) + assert.NotEmpty(t, p.diffError, "diffError says why the templates could not be read") + assert.Equal(t, []string{"page server.Summary", "name string", "count float64"}, mutatedTemplates(p), "mutated: every template") + assert.Contains(t, reportText(t, p), "every template counts as changed: the templates at empty could not be read (", "report says why everything was mutated") } // TestNewPlanWithDiffAtARevisionGitDoesNotKnow states that an unknown @@ -338,10 +303,7 @@ func TestNewPlanWithDiffAtARevisionGitDoesNotKnow(t *testing.T) { config.Diff = "no-such-revision" _, err := newPlan(config, web) - if err == nil || !strings.Contains(err.Error(), "no-such-revision") { - t.Fatalf("newPlan = %v, want an error naming the revision", err) - } - if !strings.Contains(err.Error(), "fatal:") { - t.Errorf("newPlan = %v, want what git said", err) - } + require.Error(t, err, "newPlan") + require.ErrorContains(t, err, "no-such-revision", "newPlan names the revision") + assert.ErrorContains(t, err, "fatal:", "newPlan says what git said") } diff --git a/internal/mutation/gotest_test.go b/internal/mutation/gotest_test.go index 25e79a57..e43e1870 100644 --- a/internal/mutation/gotest_test.go +++ b/internal/mutation/gotest_test.go @@ -4,8 +4,9 @@ import ( "errors" "os/exec" "regexp" - "slices" "testing" + + "github.com/stretchr/testify/assert" ) func TestCheckGoTestArgs(t *testing.T) { @@ -25,8 +26,10 @@ func TestCheckGoTestArgs(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { err := CheckGoTestArgs(tt.args) - if refused := err != nil; refused != tt.refused { - t.Errorf("CheckGoTestArgs(%q) = %v, want refused %t", tt.args, err, tt.refused) + if tt.refused { + assert.Error(t, err, "CheckGoTestArgs(%q)", tt.args) + } else { + assert.NoError(t, err, "CheckGoTestArgs(%q)", tt.args) } }) } @@ -59,9 +62,7 @@ func TestGoTestArgsPutTheCallersFlagsAfterThePackages(t *testing.T) { }, } { t.Run(tt.name, func(t *testing.T) { - if got := tt.test.args(tt.flags...); !slices.Equal(got, tt.want) { - t.Errorf("args(%q) = %q, want %q", tt.flags, got, tt.want) - } + assert.Equal(t, tt.want, tt.test.args(tt.flags...), "args(%q)", tt.flags) }) } } @@ -86,10 +87,11 @@ func TestConfigurationGoTest(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { got := tt.config.goTest("/work") - if got.dir != tt.want.dir || !slices.Equal(got.packages, tt.want.packages) || got.match != tt.want.match || - !slices.Equal(got.extra, tt.want.extra) || !slices.Equal(got.env, tt.want.env) { - t.Errorf("goTest(/work) = %+v, want %+v", got, tt.want) - } + assert.Equal(t, tt.want.dir, got.dir, "goTest(/work) dir") + assert.Equal(t, tt.want.packages, got.packages, "goTest(/work) packages") + assert.Same(t, tt.want.match, got.match, "goTest(/work) match") + assert.Equal(t, tt.want.extra, got.extra, "goTest(/work) extra") + assert.Equal(t, tt.want.env, got.env, "goTest(/work) env") }) } } @@ -110,8 +112,11 @@ func TestVerdictOf(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { got, err := verdictOf(tt.err) - if got != tt.want || (err != nil) != tt.wantHasErr || (tt.wantHasErr && !errors.Is(err, tt.wantErr)) { - t.Errorf("verdictOf(%v) = %q, %v, want %q, %v", tt.err, got, err, tt.want, tt.wantErr) + assert.Equal(t, tt.want, got, "verdictOf(%v) status", tt.err) + if tt.wantHasErr { + assert.ErrorIs(t, err, tt.wantErr, "verdictOf(%v) error", tt.err) + } else { + assert.NoError(t, err, "verdictOf(%v) error", tt.err) } }) } @@ -136,9 +141,7 @@ func TestIsTestFailureOnlyCountsATestThatRan(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { err := exec.Command("sh", "-c", tt.script).Run() - if got := isTestFailure(err); got != tt.want { - t.Errorf("isTestFailure(%v) = %t, want %t", err, got, tt.want) - } + assert.Equal(t, tt.want, isTestFailure(err), "isTestFailure(%v)", err) }) } } From 5e31391037adb1d8cd73f55d63ec15f598aaf53a Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:32:59 -0700 Subject: [PATCH 12/23] use testify in the run and runner tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/mutation/run_test.go | 132 ++++++++++--------------------- internal/mutation/runner_test.go | 132 ++++++++++++------------------- 2 files changed, 91 insertions(+), 173 deletions(-) diff --git a/internal/mutation/run_test.go b/internal/mutation/run_test.go index c02c956c..077b90ed 100644 --- a/internal/mutation/run_test.go +++ b/internal/mutation/run_test.go @@ -3,9 +3,11 @@ package mutation import ( "errors" "regexp" - "slices" "strings" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func greetingConfig() Configuration { @@ -26,27 +28,19 @@ func TestRun(t *testing.T) { var status strings.Builder report, err := Run(config, dir, &status) - if err != nil { - t.Fatal(err) - } - if !report.Baseline.Passed { - t.Error("baseline did not pass") - } + require.NoError(t, err) + assert.True(t, report.Baseline.Passed, "baseline passed") var got []string for _, result := range report.Groups[0].Templates[0].Results { got = append(got, string(result.Status)+" "+string(result.Operator)) } - if want := []string{"KILL action-zero", "MISS if-false", "KILL if-true"}; !slices.Equal(got, want) { - t.Errorf("results %q, want %q", got, want) - } - if report.Killed != 2 || report.Missed != 1 { - t.Errorf("killed %d, missed %d, want 2 and 1", report.Killed, report.Missed) - } + assert.Equal(t, []string{"KILL action-zero", "MISS if-false", "KILL if-true"}, got, "results") + assert.Equal(t, 2, report.Killed, "killed") + assert.Equal(t, 1, report.Missed, "missed") lines := strings.Split(strings.TrimSpace(status.String()), "\n") - if len(lines) != 4 || !strings.HasPrefix(lines[0], "3 mutants across 1 template (complexity 2), baseline ") { - t.Errorf("status:\n%s\nwant the preamble and a line per mutant", status.String()) - } + assert.Len(t, lines, 4, "status has the preamble and a line per mutant:\n%s", status.String()) + assert.True(t, strings.HasPrefix(lines[0], "3 mutants across 1 template (complexity 2), baseline "), "status opens with the preamble:\n%s", status.String()) } // TestRunDryRun states that a dry run enumerates and tests nothing: here a @@ -62,15 +56,10 @@ func TestRunDryRun(t *testing.T) { config.DryRun = true var status strings.Builder report, err := Run(config, dir, &status) - if err != nil { - t.Fatal(err) - } - if !report.DryRun || report.Total != 3 { - t.Errorf("dry run %t with %d mutants, want a dry run of 3", report.DryRun, report.Total) - } - if status.Len() != 0 { - t.Errorf("status = %q, want nothing: no baseline ran", status.String()) - } + require.NoError(t, err) + assert.True(t, report.DryRun, "the report is a dry run") + assert.Equal(t, 3, report.Total, "mutants") + assert.Empty(t, status.String(), "status: no baseline ran") } // TestRunStopsWhenTheBaselineFails states that tests failing with nothing @@ -84,12 +73,8 @@ func TestRunStopsWhenTheBaselineFails(t *testing.T) { }) _, err := Run(greetingConfig(), dir, nil) baseline, ok := errors.AsType[*BaselineFailedError](err) - if !ok { - t.Fatalf("Run = %v, want a failing baseline", err) - } - if !strings.Contains(baseline.Output, "broken before mutation") { - t.Errorf("baseline output does not hold the failure:\n%s", baseline.Output) - } + require.True(t, ok, "Run = %v, want a failing baseline", err) + assert.Contains(t, baseline.Output, "broken before mutation", "baseline output holds the failure") } // TestRunStopsWhenGoTestCannotRun states that go test refusing to run, here @@ -101,12 +86,8 @@ func TestRunStopsWhenGoTestCannotRun(t *testing.T) { config := greetingConfig() config.GoTestArgs = []string{"-count=many"} _, err := Run(config, dir, nil) - if err == nil { - t.Fatal("Run = nil, want an error") - } - if _, ok := errors.AsType[*BaselineFailedError](err); ok { - t.Errorf("Run = %v, want an error other than a failing baseline", err) - } + require.Error(t, err, "Run") + assert.NotErrorAs(t, err, new(*BaselineFailedError), "Run: an error other than a failing baseline") } // TestNewPlanIncludesTestCallersWhenAsked states that a template rendered @@ -124,38 +105,28 @@ func TestNewPlanIncludesTestCallersWhenAsked(t *testing.T) { "func TestGreeting(t *testing.T) {\n\tif err := templates.ExecuteTemplate(io.Discard, \"greeting\", Greeting{Name: \"World\"}); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n", }) - if _, err := newPlan(greetingConfig(), dir); err == nil { - t.Fatal("newPlan = nil, want no call sites: the only one is in a test") - } else if _, ok := errors.AsType[*NoCallSitesError](err); !ok { - t.Fatalf("newPlan = %v, want no call sites", err) - } + _, err := newPlan(greetingConfig(), dir) + require.ErrorAs(t, err, new(*NoCallSitesError), "newPlan: the only call site is in a test") config := greetingConfig() config.IncludeTests = true p, err := newPlan(config, dir) - if err != nil { - t.Fatal(err) - } - if len(p.mutants) != 1 { - t.Fatalf("mutants = %d, want the one action", len(p.mutants)) - } + require.NoError(t, err) + require.Len(t, p.mutants, 1, "the one action") m := p.mutants[0] - if m.Path != "template.go" || m.Line != 5 { - t.Errorf("mutant at %s:%d, want template.go:5", m.Path, m.Line) - } - if want := `Parse("Hello, {{\"\"}}!\n")`; !strings.Contains(m.Apply(), want) { - t.Errorf("mutated file does not hold %s:\n%s", want, m.Apply()) - } + assert.Equal(t, "template.go", m.Path, "mutant path") + assert.Equal(t, 5, m.Line, "mutant line") + assert.Contains(t, m.Apply(), `Parse("Hello, {{\"\"}}!\n")`, "mutated file") } // neverRun is a suite that fails the test if anything runs it. func neverRun(t *testing.T) (func(...string) (string, error), func(string) (Status, error)) { t.Helper() return func(...string) (string, error) { - t.Error("the suite ran") + assert.Fail(t, "the suite ran") return "", nil }, func(string) (Status, error) { - t.Error("a mutant ran") + assert.Fail(t, "a mutant ran") return StatusMissed, nil } } @@ -169,15 +140,9 @@ func TestRunPlanDryRun(t *testing.T) { var status strings.Builder report, err := runPlan(p, Configuration{DryRun: true}, &status, baseline, verdict) - if err != nil { - t.Fatal(err) - } - if !report.DryRun { - t.Error("the report does not say it was a dry run") - } - if status.Len() != 0 { - t.Errorf("status = %q, want nothing", status.String()) - } + require.NoError(t, err) + assert.True(t, report.DryRun, "the report says it was a dry run") + assert.Empty(t, status.String(), "status") } // TestRunPlanStopsWhenTheBaselineFails states that tests failing with @@ -194,12 +159,8 @@ func TestRunPlanStopsWhenTheBaselineFails(t *testing.T) { }, verdict) baselineErr, ok := errors.AsType[*BaselineFailedError](err) - if !ok { - t.Fatalf("runPlan = %v, want a failing baseline", err) - } - if !strings.Contains(baselineErr.Output, "--- FAIL: TestIndex") { - t.Errorf("the error does not carry what failed: %q", baselineErr.Output) - } + require.True(t, ok, "runPlan = %v, want a failing baseline", err) + assert.Contains(t, baselineErr.Output, "--- FAIL: TestIndex", "the error carries what failed") } // TestRunPlanStopsWhenTheSuiteCannotRun states that go test failing to run @@ -215,12 +176,8 @@ func TestRunPlanStopsWhenTheSuiteCannotRun(t *testing.T) { return "", cannotRun }, verdict) - if !errors.Is(err, cannotRun) { - t.Fatalf("runPlan = %v, want the error go gave", err) - } - if _, ok := errors.AsType[*BaselineFailedError](err); ok { - t.Error("a command that could not run was reported as a failing baseline") - } + require.ErrorIs(t, err, cannotRun, "runPlan: the error go gave") + assert.NotErrorAs(t, err, new(*BaselineFailedError), "a command that could not run is not a failing baseline") } // TestRunPlanReportsWhatTheSuiteSaid states a whole run without a suite of @@ -239,20 +196,11 @@ func TestRunPlanReportsWhatTheSuiteSaid(t *testing.T) { } return StatusMissed, nil }) - if err != nil { - t.Fatal(err) - } - if got.Killed != 2 || got.Missed != 1 { - t.Errorf("killed %d, missed %d, want 2 and 1", got.Killed, got.Missed) - } - if !got.Baseline.Passed { - t.Error("the report does not say the baseline passed") - } - if !strings.HasPrefix(status.String(), "3 mutants across 1 template (complexity 0), baseline ") { - t.Errorf("status does not open with the preamble:\n%s", status.String()) - } + require.NoError(t, err) + assert.Equal(t, 2, got.Killed, "killed") + assert.Equal(t, 1, got.Missed, "missed") + assert.True(t, got.Baseline.Passed, "the report says the baseline passed") + assert.True(t, strings.HasPrefix(status.String(), "3 mutants across 1 template (complexity 0), baseline "), "status opens with the preamble:\n%s", status.String()) lines := strings.Count(strings.TrimSpace(status.String()), "\n") + 1 - if lines != 4 { - t.Errorf("status has %d lines, want the preamble and one per mutant:\n%s", lines, status.String()) - } + assert.Equal(t, 4, lines, "status has the preamble and one line per mutant:\n%s", status.String()) } diff --git a/internal/mutation/runner_test.go b/internal/mutation/runner_test.go index a61d216e..588565f7 100644 --- a/internal/mutation/runner_test.go +++ b/internal/mutation/runner_test.go @@ -6,11 +6,13 @@ import ( "fmt" "os" "path/filepath" - "slices" "strings" "sync/atomic" "testing" "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // runnerFixture builds a report of n runnable mutants of one template. @@ -53,21 +55,15 @@ func runnerFixture(t *testing.T, kills []bool) (*Report, *plan) { func readMutated(t *testing.T, overlay string) string { t.Helper() b, err := os.ReadFile(overlay) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var o struct{ Replace map[string]string } - if err := json.Unmarshal(b, &o); err != nil { - t.Fatal(err) - } + require.NoError(t, json.Unmarshal(b, &o)) for _, mutated := range o.Replace { text, err := os.ReadFile(mutated) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) return string(text) } - t.Fatal("overlay replaces nothing") + require.Fail(t, "overlay replaces nothing") return "" } @@ -81,35 +77,22 @@ func TestWriteMutantMapsTheFileToItsMutatedCopy(t *testing.T) { var dirs []string for i, mutant := range p.mutants { overlay, err := writeMutant(scratch, mutant) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) b, err := os.ReadFile(overlay) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var got struct{ Replace map[string]string } - if err := json.Unmarshal(b, &got); err != nil { - t.Fatal(err) - } + require.NoError(t, json.Unmarshal(b, &got)) mutated, ok := got.Replace[mutant.File] - if !ok || len(got.Replace) != 1 { - t.Fatalf("mutant %d overlay = %v, want only %s replaced", i, got.Replace, mutant.File) - } - if want := mutant.Apply(); readMutated(t, overlay) != want { - t.Errorf("mutant %d copy = %q, want %q", i, readMutated(t, overlay), want) - } - if filepath.Base(mutated) != filepath.Base(mutant.File) { - t.Errorf("mutant %d copy is named %q, want %q", i, filepath.Base(mutated), filepath.Base(mutant.File)) - } - if filepath.Dir(mutated) != filepath.Dir(overlay) || filepath.Dir(filepath.Dir(overlay)) != scratch { - t.Errorf("mutant %d wrote %s and %s, want both in a directory of its own under %s", i, mutated, overlay, scratch) - } + require.True(t, ok, "mutant %d overlay = %v, want %s replaced", i, got.Replace, mutant.File) + require.Len(t, got.Replace, 1, "mutant %d overlay replaces only %s", i, mutant.File) + + assert.Equal(t, mutant.Apply(), readMutated(t, overlay), "mutant %d copy", i) + assert.Equal(t, filepath.Base(mutant.File), filepath.Base(mutated), "mutant %d copy name", i) + assert.Equal(t, filepath.Dir(overlay), filepath.Dir(mutated), "mutant %d copy and overlay share a directory", i) + assert.Equal(t, scratch, filepath.Dir(filepath.Dir(overlay)), "mutant %d directory is directly under the scratch directory", i) dirs = append(dirs, filepath.Dir(overlay)) } - if dirs[0] == dirs[1] { - t.Errorf("mutants share the directory %s", dirs[0]) - } + assert.NotEqual(t, dirs[0], dirs[1], "mutants share a directory") } // TestRunAllWritesVerdictsInPlanOrder states that however many mutants run @@ -118,35 +101,32 @@ func TestWriteMutantMapsTheFileToItsMutatedCopy(t *testing.T) { func TestRunAllWritesVerdictsInPlanOrder(t *testing.T) { kills := []bool{true, false, true, true, false, false, true, false} for _, workers := range []int{1, 4} { - report, p := runnerFixture(t, kills) - r := &mutantRunner{ - plan: p, - scratch: t.TempDir(), - clock: &estimate{remaining: len(kills), workers: workers}, - test: func(overlay string) (Status, error) { - if strings.Contains(readMutated(t, overlay), "K") { - return StatusKilled, nil - } - return StatusMissed, nil - }, - } - if err := r.runAll(report, workers); err != nil { - t.Fatalf("workers %d: runAll = %v", workers, err) - } - - results := report.Groups[0].Templates[0].Results - for i, kill := range kills { - want := StatusMissed - if kill { - want = StatusKilled + t.Run(fmt.Sprintf("workers %d", workers), func(t *testing.T) { + report, p := runnerFixture(t, kills) + r := &mutantRunner{ + plan: p, + scratch: t.TempDir(), + clock: &estimate{remaining: len(kills), workers: workers}, + test: func(overlay string) (Status, error) { + if strings.Contains(readMutated(t, overlay), "K") { + return StatusKilled, nil + } + return StatusMissed, nil + }, } - if results[i].Status != want { - t.Errorf("workers %d: result %d = %s, want %s", workers, i, results[i].Status, want) + require.NoError(t, r.runAll(report, workers)) + + results := report.Groups[0].Templates[0].Results + for i, kill := range kills { + want := StatusMissed + if kill { + want = StatusKilled + } + assert.Equal(t, want, results[i].Status, "result %d", i) } - } - if report.Killed != 4 || report.Missed != 4 { - t.Errorf("workers %d: killed %d, missed %d, want 4 and 4", workers, report.Killed, report.Missed) - } + assert.Equal(t, 4, report.Killed, "killed") + assert.Equal(t, 4, report.Missed, "missed") + }) } } @@ -167,12 +147,8 @@ func TestRunAllStopsDispatchingAfterAnError(t *testing.T) { return "", errors.New("go could not run") }, } - if err := r.runAll(report, 1); err == nil { - t.Fatal("runAll = nil, want the error the go command gave") - } - if got := calls.Load(); got != 1 { - t.Errorf("mutants started = %d, want 1: nothing should start after the first error", got) - } + require.Error(t, r.runAll(report, 1), "runAll: the error the go command gave") + assert.Equal(t, int32(1), calls.Load(), "mutants started: nothing should start after the first error") } // TestRunAllReportsEachMutantAsItFinishes states the progress stream: a @@ -199,18 +175,14 @@ func TestRunAllReportsEachMutantAsItFinishes(t *testing.T) { return StatusMissed, nil }, } - if err := r.runAll(report, 1); err != nil { - t.Fatalf("runAll = %v", err) - } + require.NoError(t, r.runAll(report, 1)) want := []string{ fmt.Sprintf(`[1/3] KILL page.gohtml:0:0 "page" %s (0s, ~0s left)`, OperatorActionEmpty), fmt.Sprintf(`[2/3] SKIP page.gohtml:0:0 "page" %s (does not type check)`, OperatorActionEmpty), fmt.Sprintf(`[3/3] MISS page.gohtml:0:0 "page" %s (0s, ~0s left)`, OperatorActionEmpty), } - if got := strings.Split(strings.TrimSpace(progress.String()), "\n"); !slices.Equal(got, want) { - t.Errorf("progress:\n got %q\nwant %q", got, want) - } + assert.Equal(t, want, strings.Split(strings.TrimSpace(progress.String()), "\n"), "progress") } // TestReportTrims states the progress line for a trimmed subtree, which @@ -220,10 +192,8 @@ func TestReportTrims(t *testing.T) { trimmed := []TrimmedTemplate{{CallSite: "page.go:12:9", Template: "row", DataType: "server.Row", FirstSeenAt: "page.go:9:9"}} var out strings.Builder reportTrims(&out, trimmed) - if got, want := out.String(), "trimmed \"row\" at page.go:12:9: already mutated with server.Row from page.go:9:9\n"; got != want { - t.Errorf("reportTrims wrote %q, want %q", got, want) - } - reportTrims(nil, trimmed) + assert.Equal(t, "trimmed \"row\" at page.go:12:9: already mutated with server.Row from page.go:9:9\n", out.String(), "reportTrims") + assert.NotPanics(t, func() { reportTrims(nil, trimmed) }, "reportTrims without a writer") } // TestRoundDuration states how a duration is shown: to a tenth of a second @@ -237,8 +207,8 @@ func TestRoundDuration(t *testing.T) { {d: 1234 * time.Millisecond, want: "1.2s"}, {d: 90*time.Second + 600*time.Millisecond, want: "1m31s"}, } { - if got := roundDuration(tt.d); got != tt.want { - t.Errorf("roundDuration(%v) = %q, want %q", tt.d, got, tt.want) - } + t.Run(tt.want, func(t *testing.T) { + assert.Equal(t, tt.want, roundDuration(tt.d), "roundDuration(%v)", tt.d) + }) } } From b2d9dd925f16417d9947d2f16d27d92190d23f28 Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:34:06 -0700 Subject: [PATCH 13/23] use testify in the report and snapshot tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/mutation/report_test.go | 27 ++++++------------ internal/mutation/snapshot_test.go | 46 +++++++++++------------------- 2 files changed, 25 insertions(+), 48 deletions(-) diff --git a/internal/mutation/report_test.go b/internal/mutation/report_test.go index 455a84dd..78d3cd87 100644 --- a/internal/mutation/report_test.go +++ b/internal/mutation/report_test.go @@ -3,6 +3,9 @@ package mutation import ( "strings" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // sampleReport is a finished run over two call sites. "page" holds a mutant @@ -53,12 +56,8 @@ func writeReport(t *testing.T, report *Report) string { t.Helper() var out strings.Builder n, err := report.WriteTo(&out) - if err != nil { - t.Fatal(err) - } - if n != int64(out.Len()) { - t.Errorf("WriteTo = %d, but wrote %d bytes", n, out.Len()) - } + require.NoError(t, err) + assert.Equal(t, int64(out.Len()), n, "WriteTo count of bytes written") return out.String() } @@ -77,9 +76,7 @@ page.go:9:9 ExecuteTemplate "page" (dot: server.Page) 7 mutants, 3 killed, 3 missed, 1 skipped ` - if got := writeReport(t, sampleReport()); got != want { - t.Errorf("report:\n%s\nwant:\n%s", got, want) - } + assert.Equal(t, want, writeReport(t, sampleReport()), "report") } // TestVerboseReportListsEverything states that a verbose report lists every @@ -109,9 +106,7 @@ trimmed 1 subtree already mutated with the same dot: 7 mutants, 3 killed, 3 missed, 1 skipped ` - if got := writeReport(t, report); got != want { - t.Errorf("report:\n%s\nwant:\n%s", got, want) - } + assert.Equal(t, want, writeReport(t, report), "report") } // TestDryRunReportListsWhatWouldRun states that a dry run has no verdicts to @@ -125,9 +120,7 @@ func TestDryRunReportListsWhatWouldRun(t *testing.T) { " KILL 3:5 template-drop\n", "\n7 mutants, 6 runnable, 1 skipped\n", } { - if !strings.Contains(got, want) { - t.Errorf("report does not say %q:\n%s", want, got) - } + assert.Contains(t, got, want, "report") } } @@ -136,7 +129,5 @@ func TestDryRunReportListsWhatWouldRun(t *testing.T) { func TestReportSaysTheBaselineFailed(t *testing.T) { report := sampleReport() report.Baseline = BaselineResult{} - if got := writeReport(t, report); !strings.Contains(got, "(complexity 4, seed 1)\nbaseline failed\n") { - t.Errorf("report does not say the baseline failed:\n%s", got) - } + assert.Contains(t, writeReport(t, report), "(complexity 4, seed 1)\nbaseline failed\n", "report says the baseline failed") } diff --git a/internal/mutation/snapshot_test.go b/internal/mutation/snapshot_test.go index ab074f7d..79e8b380 100644 --- a/internal/mutation/snapshot_test.go +++ b/internal/mutation/snapshot_test.go @@ -10,6 +10,8 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/tools/txtar" "github.com/typelate/muxt/internal/configjson" @@ -44,16 +46,11 @@ var update = flag.Bool("update", false, "rewrite the want/ files of the snapshot // run_test.go and the integration suite do. func TestSnapshots(t *testing.T) { const command = "test-template-mutations" - if stray, _ := filepath.Glob(filepath.Join("testdata", "*.txtar")); len(stray) > 0 { - t.Fatalf("%s is not in a command's directory; the mutation run's archives are in testdata/%s", stray[0], command) - } + stray, _ := filepath.Glob(filepath.Join("testdata", "*.txtar")) + require.Empty(t, stray, "archives outside a command's directory; the mutation run's archives are in testdata/%s", command) archives, err := filepath.Glob(filepath.Join("testdata", command, "*.txtar")) - if err != nil { - t.Fatal(err) - } - if len(archives) == 0 { - t.Fatalf("no archives in testdata/%s", command) - } + require.NoError(t, err) + require.NotEmpty(t, archives, "no archives in testdata/%s", command) t.Run(command, func(t *testing.T) { for _, archivePath := range archives { runSnapshot(t, archivePath) @@ -67,13 +64,9 @@ func runSnapshot(t *testing.T, archivePath string) { t.Helper() t.Run(strings.TrimSuffix(filepath.Base(archivePath), ".txtar"), func(t *testing.T) { archive, err := txtar.ParseFile(archivePath) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) config := configuration(t, archive) - if !config.DryRun { - t.Fatal("a snapshot plans a dry run; config.json must set DryRun") - } + require.True(t, config.DryRun, "a snapshot plans a dry run; config.json must set DryRun") got := dryRunSnapshot(t, config, archive) if *update { files := slices.DeleteFunc(slices.Clone(archive.Files), func(file txtar.File) bool { @@ -85,9 +78,7 @@ func runSnapshot(t *testing.T, archivePath string) { } } archive.Files = files - if err := os.WriteFile(archivePath, txtar.Format(archive), 0o644); err != nil { - t.Fatal(err) - } + require.NoError(t, os.WriteFile(archivePath, txtar.Format(archive), 0o644)) return } want := make(map[string]string) @@ -97,9 +88,7 @@ func runSnapshot(t *testing.T, archivePath string) { } } for _, name := range []string{"error.txt", "report.txt"} { - if got[name] != want[name] { - t.Errorf("want/%s differs (run go test -run TestSnapshots -update to rewrite):\n--- got\n%s\n--- want\n%s", name, got[name], want[name]) - } + assert.Equal(t, want[name], got[name], "want/%s differs (run go test -run TestSnapshots -update to rewrite)", name) } }) } @@ -113,12 +102,10 @@ func configuration(t *testing.T, archive *txtar.Archive) Configuration { continue } var config Configuration - if err := json.Unmarshal(file.Data, &config, configjson.Options()); err != nil { - t.Fatalf("config.json: %v", err) - } + require.NoError(t, json.Unmarshal(file.Data, &config, configjson.Options()), "config.json") return config } - t.Fatal("the archive has no config.json") + require.Fail(t, "the archive has no config.json") return Configuration{} } @@ -173,19 +160,18 @@ func dryRunSnapshot(t *testing.T, config Configuration, archive *txtar.Archive) return fail(err) } report, err := runPlan(p, config, nil, func(...string) (string, error) { - t.Fatal("a dry run ran the baseline") + require.Fail(t, "a dry run ran the baseline") return "", nil }, func(string) (Status, error) { - t.Fatal("a dry run ran a mutant") + require.Fail(t, "a dry run ran a mutant") return "", nil }) if err != nil { return fail(err) } var out strings.Builder - if _, err := report.WriteTo(&out); err != nil { - t.Fatal(err) - } + _, err = report.WriteTo(&out) + require.NoError(t, err) got["report.txt"] = out.String() return got } From d2c0236fc433927c76c91227b1453e1768cfb57f Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:35:58 -0700 Subject: [PATCH 14/23] use testify in the scan, source, operand, dot, traverse and walk tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/mutation/dot_test.go | 24 +++++----- internal/mutation/operand_test.go | 25 ++++------- internal/mutation/scan_test.go | 72 +++++++++++++++--------------- internal/mutation/source_test.go | 35 ++++++--------- internal/mutation/traverse_test.go | 25 +++++------ internal/mutation/walk_test.go | 20 +++------ 6 files changed, 86 insertions(+), 115 deletions(-) diff --git a/internal/mutation/dot_test.go b/internal/mutation/dot_test.go index 23bbd999..4f637e3b 100644 --- a/internal/mutation/dot_test.go +++ b/internal/mutation/dot_test.go @@ -6,6 +6,8 @@ import ( "testing" "text/template/parse" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/typelate/check" "github.com/typelate/muxt/internal/asteval" @@ -16,9 +18,7 @@ import ( func TestRangeDot(t *testing.T) { item := dataType(t, pageSource, "Item") trees, err := asteval.ParseTrees("t", `{{range .}}{{end}}`, "", "", nil) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) pipe := trees["t"].Root.Nodes[0].(*parse.RangeNode).Pipe for _, tt := range []struct { name string @@ -34,10 +34,10 @@ func TestRangeDot(t *testing.T) { {name: "a struct", over: item}, {name: "nothing known", over: nil}, } { - got := rangeDot(tt.over, pipe, nil) - if types.TypeString(got, nil) != types.TypeString(tt.want, nil) { - t.Errorf("%s: rangeDot = %v, want %v", tt.name, got, tt.want) - } + t.Run(tt.name, func(t *testing.T) { + got := rangeDot(tt.over, pipe, nil) + assert.Equal(t, types.TypeString(tt.want, nil), types.TypeString(got, nil), "rangeDot over %s", tt.name) + }) } } @@ -84,13 +84,11 @@ func TestZeroLiteral(t *testing.T) { dot = page } trees, err := asteval.ParseTrees("t", tt.action, "", "", functions) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) pipe := trees["t"].Root.Nodes[0].(*parse.ActionNode).Pipe - if got, typed := zeroLiteral(dot, pipe, functions); got != tt.want || typed != tt.typed { - t.Errorf("zeroLiteral(%s) = %q, %t, want %q, %t", tt.action, got, typed, tt.want, tt.typed) - } + got, typed := zeroLiteral(dot, pipe, functions) + assert.Equal(t, tt.want, got, "zeroLiteral(%s) literal", tt.action) + assert.Equal(t, tt.typed, typed, "zeroLiteral(%s) typed", tt.action) }) } } diff --git a/internal/mutation/operand_test.go b/internal/mutation/operand_test.go index 8e533a4b..cdba5a2e 100644 --- a/internal/mutation/operand_test.go +++ b/internal/mutation/operand_test.go @@ -1,10 +1,12 @@ package mutation import ( - "slices" "testing" "text/template/parse" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/asteval" ) @@ -29,9 +31,7 @@ func TestOperandsAreFoundWhereTheyAreWritten(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { trees, err := asteval.ParseTrees("t", tt.text, "", "", nil) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var got []string for _, node := range trees["t"].Root.Nodes { a, ok := node.(*parse.ActionNode) @@ -39,15 +39,11 @@ func TestOperandsAreFoundWhereTheyAreWritten(t *testing.T) { continue } for _, op := range operands(tt.text, nil, a.Pipe) { - if written := tt.text[op.start:op.end]; written != op.text { - t.Errorf("operand %q spans %q", op.text, written) - } + assert.Equal(t, op.text, tt.text[op.start:op.end], "operand %q span", op.text) got = append(got, op.text) } } - if !slices.Equal(got, tt.want) { - t.Errorf("operands = %q, want %q", got, tt.want) - } + assert.Equal(t, tt.want, got, "operands") }) } } @@ -61,16 +57,11 @@ func TestCombinations(t *testing.T) { } editSets, details := combinations(ops, []string{`"x"`, `1`}) - wantDetails := []string{`.A="x"`, `.B=1`, `.A="x" .B=1`} - if !slices.Equal(details, wantDetails) { - t.Errorf("details = %q, want %q", details, wantDetails) - } + assert.Equal(t, []string{`.A="x"`, `.B=1`, `.A="x" .B=1`}, details, "details") wantEdits := [][]edit{ {{start: 2, end: 4, text: `"x"`}}, {{start: 5, end: 7, text: `1`}}, {{start: 2, end: 4, text: `"x"`}, {start: 5, end: 7, text: `1`}}, } - if !slices.EqualFunc(editSets, wantEdits, slices.Equal) { - t.Errorf("edits = %v, want %v", editSets, wantEdits) - } + assert.Equal(t, wantEdits, editSets, "edits") } diff --git a/internal/mutation/scan_test.go b/internal/mutation/scan_test.go index 6076df25..e7a5c353 100644 --- a/internal/mutation/scan_test.go +++ b/internal/mutation/scan_test.go @@ -1,6 +1,12 @@ package mutation -import "testing" +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) // TestRegionsBoundTheActionsContent states where an action's content // begins and ends, which is what a pipeline substitution is spliced over. @@ -62,23 +68,16 @@ func TestRegionsBoundTheActionsContent(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { found := regions(tt.text, "", "") - if len(found) != 1 { - t.Fatalf("regions = %d, want 1", len(found)) - } + require.Len(t, found, 1, "regions") r := found[0] - if r.start != 0 || r.end != len(tt.text) { - t.Errorf("region spans [%d,%d), want [0,%d)", r.start, r.end, len(tt.text)) - } + assert.Equal(t, 0, r.start, "region start") + assert.Equal(t, len(tt.text), r.end, "region end") // The content runs from after the delimiter and any leading // marker, which trimLeft finds, to innerEnd. from := trimLeft(tt.text, r.start+2, r.innerEnd) - if got := tt.text[from:r.innerEnd]; got != tt.content { - t.Errorf("content = %q, want %q", got, tt.content) - } - if r.keyword != tt.keyword { - t.Errorf("keyword = %q, want %q", r.keyword, tt.keyword) - } + assert.Equal(t, tt.content, tt.text[from:r.innerEnd], "content") + assert.Equal(t, tt.keyword, r.keyword, "keyword") }) } } @@ -88,9 +87,9 @@ func TestRegionsBoundTheActionsContent(t *testing.T) { // withDefault, and text/template lexes those as one identifier. func TestLeadingWordReadsTheWholeIdentifier(t *testing.T) { for _, text := range []string{`{{endX}}`, `{{end2}}`, `{{end_x}}`, `{{withDefault .A "x"}}`, `{{ifEmpty .A}}`, `{{rangeOf .A}}`} { - if got := regions(text, "", "")[0].keyword; got != "" { - t.Errorf("regions(%q) keyword = %q, want none", text, got) - } + t.Run(text, func(t *testing.T) { + assert.Empty(t, regions(text, "", "")[0].keyword, "regions(%q) keyword", text) + }) } } @@ -98,18 +97,20 @@ func TestLeadingWordReadsTheWholeIdentifier(t *testing.T) { // does not open a block, so the end is matched at the right depth. func TestMatchEndSkipsAFunctionNamedLikeAKeyword(t *testing.T) { const text = `{{range .Items}}{{withDefault . "x"}}{{end}}` - if end, _, ok := matchEnd(regions(text, "", ""), 0); !ok || end != 2 { - t.Errorf("matchEnd = %d, %t, want 2, true", end, ok) - } + end, _, ok := matchEnd(regions(text, "", ""), 0) + assert.True(t, ok, "matchEnd ok") + assert.Equal(t, 2, end, "matchEnd end") } // TestRegionsIgnoresAnUnfinishedTail states that an action cut off by the // end of the text is not reported, and costs the actions before it nothing. func TestRegionsIgnoresAnUnfinishedTail(t *testing.T) { for _, text := range []string{`{{.A}}{{`, `{{.A}}{{-`, `{{.A}}{{.B`, `{{.A}}{{/* x`, `{{.A}}{{/* x */`} { - if found := regions(text, "", ""); len(found) != 1 || text[found[0].start:found[0].end] != "{{.A}}" { - t.Errorf("regions(%q) = %v, want only {{.A}}", text, found) - } + t.Run(text, func(t *testing.T) { + found := regions(text, "", "") + require.Len(t, found, 1, "regions(%q)", text) + assert.Equal(t, "{{.A}}", text[found[0].start:found[0].end], "regions(%q) only", text) + }) } } @@ -119,9 +120,8 @@ func TestRegionsIgnoresAnUnfinishedTail(t *testing.T) { func TestRegionsEndAStringAtALineEnd(t *testing.T) { const text = "{{printf \"a\nb\"}}{{.B}}" found := regions(text, "", "") - if len(found) != 1 || text[found[0].start:found[0].end] != "{{.B}}" { - t.Errorf("regions(%q) = %v, want only {{.B}}", text, found) - } + require.Len(t, found, 1, "regions(%q)", text) + assert.Equal(t, "{{.B}}", text[found[0].start:found[0].end], "regions(%q) only", text) } // TestRegionAt states that a position belongs to the action whose @@ -141,9 +141,11 @@ func TestRegionAt(t *testing.T) { {pos: 12, index: 1, ok: true}, {pos: 13}, } { - if index, _, ok := regionAt(found, tt.pos); index != tt.index || ok != tt.ok { - t.Errorf("regionAt(%d) = %d, %t, want %d, %t", tt.pos, index, ok, tt.index, tt.ok) - } + t.Run(strconv.Itoa(tt.pos), func(t *testing.T) { + index, _, ok := regionAt(found, tt.pos) + assert.Equal(t, tt.index, index, "regionAt(%d) index", tt.pos) + assert.Equal(t, tt.ok, ok, "regionAt(%d) ok", tt.pos) + }) } } @@ -165,9 +167,9 @@ func TestMatchEnd(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { end, elseIndex, ok := matchEnd(regions(tt.text, "", ""), 0) - if end != tt.wantEnd || elseIndex != tt.wantElse || ok != tt.ok { - t.Errorf("matchEnd = %d, %d, %t, want %d, %d, %t", end, elseIndex, ok, tt.wantEnd, tt.wantElse, tt.ok) - } + assert.Equal(t, tt.wantEnd, end, "matchEnd end") + assert.Equal(t, tt.wantElse, elseIndex, "matchEnd else") + assert.Equal(t, tt.ok, ok, "matchEnd ok") }) } } @@ -185,11 +187,7 @@ func TestRegionsKeepsScanningPastSomethingItCannotRead(t *testing.T) { for _, r := range found { starts = append(starts, r.start) } - if len(found) < 2 { - t.Fatalf("regions = %v, want the actions on both sides of the unreadable one", starts) - } + require.GreaterOrEqual(t, len(found), 2, "regions starting at %v: want the actions on both sides of the unreadable one", starts) last := found[len(found)-1] - if got := text[last.start:last.end]; got != `{{.B}}` { - t.Errorf("last region = %q, want %q", got, `{{.B}}`) - } + assert.Equal(t, `{{.B}}`, text[last.start:last.end], "last region") } diff --git a/internal/mutation/source_test.go b/internal/mutation/source_test.go index 690bd67f..0e34fc2c 100644 --- a/internal/mutation/source_test.go +++ b/internal/mutation/source_test.go @@ -1,9 +1,11 @@ package mutation import ( - "slices" "strconv" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // TestLiteralOffsetsMapsEveryByteBack states where each byte of a @@ -79,23 +81,13 @@ func TestLiteralOffsetsMapsEveryByteBack(t *testing.T) { // The pair has to be a real one, or the table is asserting // against arithmetic nobody will ever run. decoded, err := strconv.Unquote(tt.literal) - if err != nil { - t.Fatalf("Unquote(%s) = %v", tt.literal, err) - } - if decoded != tt.value { - t.Fatalf("%s decodes to %q, but the table says %q", tt.literal, decoded, tt.value) - } + require.NoError(t, err, "Unquote(%s)", tt.literal) + require.Equal(t, tt.value, decoded, "%s decodes to a value other than the table's", tt.literal) got, err := literalOffsets(tt.literal, tt.value) - if err != nil { - t.Fatalf("literalOffsets(%s) = %v", tt.literal, err) - } - if !slices.Equal(got, tt.want) { - t.Fatalf("literalOffsets(%s) = %v, want %v", tt.literal, got, tt.want) - } - if len(got) != len(tt.value)+1 { - t.Errorf("offsets = %d, want one per byte of the value plus an end", len(got)) - } + require.NoError(t, err, "literalOffsets(%s)", tt.literal) + require.Equal(t, tt.want, got, "literalOffsets(%s)", tt.literal) + assert.Len(t, got, len(tt.value)+1, "offsets: one per byte of the value plus an end") }) } } @@ -117,9 +109,8 @@ func TestLiteralOffsetsRefusesAValueItCannotAccountFor(t *testing.T) { {name: "an escape that is not one", literal: `"\q"`, value: "q"}, } { t.Run(tt.name, func(t *testing.T) { - if got, err := literalOffsets(tt.literal, tt.value); err == nil { - t.Errorf("literalOffsets(%s, %q) = %v, want an error", tt.literal, tt.value, got) - } + got, err := literalOffsets(tt.literal, tt.value) + assert.Error(t, err, "literalOffsets(%s, %q) = %v", tt.literal, tt.value, got) }) } } @@ -136,8 +127,8 @@ func TestLiteralEncoder(t *testing.T) { {name: "raw cannot hold a carriage return", literal: "`{{.A}}`", mutated: "a\rb", want: `"a\rb"`}, {name: "interpreted is quoted", literal: `"{{.A}}\n"`, mutated: "{{0}}\n", want: `"{{0}}\n"`}, } { - if got := literalEncoder(tt.literal)(tt.mutated); got != tt.want { - t.Errorf("%s: encoded %q, want %q", tt.name, got, tt.want) - } + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, literalEncoder(tt.literal)(tt.mutated), "literalEncoder(%s)(%q)", tt.literal, tt.mutated) + }) } } diff --git a/internal/mutation/traverse_test.go b/internal/mutation/traverse_test.go index f5283897..54c1bf0a 100644 --- a/internal/mutation/traverse_test.go +++ b/internal/mutation/traverse_test.go @@ -4,6 +4,9 @@ import ( "go/types" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/asteval" ) @@ -24,13 +27,11 @@ func TestComplexity(t *testing.T) { {text: `{{if .A}}{{else if .B}}{{end}}`, want: 3}, {text: `{{if .A}}{{else}}{{with .B}}{{end}}{{end}}`, want: 3}, } { - trees, err := asteval.ParseTrees("t", tt.text, "", "", nil) - if err != nil { - t.Fatal(err) - } - if got := complexity(trees["t"].Root); got != tt.want { - t.Errorf("complexity(%s) = %d, want %d", tt.text, got, tt.want) - } + t.Run(tt.text, func(t *testing.T) { + trees, err := asteval.ParseTrees("t", tt.text, "", "", nil) + require.NoError(t, err) + assert.Equal(t, tt.want, complexity(trees["t"].Root), "complexity(%s)", tt.text) + }) } } @@ -48,11 +49,9 @@ func TestTypeNames(t *testing.T) { {typ: page, key: "example.com/data.Page", display: "data.Page"}, {typ: types.NewPointer(page), key: "*example.com/data.Page", display: "*data.Page"}, } { - if got := typeKey(tt.typ); got != tt.key { - t.Errorf("typeKey(%v) = %q, want %q", tt.typ, got, tt.key) - } - if got := typeDisplay(tt.typ); got != tt.display { - t.Errorf("typeDisplay(%v) = %q, want %q", tt.typ, got, tt.display) - } + t.Run(tt.key, func(t *testing.T) { + assert.Equal(t, tt.key, typeKey(tt.typ), "typeKey(%v)", tt.typ) + assert.Equal(t, tt.display, typeDisplay(tt.typ), "typeDisplay(%v)", tt.typ) + }) } } diff --git a/internal/mutation/walk_test.go b/internal/mutation/walk_test.go index 4eeba2a6..28597183 100644 --- a/internal/mutation/walk_test.go +++ b/internal/mutation/walk_test.go @@ -2,9 +2,11 @@ package mutation import ( "go/types" - "strings" "testing" "text/template/parse" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // TestWalkActionsOrder states the order actions are reported in, which is @@ -20,9 +22,7 @@ func TestWalkActionsOrder(t *testing.T) { `{{end}}` trees, err := parse.Parse("t", text+`{{define "p"}}x{{end}}`, "", "") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var got []string walkActions(text, regions(text, "", ""), nil, nil, trees["t"].Root, func(a action) { @@ -35,9 +35,7 @@ func TestWalkActionsOrder(t *testing.T) { "{{with .G}}", "{{.H}}", "{{.I}}", `{{template "p" .J}}`, } - if strings.Join(got, " ") != strings.Join(want, " ") { - t.Errorf("walk order:\n got %v\nwant %v", got, want) - } + assert.Equal(t, want, got, "walk order") } // TestWalkActionsNarrowsDot states that a body is walked with the dot its @@ -54,9 +52,7 @@ func TestWalkActionsNarrowsDot(t *testing.T) { `{{with .Owner}}{{.}}{{else}}{{.}}{{end}}` + `{{end}}` trees, err := parse.Parse("t", text, "", "") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) var got []string walkActions(text, regions(text, "", ""), dataType(t, pageSource, "Page"), nil, trees["t"].Root, func(a action) { @@ -71,7 +67,5 @@ func TestWalkActionsNarrowsDot(t *testing.T) { "*example.com/data.User", "example.com/data.Page", } - if strings.Join(got, " ") != strings.Join(want, " ") { - t.Errorf("body dots:\n got %v\nwant %v", got, want) - } + assert.Equal(t, want, got, "body dots") } From 16c1b77c969c589d66de715f5738875b70b1badd Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:30:40 -0700 Subject: [PATCH 15/23] use testify in the call resolution tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/muxt/argument_bind_internal_test.go | 11 +- internal/muxt/argument_test.go | 41 ++---- internal/muxt/body_wrappers_internal_test.go | 10 +- internal/muxt/call_internal_test.go | 133 +++++++----------- internal/muxt/call_resolve_internal_test.go | 27 ++-- internal/muxt/call_test.go | 4 +- .../muxt/definition_call_internal_test.go | 17 ++- 7 files changed, 97 insertions(+), 146 deletions(-) diff --git a/internal/muxt/argument_bind_internal_test.go b/internal/muxt/argument_bind_internal_test.go index bd776da4..90541315 100644 --- a/internal/muxt/argument_bind_internal_test.go +++ b/internal/muxt/argument_bind_internal_test.go @@ -3,6 +3,8 @@ package muxt import ( "go/types" "testing" + + "github.com/stretchr/testify/assert" ) func TestCheckRepeatedArgument(t *testing.T) { @@ -37,12 +39,11 @@ func TestCheckRepeatedArgument(t *testing.T) { t.Run(tt.name, func(t *testing.T) { tt.first.Identifier, tt.second.Identifier = "id", "id" err := checkRepeatedArgument(&Definition{}, nil, &tt.first, &tt.second) - switch { - case tt.wantErr == "" && err != nil: - t.Errorf("checkRepeatedArgument() = %v, want nil", err) - case tt.wantErr != "" && (err == nil || err.Error() != tt.wantErr): - t.Errorf("checkRepeatedArgument() = %v, want %q", err, tt.wantErr) + if tt.wantErr == "" { + assert.NoError(t, err, "checkRepeatedArgument()") + return } + assert.EqualError(t, err, tt.wantErr, "checkRepeatedArgument()") }) } } diff --git a/internal/muxt/argument_test.go b/internal/muxt/argument_test.go index 2830d175..d894f4b3 100644 --- a/internal/muxt/argument_test.go +++ b/internal/muxt/argument_test.go @@ -3,9 +3,11 @@ package muxt_test import ( "go/types" "html/template" - "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/fake" "github.com/typelate/muxt/internal/muxt" "github.com/typelate/muxt/internal/source" @@ -37,22 +39,13 @@ func TestReservedArgumentBinding(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { defs := resolveTemplate(t, pkg, receiver, checker, tt.template) - if len(defs) == 0 || len(defs[0].Arguments) == 0 { - t.Fatalf("%s resolved no arguments", tt.name) - } + require.NotEmpty(t, defs, "%s resolved no definitions", tt.name) + require.NotEmpty(t, defs[0].Arguments, "%s resolved no arguments", tt.name) arg := defs[0].Arguments[0] - if arg.Type != tt.wantType { - t.Errorf("%s Type = %v, want %v", tt.name, arg.Type, tt.wantType) - } - if arg.Direct() != tt.wantDirect { - t.Errorf("%s Direct() = %t, want %t", tt.name, arg.Direct(), tt.wantDirect) - } - if arg.Declares() != tt.wantDeclares { - t.Errorf("%s Declares() = %t, want %t", tt.name, arg.Declares(), tt.wantDeclares) - } - if got := formatType(arg.ScopeType()); got != tt.wantScope { - t.Errorf("%s ScopeType() = %q, want %q", tt.name, got, tt.wantScope) - } + assert.Equal(t, tt.wantType, arg.Type, "%s Type", tt.name) + assert.Equal(t, tt.wantDirect, arg.Direct(), "%s Direct()", tt.name) + assert.Equal(t, tt.wantDeclares, arg.Declares(), "%s Declares()", tt.name) + assert.Equal(t, tt.wantScope, formatType(arg.ScopeType()), "%s ScopeType()", tt.name) }) } } @@ -73,13 +66,9 @@ func TestReservedArgumentBindingErrors(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { defs, err := muxt.Definitions(source.Variable{Name: "templates", Set: template.Must(template.New("templates").Parse(tt.template))}) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) err = muxt.ResolveCall(&defs[0], source.Package{Fset: fake.FileSet, Types: pkg}, receiver, unbound) - if err == nil || !strings.Contains(err.Error(), tt.wantErr) { - t.Errorf("ResolveCall(%s) error = %v, want it to contain %q", tt.name, err, tt.wantErr) - } + assert.ErrorContains(t, err, tt.wantErr, "ResolveCall(%s)", tt.name) }) } } @@ -87,13 +76,9 @@ func TestReservedArgumentBindingErrors(t *testing.T) { func resolveTemplate(t *testing.T, pkg *types.Package, receiver *types.Named, checker muxt.Checker, text string) []muxt.Definition { t.Helper() defs, err := muxt.Definitions(source.Variable{Name: "templates", Set: template.Must(template.New("templates").Parse(text))}) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) for i := range defs { - if err := muxt.ResolveCall(&defs[i], source.Package{Fset: fake.FileSet, Types: pkg}, receiver, checker); err != nil { - t.Fatal(err) - } + require.NoError(t, muxt.ResolveCall(&defs[i], source.Package{Fset: fake.FileSet, Types: pkg}, receiver, checker)) } return defs } diff --git a/internal/muxt/body_wrappers_internal_test.go b/internal/muxt/body_wrappers_internal_test.go index 5587453b..798630d0 100644 --- a/internal/muxt/body_wrappers_internal_test.go +++ b/internal/muxt/body_wrappers_internal_test.go @@ -1,6 +1,10 @@ package muxt -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestScanBodyBindings(t *testing.T) { for _, tt := range []struct { @@ -21,9 +25,7 @@ func TestScanBodyBindings(t *testing.T) { {name: "nested and direct", call: `F(form, G(multipart, body))`, want: bodyBindings{reads: 1, hasForm: true, hasMultipart: true}}, } { t.Run(tt.name, func(t *testing.T) { - if got := scanBodyBindings(mustParseCall(t, tt.call)); got != tt.want { - t.Errorf("scanBodyBindings(%s) = %+v, want %+v", tt.call, got, tt.want) - } + assert.Equal(t, tt.want, scanBodyBindings(mustParseCall(t, tt.call)), "scanBodyBindings(%s)", tt.call) }) } } diff --git a/internal/muxt/call_internal_test.go b/internal/muxt/call_internal_test.go index 704ae682..08e10cee 100644 --- a/internal/muxt/call_internal_test.go +++ b/internal/muxt/call_internal_test.go @@ -8,6 +8,9 @@ import ( "html/template" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/astgen" "github.com/typelate/muxt/internal/source" ) @@ -15,13 +18,9 @@ import ( func mustParseCall(t *testing.T, src string) *ast.CallExpr { t.Helper() e, err := parser.ParseExpr(src) - if err != nil { - t.Fatalf("ParseExpr(%q) = %v", src, err) - } + require.NoError(t, err, "ParseExpr(%q)", src) call, ok := e.(*ast.CallExpr) - if !ok { - t.Fatalf("ParseExpr(%q) is %T, want *ast.CallExpr", src, e) - } + require.True(t, ok, "ParseExpr(%q) is %T, want *ast.CallExpr", src, e) return call } @@ -46,37 +45,37 @@ func TestCountBodyConsumers(t *testing.T) { {expr: `Outer(Inner(form), body)`, want: 2}, } { t.Run(tt.expr, func(t *testing.T) { - if got := countBodyConsumers(mustParseCall(t, tt.expr)); got != tt.want { - t.Errorf("countBodyConsumers(%q) = %d, want %d", tt.expr, got, tt.want) - } + assert.Equal(t, tt.want, countBodyConsumers(mustParseCall(t, tt.expr)), "countBodyConsumers(%q)", tt.expr) }) } } func TestDefinitionsBodyArgumentErrors(t *testing.T) { + const ( + unmarshalJSONShape = "the unmarshalJSON wrapper requires exactly one argument, the reserved body identifier: unmarshalJSON(body)" + unmarshalFormShape = "the unmarshalForm wrapper requires exactly one argument, the reserved body identifier: unmarshalForm(body)" + consumedTwice = "call Save reads the request body 2 times; the request body is a single-use stream and may be consumed at most once" + nestedExecute = "call Outer argument error: the execute callback must be a direct argument of the route's method call" + ) for _, tt := range []struct { name, template, wantErr string }{ - {name: "unmarshalJSON requires the body identifier", template: `{{define "POST / Save(unmarshalJSON(form))"}}{{end}}`, wantErr: "the unmarshalJSON wrapper requires exactly one argument, the reserved body identifier: unmarshalJSON(body)"}, - {name: "unmarshalJSON requires exactly one argument", template: `{{define "POST / Save(unmarshalJSON(body, ctx))"}}{{end}}`, wantErr: "the unmarshalJSON wrapper requires exactly one argument, the reserved body identifier: unmarshalJSON(body)"}, - {name: "request body may be consumed at most once", template: `{{define "POST / Save(ctx, body, unmarshalJSON(body))"}}{{end}}`, wantErr: "call Save reads the request body 2 times; the request body is a single-use stream and may be consumed at most once"}, - {name: "unmarshalForm requires the body identifier", template: `{{define "POST / Save(unmarshalForm(form))"}}{{end}}`, wantErr: "the unmarshalForm wrapper requires exactly one argument, the reserved body identifier: unmarshalForm(body)"}, - {name: "form parses the request body", template: `{{define "POST / Save(ctx, form, body)"}}{{end}}`, wantErr: "call Save reads the request body 2 times; the request body is a single-use stream and may be consumed at most once"}, - {name: "multipart parses the request body", template: `{{define "POST / Save(ctx, multipart, unmarshalJSON(body))"}}{{end}}`, wantErr: "call Save reads the request body 2 times; the request body is a single-use stream and may be consumed at most once"}, - {name: "execute nested in a call argument", template: `{{define "GET / Outer(Inner(execute))"}}{{end}}`, wantErr: "call Outer argument error: the execute callback must be a direct argument of the route's method call"}, - {name: "execute nested inside a representation wrapper call argument", template: `{{define "GET / marshalJSON(Outer(Inner(execute)))"}}{{end}}`, wantErr: "call Outer argument error: the execute callback must be a direct argument of the route's method call"}, + {name: "unmarshalJSON requires the body identifier", template: `{{define "POST / Save(unmarshalJSON(form))"}}{{end}}`, wantErr: unmarshalJSONShape}, + {name: "unmarshalJSON requires exactly one argument", template: `{{define "POST / Save(unmarshalJSON(body, ctx))"}}{{end}}`, wantErr: unmarshalJSONShape}, + {name: "request body may be consumed at most once", template: `{{define "POST / Save(ctx, body, unmarshalJSON(body))"}}{{end}}`, wantErr: consumedTwice}, + {name: "unmarshalForm requires the body identifier", template: `{{define "POST / Save(unmarshalForm(form))"}}{{end}}`, wantErr: unmarshalFormShape}, + {name: "form parses the request body", template: `{{define "POST / Save(ctx, form, body)"}}{{end}}`, wantErr: consumedTwice}, + {name: "multipart parses the request body", template: `{{define "POST / Save(ctx, multipart, unmarshalJSON(body))"}}{{end}}`, wantErr: consumedTwice}, + {name: "execute nested in a call argument", template: `{{define "GET / Outer(Inner(execute))"}}{{end}}`, wantErr: nestedExecute}, + {name: "execute nested inside a representation wrapper call argument", template: `{{define "GET / marshalJSON(Outer(Inner(execute)))"}}{{end}}`, wantErr: nestedExecute}, {name: "sse callback nested in a call argument", template: `{{define "GET / sse(Outer(Inner(sseClock)))"}}{{end}}`, wantErr: "call Outer argument error: the sseClock callback must be a direct argument of the route's method call"}, {name: "unmarshalForm conflicts with multipart like form does", template: `{{define "POST / Save(unmarshalForm(body), multipart)"}}{{end}}`, wantErr: `call Save has both "form" and "multipart" arguments; use only one (multipart parses url-encoded fields too)`}, } { t.Run(tt.name, func(t *testing.T) { ts := template.Must(template.New("").Parse(tt.template)) _, err := Definitions(source.Variable{Name: "templates", Set: ts}) - if err == nil { - t.Fatalf("Definitions(%q) = nil error, want %q", tt.template, tt.wantErr) - } - if err.Error() != tt.wantErr { - t.Errorf("Definitions(%q) error = %q, want %q", tt.template, err.Error(), tt.wantErr) - } + require.Error(t, err, "Definitions(%q)", tt.template) + assert.Equal(t, tt.wantErr, err.Error(), "Definitions(%q) error", tt.template) }) } } @@ -92,9 +91,7 @@ func TestRewriteBodyFormWrappers(t *testing.T) { t.Run(tt.expr, func(t *testing.T) { call := mustParseCall(t, tt.expr) rewriteBodyFormWrappers(call) - if got := astgen.Format(call); got != tt.want { - t.Errorf("rewriteBodyFormWrappers(%q) = %q, want %q", tt.expr, got, tt.want) - } + assert.Equal(t, tt.want, astgen.Format(call), "rewriteBodyFormWrappers(%q)", tt.expr) }) } } @@ -117,21 +114,13 @@ func TestPeelRepresentationWrapper(t *testing.T) { t.Run(tt.expr, func(t *testing.T) { call := mustParseCall(t, tt.expr) representation, inner, innerFun, ok := peelRepresentationWrapper(call.Fun.(*ast.Ident), call) - if ok != tt.peeled { - t.Fatalf("peelRepresentationWrapper(%q) ok = %t, want %t", tt.expr, ok, tt.peeled) - } + require.Equal(t, tt.peeled, ok, "peelRepresentationWrapper(%q) ok", tt.expr) if !tt.peeled { return } - if representation != tt.representation { - t.Errorf("peelRepresentationWrapper(%q) representation = %q, want %q", tt.expr, representation, tt.representation) - } - if innerFun.Name != tt.fun { - t.Errorf("peelRepresentationWrapper(%q) fun = %q, want %q", tt.expr, innerFun.Name, tt.fun) - } - if inner == nil { - t.Errorf("peelRepresentationWrapper(%q) inner call is nil", tt.expr) - } + assert.Equal(t, tt.representation, representation, "peelRepresentationWrapper(%q) representation", tt.expr) + assert.Equal(t, tt.fun, innerFun.Name, "peelRepresentationWrapper(%q) fun", tt.expr) + assert.NotNil(t, inner, "peelRepresentationWrapper(%q) inner call", tt.expr) }) } } @@ -151,12 +140,8 @@ func TestRewriteSignalsArguments(t *testing.T) { t.Run(tt.expr, func(t *testing.T) { call := mustParseCall(t, tt.expr) rewritten := rewriteSignalsArguments(call, tt.segments) - if rewritten != tt.rewritten { - t.Errorf("rewriteSignalsArguments(%q) = %t, want %t", tt.expr, rewritten, tt.rewritten) - } - if got := astgen.Format(call); got != tt.want { - t.Errorf("rewriteSignalsArguments(%q) rewrote to %q, want %q", tt.expr, got, tt.want) - } + assert.Equal(t, tt.rewritten, rewritten, "rewriteSignalsArguments(%q)", tt.expr) + assert.Equal(t, tt.want, astgen.Format(call), "rewriteSignalsArguments(%q) rewrote to", tt.expr) }) } } @@ -165,53 +150,43 @@ func TestDefinitionsSignals(t *testing.T) { t.Run("signals marks the definition", func(t *testing.T) { ts := template.Must(template.New("").Parse(`{{define "POST /search Save(ctx, signals)"}}{{end}}`)) defs, err := Definitions(source.Variable{Name: "templates", Set: ts}) - if err != nil { - t.Fatal(err) - } - if !defs[0].UsesSignals() { - t.Error("UsesSignals() = false, want true") - } - if got, want := astgen.Format(defs[0].CallExpression()), "Save(ctx, unmarshalJSON(body))"; got != want { - t.Errorf("call = %q, want %q", got, want) - } + require.NoError(t, err) + assert.True(t, defs[0].UsesSignals(), "UsesSignals()") + assert.Equal(t, "Save(ctx, unmarshalJSON(body))", astgen.Format(defs[0].CallExpression()), "call") }) t.Run("a signals path wildcard keeps its path-value meaning", func(t *testing.T) { ts := template.Must(template.New("").Parse(`{{define "GET /s/{signals} Show(ctx, signals)"}}{{end}}`)) defs, err := Definitions(source.Variable{Name: "templates", Set: ts}) - if err != nil { - t.Fatal(err) - } - if defs[0].UsesSignals() { - t.Error("UsesSignals() = true, want false") - } + require.NoError(t, err) + assert.False(t, defs[0].UsesSignals(), "UsesSignals()") }) } func TestIsSignalsCallbackArgument(t *testing.T) { - for name, want := range map[string]bool{ - "countsSignals": true, - "Signals": true, - "signals": false, - "countsSignal": false, - "signalsCounts": false, - "boardStateSignals": true, + for _, tt := range []struct { + name string + want bool + }{ + {name: "countsSignals", want: true}, + {name: "Signals", want: true}, + {name: "signals", want: false}, + {name: "countsSignal", want: false}, + {name: "signalsCounts", want: false}, + {name: "boardStateSignals", want: true}, } { - if got := isSignalsCallbackArgument(name); got != want { - t.Errorf("isSignalsCallbackArgument(%q) = %t, want %t", name, got, want) - } + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, isSignalsCallbackArgument(tt.name), "isSignalsCallbackArgument(%q)", tt.name) + }) } } func TestDefinitionsSignalsCallback(t *testing.T) { ts := template.Must(template.New("").Parse(`{{define "GET /board sse(Stream(ctx, execute, countsSignals))"}}{{end}}`)) defs, err := Definitions(source.Variable{Name: "templates", Set: ts}) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) name, ok := defs[0].SignalsCallback() - if !ok || name != "countsSignals" { - t.Errorf("SignalsCallback() = %q, %t; want %q, true", name, ok, "countsSignals") - } + assert.True(t, ok, "SignalsCallback() ok") + assert.Equal(t, "countsSignals", name, "SignalsCallback() name") } // TestTypeQualifier states how a type is named in a message about a route: @@ -226,10 +201,6 @@ func TestTypeQualifier(t *testing.T) { obj := types.NewTypeName(token.NoPos, pkg, name, nil) return types.NewNamed(obj, types.NewStruct(nil, nil), nil) } - if got := types.TypeString(named(receiverPkg, "Page"), qual); got != "Page" { - t.Errorf("a receiver package type is %q, want %q", got, "Page") - } - if got := types.TypeString(named(otherPkg, "Page"), qual); got != "models.Page" { - t.Errorf("another package's type is %q, want %q", got, "models.Page") - } + assert.Equal(t, "Page", types.TypeString(named(receiverPkg, "Page"), qual), "a receiver package type") + assert.Equal(t, "models.Page", types.TypeString(named(otherPkg, "Page"), qual), "another package's type") } diff --git a/internal/muxt/call_resolve_internal_test.go b/internal/muxt/call_resolve_internal_test.go index bf0d63b6..53babf70 100644 --- a/internal/muxt/call_resolve_internal_test.go +++ b/internal/muxt/call_resolve_internal_test.go @@ -7,6 +7,9 @@ import ( "go/types" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/source" ) @@ -72,26 +75,14 @@ func TestResolveCall(t *testing.T) { def := &Definition{call: call, fun: ast.NewIdent("x")} sig, isMethod, args, err := resolveCall(def, call, source.Package{Types: pkg}, receiver, checker) if tt.wantErr != "" { - if err == nil || err.Error() != tt.wantErr { - t.Fatalf("resolveCall(%s) error = %v, want %q", tt.call, err, tt.wantErr) - } + require.EqualError(t, err, tt.wantErr, "resolveCall(%s)", tt.call) return } - if err != nil { - t.Fatalf("resolveCall(%s) error = %v", tt.call, err) - } - if got := types.TypeString(sig, typeQualifier(pkg)); got != tt.wantSig { - t.Errorf("resolveCall(%s) signature = %s, want %s", tt.call, got, tt.wantSig) - } - if isMethod != tt.wantIsMethod { - t.Errorf("resolveCall(%s) isMethod = %t, want %t", tt.call, isMethod, tt.wantIsMethod) - } - if len(args) != tt.wantArgs { - t.Errorf("resolveCall(%s) returned %d arguments, want %d", tt.call, len(args), tt.wantArgs) - } - if fmt.Sprint(def.synthesizedMethods) != fmt.Sprint(tt.wantSynth) { - t.Errorf("resolveCall(%s) synthesized %v, want %v", tt.call, def.synthesizedMethods, tt.wantSynth) - } + require.NoError(t, err, "resolveCall(%s)", tt.call) + assert.Equal(t, tt.wantSig, types.TypeString(sig, typeQualifier(pkg)), "resolveCall(%s) signature", tt.call) + assert.Equal(t, tt.wantIsMethod, isMethod, "resolveCall(%s) isMethod", tt.call) + assert.Len(t, args, tt.wantArgs, "resolveCall(%s) arguments", tt.call) + assert.Equal(t, fmt.Sprint(tt.wantSynth), fmt.Sprint(def.synthesizedMethods), "resolveCall(%s) synthesized methods", tt.call) }) } } diff --git a/internal/muxt/call_test.go b/internal/muxt/call_test.go index 4d60e7f5..799bdbc1 100644 --- a/internal/muxt/call_test.go +++ b/internal/muxt/call_test.go @@ -505,9 +505,7 @@ func TestArgument(t *testing.T) { t.Run(tc.Name, func(t *testing.T) { ts := template.Must(template.New("").Parse(tc.Template)) defs, err := muxt.Definitions(source.Variable{Name: "templates", Set: ts}) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) for i := range defs { err = muxt.ResolveCall(&defs[i], pkg, tc.Receiver, checker) diff --git a/internal/muxt/definition_call_internal_test.go b/internal/muxt/definition_call_internal_test.go index 9a5edded..3dd42a10 100644 --- a/internal/muxt/definition_call_internal_test.go +++ b/internal/muxt/definition_call_internal_test.go @@ -1,6 +1,10 @@ package muxt -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" +) func TestCallWriteHeader(t *testing.T) { for _, tt := range []struct { @@ -17,12 +21,11 @@ func TestCallWriteHeader(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { def := Definition{call: mustParseCall(t, tt.call)} - if got := def.callWriteHeader(); got != tt.want { - t.Errorf("callWriteHeader(%s) = %t, want %t", tt.call, got, tt.want) - } + assert.Equal(t, tt.want, def.callWriteHeader(), "callWriteHeader(%s)", tt.call) }) } - if !(Definition{}).callWriteHeader() { - t.Error("callWriteHeader() of a definition without a call = false, want true") - } + + t.Run("a definition without a call", func(t *testing.T) { + assert.True(t, (Definition{}).callWriteHeader(), "callWriteHeader() of a definition without a call") + }) } From 0e390a31da55f905286d2b01c86d2bcdd171aa0f Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:32:07 -0700 Subject: [PATCH 16/23] use testify in the definition tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- .../muxt/definition_failures_internal_test.go | 35 +++----- internal/muxt/definition_fuzz_test.go | 15 ++-- internal/muxt/definition_internal_test.go | 30 +++---- internal/muxt/definition_name_fuzz_test.go | 9 +- .../definition_name_helpers_internal_test.go | 89 +++++++++++-------- .../muxt/definition_name_identifier_test.go | 6 +- .../muxt/definition_redirect_internal_test.go | 48 ++++------ 7 files changed, 108 insertions(+), 124 deletions(-) diff --git a/internal/muxt/definition_failures_internal_test.go b/internal/muxt/definition_failures_internal_test.go index 03e242fc..faaf3a3e 100644 --- a/internal/muxt/definition_failures_internal_test.go +++ b/internal/muxt/definition_failures_internal_test.go @@ -4,10 +4,11 @@ import ( "errors" "go/token" "html/template" - "maps" - "slices" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/source" ) @@ -29,9 +30,9 @@ func TestDefinitionBySourceThenName(t *testing.T) { {name: "same", a: at("a.gohtml", 5, "a"), b: at("a.gohtml", 5, "a")}, } { t.Run(tt.name, func(t *testing.T) { - if got := tt.a.bySourceThenName(tt.b); (got > 0) != (tt.wantSign > 0) || (got < 0) != (tt.wantSign < 0) { - t.Errorf("bySourceThenName() = %d, want sign %d", got, tt.wantSign) - } + got := tt.a.bySourceThenName(tt.b) + assert.Equal(t, tt.wantSign > 0, got > 0, "bySourceThenName() = %d, want sign %d", got, tt.wantSign) + assert.Equal(t, tt.wantSign < 0, got < 0, "bySourceThenName() = %d, want sign %d", got, tt.wantSign) }) } } @@ -42,16 +43,12 @@ func TestCombineNameFailuresIsSorted(t *testing.T) { {def: Definition{sourceFile: "a.gohtml", name: "y"}, err: errors.New("first")}, } list, ok := combineNameFailures(failures).(ErrorList) - if !ok { - t.Fatalf("combineNameFailures() is not an ErrorList") - } + require.True(t, ok, "combineNameFailures() is not an ErrorList") var got []string for _, err := range list { got = append(got, err.Error()) } - if want := []string{"first", "second"}; !slices.Equal(got, want) { - t.Errorf("combineNameFailures() = %q, want %q", got, want) - } + assert.Equal(t, []string{"first", "second"}, got, "combineNameFailures()") } func TestCheckForDuplicatePatternsOrdersLocations(t *testing.T) { @@ -73,12 +70,8 @@ func TestCheckForDuplicatePatternsOrdersLocations(t *testing.T) { t.Run(tt.name, func(t *testing.T) { err := CheckForDuplicatePatterns(tt.defs) dup, ok := err.(*DuplicatePatternError) - if !ok { - t.Fatalf("CheckForDuplicatePatterns() = %v, want a *DuplicatePatternError", err) - } - if !slices.Equal(dup.Locations, tt.want) { - t.Errorf("CheckForDuplicatePatterns() locations = %q, want %q", dup.Locations, tt.want) - } + require.True(t, ok, "CheckForDuplicatePatterns() = %v, want a *DuplicatePatternError", err) + assert.Equal(t, tt.want, dup.Locations, "CheckForDuplicatePatterns() locations") }) } } @@ -88,15 +81,11 @@ func TestDefinitionsSourceFile(t *testing.T) { template.Must(ts.New("a.gohtml").Parse(`{{define "GET /a A()"}}x{{end}}`)) ts.New("GET /b B()") // declared, never parsed: no tree defs, err := Definitions(source.Variable{Name: "templates", Set: ts}) - if err != nil { - t.Fatalf("Definitions() error = %v", err) - } + require.NoError(t, err, "Definitions()") got := make(map[string]string) for _, def := range defs { got[def.Name()] = def.SourceFile() } want := map[string]string{"GET /a A()": "a.gohtml", "GET /b B()": ""} - if !maps.Equal(got, want) { - t.Errorf("Definitions() source files = %v, want %v", got, want) - } + assert.Equal(t, want, got, "Definitions() source files") } diff --git a/internal/muxt/definition_fuzz_test.go b/internal/muxt/definition_fuzz_test.go index 78fe9838..e0d29a27 100644 --- a/internal/muxt/definition_fuzz_test.go +++ b/internal/muxt/definition_fuzz_test.go @@ -3,6 +3,8 @@ package muxt import ( "html/template" "testing" + + "github.com/stretchr/testify/require" ) func FuzzNewDefinition(f *testing.F) { @@ -42,9 +44,7 @@ func FuzzNewDefinition(f *testing.F) { return } // Invariants on a successfully parsed definition. - if def.Path() == "" { - t.Fatalf("parsed definition has empty path: %q", name) - } + require.NotEmpty(t, def.Path(), "parsed definition has empty path: %q", name) // Note: status-code range is NOT validated by the parser today — // "/ 00" yields 0 and "/ 700" yields 700. Worth a separate fix; // intentionally not asserted here so the fuzzer keeps hunting @@ -53,15 +53,12 @@ func FuzzNewDefinition(f *testing.F) { // Wildcard segments must name unique path parameters. seen := make(map[string]struct{}) for _, segment := range def.Segments { - if segment.Kind() == SegmentKindUnknown { - t.Fatalf("segment of unknown kind %q in %q", segment.Value(), name) - } + require.NotEqual(t, SegmentKindUnknown, segment.Kind(), "segment of unknown kind %q in %q", segment.Value(), name) if !segment.IsWildcard() { continue } - if _, dup := seen[segment.Value()]; dup { - t.Fatalf("duplicate path value identifier %q in %q", segment.Value(), name) - } + _, dup := seen[segment.Value()] + require.False(t, dup, "duplicate path value identifier %q in %q", segment.Value(), name) seen[segment.Value()] = struct{}{} } }) diff --git a/internal/muxt/definition_internal_test.go b/internal/muxt/definition_internal_test.go index 69b8be34..ad2611ce 100644 --- a/internal/muxt/definition_internal_test.go +++ b/internal/muxt/definition_internal_test.go @@ -700,20 +700,18 @@ func TestNewTemplateName(t *testing.T) { } func TestDefinition_IsIndex(t *testing.T) { - t.Run("not index", func(t *testing.T) { - def := Definition{path: "/foo"} - require.False(t, def.IsIndex()) - }) - t.Run("slash", func(t *testing.T) { - def := Definition{path: "/"} - require.True(t, def.IsIndex()) - }) - t.Run("slash dollar", func(t *testing.T) { - def := Definition{path: "/{$}"} - require.True(t, def.IsIndex()) - }) - t.Run("malformed path", func(t *testing.T) { - def := Definition{path: " / "} - require.True(t, def.IsIndex()) - }) + for _, tt := range []struct { + name string + path string + want bool + }{ + {name: "not index", path: "/foo", want: false}, + {name: "slash", path: "/", want: true}, + {name: "slash dollar", path: "/{$}", want: true}, + {name: "malformed path", path: " / ", want: true}, + } { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, Definition{path: tt.path}.IsIndex(), "IsIndex() of %q", tt.path) + }) + } } diff --git a/internal/muxt/definition_name_fuzz_test.go b/internal/muxt/definition_name_fuzz_test.go index 812fb421..32f20952 100644 --- a/internal/muxt/definition_name_fuzz_test.go +++ b/internal/muxt/definition_name_fuzz_test.go @@ -4,6 +4,8 @@ import ( "go/ast" "strings" "testing" + + "github.com/stretchr/testify/require" ) // FuzzCalculateIdentifiers feeds synthetic Definition slices to @@ -79,10 +81,9 @@ func FuzzCalculateIdentifiers(f *testing.F) { if id == "" { continue } - if prev, ok := seen[id]; ok { - t.Fatalf("duplicate identifier %q at indexes %d and %d\ninput: %q\ndefs: %+v", - id, prev, i, encoded, defs) - } + prev, dup := seen[id] + require.False(t, dup, "duplicate identifier %q at indexes %d and %d\ninput: %q\ndefs: %+v", + id, prev, i, encoded, defs) seen[id] = i } }) diff --git a/internal/muxt/definition_name_helpers_internal_test.go b/internal/muxt/definition_name_helpers_internal_test.go index b5d7ef3d..8944a139 100644 --- a/internal/muxt/definition_name_helpers_internal_test.go +++ b/internal/muxt/definition_name_helpers_internal_test.go @@ -5,6 +5,9 @@ import ( "go/ast" "go/types" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFileNameToPrivateIdentifier(t *testing.T) { @@ -19,9 +22,9 @@ func TestFileNameToPrivateIdentifier(t *testing.T) { {filename: "noextension", want: "noextension"}, {filename: ".gohtml", want: ""}, } { - if got := FileNameToPrivateIdentifier(tt.filename); got != tt.want { - t.Errorf("FileNameToPrivateIdentifier(%q) = %q, want %q", tt.filename, got, tt.want) - } + t.Run(tt.filename, func(t *testing.T) { + assert.Equal(t, tt.want, FileNameToPrivateIdentifier(tt.filename), "FileNameToPrivateIdentifier(%q)", tt.filename) + }) } } @@ -37,9 +40,9 @@ func TestIsRouteDefinitionName(t *testing.T) { {name: "page.gohtml"}, {name: ""}, } { - if got := IsRouteDefinitionName(tt.name); got != tt.want { - t.Errorf("IsRouteDefinitionName(%q) = %t, want %t", tt.name, got, tt.want) - } + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, IsRouteDefinitionName(tt.name), "IsRouteDefinitionName(%q)", tt.name) + }) } } @@ -55,10 +58,15 @@ func TestExportedPathIdentifier(t *testing.T) { {identifier: "_private", wantErr: true}, {identifier: "9lives", wantErr: true}, } { - got, err := Definition{identifier: tt.identifier}.ExportedPathIdentifier() - if (err != nil) != tt.wantErr || got != tt.want { - t.Errorf("ExportedPathIdentifier(%q) = %q, %v, want %q, error %t", tt.identifier, got, err, tt.want, tt.wantErr) - } + t.Run(tt.identifier, func(t *testing.T) { + got, err := Definition{identifier: tt.identifier}.ExportedPathIdentifier() + if tt.wantErr { + assert.Error(t, err, "ExportedPathIdentifier(%q)", tt.identifier) + } else { + assert.NoError(t, err, "ExportedPathIdentifier(%q)", tt.identifier) + } + assert.Equal(t, tt.want, got, "ExportedPathIdentifier(%q)", tt.identifier) + }) } } @@ -88,48 +96,51 @@ b.gohtml:3:4: "List" is defined here`, } { t.Run(tt.name, func(t *testing.T) { err := &MethodNameCollisionError{Method: "List", Handlers: [2]string{"list", "List"}, Locations: tt.locations} - if got := err.MultiLineError(); got != tt.want { - t.Errorf("MultiLineError() = %q, want %q", got, tt.want) - } + assert.Equal(t, tt.want, err.MultiLineError(), "MultiLineError()") }) } } func TestErrorUnwrap(t *testing.T) { cause := errors.New("cause") - if !errors.Is(&NameError{err: cause}, cause) { - t.Error("errors.Is(NameError, cause) = false, want true") - } - if !errors.Is(&positionedError{err: cause}, cause) { - t.Error("errors.Is(positionedError, cause) = false, want true") - } - if !errors.Is(ErrorList{errors.New("other"), cause}, cause) { - t.Error("errors.Is(ErrorList, cause) = false, want true") + for _, tt := range []struct { + name string + err error + }{ + {name: "NameError", err: &NameError{err: cause}}, + {name: "positionedError", err: &positionedError{err: cause}}, + {name: "ErrorList", err: ErrorList{errors.New("other"), cause}}, + } { + t.Run(tt.name, func(t *testing.T) { + assert.ErrorIs(t, tt.err, cause) + }) } } func TestErrAtNode(t *testing.T) { node := ast.NewIdent("x") cause := errors.New("cause") - if got := errAtNode(node, nil); got != nil { - t.Errorf("errAtNode(node, nil) = %v, want nil", got) - } - positioned := errAt(ast.NewIdent("y"), "already") - if got := errAtNode(node, positioned); got != positioned { - t.Errorf("errAtNode(node, positioned) = %v, want the same error", got) - } - nameError := &NameError{err: cause} - if got := errAtNode(node, nameError); got != error(nameError) { - t.Errorf("errAtNode(node, nameError) = %v, want the same error", got) - } - wrapped := errAtNode(node, cause) - if pe, ok := wrapped.(*positionedError); !ok || pe.pos != node.Pos() || !errors.Is(pe, cause) { - t.Errorf("errAtNode(node, cause) = %#v, want a positioned error wrapping cause at the node", wrapped) - } + + t.Run("no error stays nil", func(t *testing.T) { + assert.NoError(t, errAtNode(node, nil), "errAtNode(node, nil)") + }) + t.Run("a positioned error is returned as it is", func(t *testing.T) { + positioned := errAt(ast.NewIdent("y"), "already") + assert.Same(t, positioned, errAtNode(node, positioned), "errAtNode(node, positioned)") + }) + t.Run("a name error is returned as it is", func(t *testing.T) { + nameError := &NameError{err: cause} + assert.Same(t, nameError, errAtNode(node, nameError), "errAtNode(node, nameError)") + }) + t.Run("any other error is positioned at the node", func(t *testing.T) { + wrapped := errAtNode(node, cause) + pe, ok := wrapped.(*positionedError) + require.True(t, ok, "errAtNode(node, cause) = %#v, want a *positionedError", wrapped) + assert.Equal(t, node.Pos(), pe.pos, "errAtNode(node, cause) position") + assert.ErrorIs(t, pe, cause, "errAtNode(node, cause)") + }) } func TestSSECallbackSignature(t *testing.T) { - if got, want := types.TypeString(sseCallbackSignature(), nil), "func(any) error"; got != want { - t.Errorf("sseCallbackSignature() = %s, want %s", got, want) - } + assert.Equal(t, "func(any) error", types.TypeString(sseCallbackSignature(), nil), "sseCallbackSignature()") } diff --git a/internal/muxt/definition_name_identifier_test.go b/internal/muxt/definition_name_identifier_test.go index fcabfae8..e8076422 100644 --- a/internal/muxt/definition_name_identifier_test.go +++ b/internal/muxt/definition_name_identifier_test.go @@ -3,6 +3,8 @@ package muxt import ( "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestGenerateEndpointPatternIdentifierParts(t *testing.T) { @@ -23,9 +25,7 @@ func TestGenerateEndpointPatternIdentifierParts(t *testing.T) { t.Run(tt.name, func(t *testing.T) { var sb strings.Builder sb.WriteString("stale") - if got := tt.def.generateEndpointPatternIdentifier(&sb); got != tt.want { - t.Errorf("generateEndpointPatternIdentifier(%q %q %q) = %q, want %q", tt.def.method, tt.def.host, tt.def.path, got, tt.want) - } + assert.Equal(t, tt.want, tt.def.generateEndpointPatternIdentifier(&sb), "generateEndpointPatternIdentifier(%q %q %q)", tt.def.method, tt.def.host, tt.def.path) }) } } diff --git a/internal/muxt/definition_redirect_internal_test.go b/internal/muxt/definition_redirect_internal_test.go index af267806..ba3a4b36 100644 --- a/internal/muxt/definition_redirect_internal_test.go +++ b/internal/muxt/definition_redirect_internal_test.go @@ -3,6 +3,9 @@ package muxt import ( "html/template" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // pageIn parses text as a set holding "page", plus whatever else the text @@ -10,13 +13,10 @@ import ( func pageIn(t *testing.T, text string) (*template.Template, *template.Template) { t.Helper() ts, err := template.New("set").Parse(`{{define "page"}}` + text + `{{end}}`) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) page := ts.Lookup("page") - if page == nil || page.Tree == nil { - t.Fatalf("the set does not hold a parsed page: %q", text) - } + require.NotNil(t, page, "the set does not hold a page: %q", text) + require.NotNil(t, page.Tree, "the set does not hold a parsed page: %q", text) return ts, page } @@ -39,9 +39,9 @@ func TestWritesResponseState(t *testing.T) { {method: "Header"}, {method: "Name"}, } { - if got := writesResponseState(tt.method); got != tt.want { - t.Errorf("writesResponseState(%q) = %t, want %t", tt.method, got, tt.want) - } + t.Run(tt.method, func(t *testing.T) { + assert.Equal(t, tt.want, writesResponseState(tt.method), "writesResponseState(%q)", tt.method) + }) } } @@ -83,9 +83,8 @@ func TestFindResponseStateCall(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ts, page := pageIn(t, tt.template) got, ok := findResponseStateCall(page.Tree.Root, ts) - if got != tt.want || ok != (tt.want != "") { - t.Errorf("findResponseStateCall(%s) = %q, %t, want %q, %t", tt.template, got, ok, tt.want, tt.want != "") - } + assert.Equal(t, tt.want, got, "findResponseStateCall(%s) method", tt.template) + assert.Equal(t, tt.want != "", ok, "findResponseStateCall(%s) found", tt.template) }) } } @@ -107,14 +106,11 @@ func TestFindResponseStateCallThroughTemplateCalls(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ts, err := template.New("set").Parse( `{{define "page"}}` + tt.call + `{{end}}{{define "part"}}{{.StatusCode 201}}{{end}}`) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) page := ts.Lookup("page") got, ok := findResponseStateCall(page.Tree.Root, ts) - if got != tt.want || ok != (tt.want != "") { - t.Errorf("findResponseStateCall(%s) = %q, %t, want %q, %t", tt.call, got, ok, tt.want, tt.want != "") - } + assert.Equal(t, tt.want, got, "findResponseStateCall(%s) method", tt.call) + assert.Equal(t, tt.want != "", ok, "findResponseStateCall(%s) found", tt.call) }) } } @@ -140,9 +136,7 @@ func TestCanTemplateRedirect(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { ts, page := pageIn(t, tt.template) - if got := canTemplateRedirect(page.Tree.Root, ts, make(map[string]bool)); got != tt.want { - t.Errorf("canTemplateRedirect(%s) = %t, want %t", tt.template, got, tt.want) - } + assert.Equal(t, tt.want, canTemplateRedirect(page.Tree.Root, ts, make(map[string]bool)), "canTemplateRedirect(%s)", tt.template) }) } } @@ -172,13 +166,9 @@ func TestCanTemplateRedirectFollowsTemplateCalls(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { ts, err := template.New("set").Parse(tt.set) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) page := ts.Lookup("page") - if got := canTemplateRedirect(page.Tree.Root, ts, make(map[string]bool)); got != tt.want { - t.Errorf("canTemplateRedirect = %t, want %t", got, tt.want) - } + assert.Equal(t, tt.want, canTemplateRedirect(page.Tree.Root, ts, make(map[string]bool)), "canTemplateRedirect(%s)", tt.set) }) } } @@ -194,8 +184,6 @@ func TestAnalyzeRedirectCalls(t *testing.T) { } analyzeRedirectCalls(ts, defs) for i, want := range []bool{true, false, false, false} { - if defs[i].canRedirect != want { - t.Errorf("analyzeRedirectCalls() set canRedirect of %q to %t, want %t", defs[i].name, defs[i].canRedirect, want) - } + assert.Equal(t, want, defs[i].canRedirect, "analyzeRedirectCalls() canRedirect of %q", defs[i].name) } } From f7fd1d6c3752928fe4ba07666457d738c07778bd Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:33:47 -0700 Subject: [PATCH 17/23] use testify in the mutant, redirect walk and response state tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/muxt/mutants_internal_test.go | 107 ++++++++---------- .../muxt/redirect_helpers_internal_test.go | 33 +++--- internal/muxt/redirect_walk_internal_test.go | 21 ++-- internal/muxt/response_state_internal_test.go | 65 +++++------ 4 files changed, 96 insertions(+), 130 deletions(-) diff --git a/internal/muxt/mutants_internal_test.go b/internal/muxt/mutants_internal_test.go index 5b3e235c..6db5dbbc 100644 --- a/internal/muxt/mutants_internal_test.go +++ b/internal/muxt/mutants_internal_test.go @@ -10,6 +10,9 @@ import ( "slices" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/source" ) @@ -33,12 +36,8 @@ func TestDefinitionRepresentationPredicates(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { def := Definition{Representation: tt.representation, Segments: tt.segments} - if got := def.isSignalsCallback(tt.argument); got != tt.signals { - t.Errorf("isSignalsCallback(%q) = %t, want %t", tt.argument, got, tt.signals) - } - if got := def.isSendMessage(tt.argument); got != tt.message { - t.Errorf("isSendMessage(%q) = %t, want %t", tt.argument, got, tt.message) - } + assert.Equal(t, tt.signals, def.isSignalsCallback(tt.argument), "isSignalsCallback(%q)", tt.argument) + assert.Equal(t, tt.message, def.isSendMessage(tt.argument), "isSendMessage(%q)", tt.argument) }) } } @@ -72,12 +71,13 @@ func (Server) NotFunction(int) {} param := method.Type().(*types.Signature).Params().At(0).Type() arg := &Argument{Identifier: "countsSignals", Type: ArgumentTypeSignalsCallback, paramType: param} err := resolveSignalsCallback(&Definition{}, arg) - if (err != nil) != tt.wantErr { - t.Fatalf("resolveSignalsCallback(%s) error = %v, want error %t", tt.method, err, tt.wantErr) - } - if !tt.wantErr && (!arg.callbackHasArg || types.TypeString(arg.callbackResult, nil) != "int") { - t.Errorf("resolveSignalsCallback(%s) recorded %v, %t, want int, true", tt.method, arg.callbackResult, arg.callbackHasArg) + if tt.wantErr { + require.Error(t, err, "resolveSignalsCallback(%s)", tt.method) + return } + require.NoError(t, err, "resolveSignalsCallback(%s)", tt.method) + assert.True(t, arg.callbackHasArg, "resolveSignalsCallback(%s) callbackHasArg", tt.method) + assert.Equal(t, "int", types.TypeString(arg.callbackResult, nil), "resolveSignalsCallback(%s) callbackResult", tt.method) }) } } @@ -85,9 +85,7 @@ func (Server) NotFunction(int) {} func TestDefinedHere(t *testing.T) { fset := token.NewFileSet() file, err := parser.ParseFile(fset, "server.go", "package p\n\nfunc Handler() {}\n", 0) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) decl := file.Decls[0].(*ast.FuncDecl) located := types.NewFunc(decl.Name.Pos(), nil, "Handler", nil) unlocated := types.NewFunc(token.NoPos, nil, "Handler", nil) @@ -104,9 +102,7 @@ func TestDefinedHere(t *testing.T) { {name: "position outside the file set", fset: token.NewFileSet(), object: located}, } { t.Run(tt.name, func(t *testing.T) { - if got := definedHere(source.Package{Fset: tt.fset}, tt.object); got != tt.want { - t.Errorf("definedHere() = %q, want %q", got, tt.want) - } + assert.Equal(t, tt.want, definedHere(source.Package{Fset: tt.fset}, tt.object), "definedHere()") }) } } @@ -129,18 +125,14 @@ func TestCheckPathAndMethod(t *testing.T) { t.Run(tt.name, func(t *testing.T) { err := tt.def.checkPathAndMethod() if tt.wantErr == "" { - if err != nil { - t.Fatalf("checkPathAndMethod() = %v, want nil", err) - } + require.NoError(t, err, "checkPathAndMethod()") return } nameError, ok := errors.AsType[*NameError](err) - if !ok { - t.Fatalf("checkPathAndMethod() = %v, want a *NameError", err) - } - if nameError.Unwrap().Error() != tt.wantErr || nameError.Offset != tt.wantOffset || nameError.Length != tt.wantLength { - t.Errorf("checkPathAndMethod() = %q at %d+%d, want %q at %d+%d", nameError.Unwrap(), nameError.Offset, nameError.Length, tt.wantErr, tt.wantOffset, tt.wantLength) - } + require.True(t, ok, "checkPathAndMethod() = %v, want a *NameError", err) + assert.EqualError(t, nameError.Unwrap(), tt.wantErr, "checkPathAndMethod() message") + assert.Equal(t, tt.wantOffset, nameError.Offset, "checkPathAndMethod() offset") + assert.Equal(t, tt.wantLength, nameError.Length, "checkPathAndMethod() length") }) } } @@ -161,15 +153,13 @@ func TestNewDefinitionStatusCodeAndResponse(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { def, err, matched := newDefinition(template.Must(template.New(tt.route).Parse(``))) - if !matched { - t.Fatalf("newDefinition(%q) did not match", tt.route) - } - if (err != nil) != tt.wantErr { - t.Fatalf("newDefinition(%q) error = %v, want error %t", tt.route, err, tt.wantErr) - } - if !tt.wantErr && def.defaultStatusCode != tt.want { - t.Errorf("newDefinition(%q) status = %d, want %d", tt.route, def.defaultStatusCode, tt.want) + require.True(t, matched, "newDefinition(%q) did not match", tt.route) + if tt.wantErr { + require.Error(t, err, "newDefinition(%q)", tt.route) + return } + require.NoError(t, err, "newDefinition(%q)", tt.route) + assert.Equal(t, tt.want, def.defaultStatusCode, "newDefinition(%q) status", tt.route) }) } } @@ -190,10 +180,11 @@ func TestWildcardName(t *testing.T) { {segment: "users", wantName: "users"}, {segment: "", wantName: ""}, } { - name, isWildcard := wildcardName(tt.segment) - if name != tt.wantName || isWildcard != tt.wantWildcard { - t.Errorf("wildcardName(%q) = (%q, %t), want (%q, %t)", tt.segment, name, isWildcard, tt.wantName, tt.wantWildcard) - } + t.Run(tt.segment, func(t *testing.T) { + name, isWildcard := wildcardName(tt.segment) + assert.Equal(t, tt.wantName, name, "wildcardName(%q) name", tt.segment) + assert.Equal(t, tt.wantWildcard, isWildcard, "wildcardName(%q) isWildcard", tt.segment) + }) } } @@ -211,23 +202,22 @@ func TestExecuteArgumentIndex(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { index, ok := Definition{Arguments: tt.arguments}.ExecuteArgumentIndex() - if index != tt.wantIndex || ok != tt.wantOK { - t.Errorf("ExecuteArgumentIndex() = (%d, %t), want (%d, %t)", index, ok, tt.wantIndex, tt.wantOK) - } + assert.Equal(t, tt.wantIndex, index, "ExecuteArgumentIndex() index") + assert.Equal(t, tt.wantOK, ok, "ExecuteArgumentIndex() ok") }) } } func TestTemplateNames(t *testing.T) { - if got := templateNames(nil); got != nil { - t.Errorf("templateNames(nil) = %v, want nil", got) - } - ts := template.Must(template.New("root").Parse(`{{define "a"}}{{end}}{{define "b"}}{{end}}`)) - got := templateNames(ts) - slices.Sort(got) - if want := []string{"a", "b", "root"}; !slices.Equal(got, want) { - t.Errorf("templateNames() = %v, want %v", got, want) - } + t.Run("no set", func(t *testing.T) { + assert.Nil(t, templateNames(nil), "templateNames(nil)") + }) + t.Run("every template in the set", func(t *testing.T) { + ts := template.Must(template.New("root").Parse(`{{define "a"}}{{end}}{{define "b"}}{{end}}`)) + got := templateNames(ts) + slices.Sort(got) + assert.Equal(t, []string{"a", "b", "root"}, got, "templateNames()") + }) } func TestResolveCallNotesOnlyTheRouteCall(t *testing.T) { @@ -242,13 +232,9 @@ func Function(Context) any { return nil } ` fset := token.NewFileSet() file, err := parser.ParseFile(fset, "p.go", src, parser.SkipObjectResolution) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) pkg, err := new(types.Config).Check("example.com/p", fset, []*ast.File{file}, nil) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) checker := scopeChecker{TemplateNameScopeIdentifierContext: pkg.Scope().Lookup("Context").Type()} server := pkg.Scope().Lookup("Server").Type().(*types.Named) for _, tt := range []struct { @@ -264,12 +250,9 @@ func Function(Context) any { return nil } t.Run(tt.name, func(t *testing.T) { call := mustParseCall(t, tt.call) def := &Definition{call: call} - if _, _, _, err := resolveCall(def, call, source.Package{Types: pkg, Fset: fset}, server, checker); err != nil { - t.Fatalf("resolveCall(%s) error = %v", tt.call, err) - } - if !slices.Equal(def.related, tt.want) { - t.Errorf("resolveCall(%s) related = %q, want %q", tt.call, def.related, tt.want) - } + _, _, _, err := resolveCall(def, call, source.Package{Types: pkg, Fset: fset}, server, checker) + require.NoError(t, err, "resolveCall(%s)", tt.call) + assert.Equal(t, tt.want, def.related, "resolveCall(%s) related", tt.call) }) } } diff --git a/internal/muxt/redirect_helpers_internal_test.go b/internal/muxt/redirect_helpers_internal_test.go index 8d39423e..fe1debaa 100644 --- a/internal/muxt/redirect_helpers_internal_test.go +++ b/internal/muxt/redirect_helpers_internal_test.go @@ -3,15 +3,16 @@ package muxt import ( "testing" "text/template/parse" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func firstCommand(t *testing.T, text string) *parse.CommandNode { t.Helper() _, page := pageIn(t, text) action, ok := page.Tree.Root.Nodes[0].(*parse.ActionNode) - if !ok { - t.Fatalf("%q does not start with an action", text) - } + require.True(t, ok, "%q does not start with an action", text) return action.Pipe.Cmds[0] } @@ -29,14 +30,13 @@ func TestContainsRedirectCall(t *testing.T) { {template: `{{"literal"}}`}, } { t.Run(tt.template, func(t *testing.T) { - if got := containsRedirectCall(firstCommand(t, tt.template)); got != tt.want { - t.Errorf("containsRedirectCall(%s) = %t, want %t", tt.template, got, tt.want) - } + assert.Equal(t, tt.want, containsRedirectCall(firstCommand(t, tt.template)), "containsRedirectCall(%s)", tt.template) }) } - if containsRedirectCall(nil) { - t.Error("containsRedirectCall(nil) = true, want false") - } + + t.Run("no command", func(t *testing.T) { + assert.False(t, containsRedirectCall(nil), "containsRedirectCall(nil)") + }) } func TestCallsMethodOnTemplateData(t *testing.T) { @@ -57,14 +57,13 @@ func TestCallsMethodOnTemplateData(t *testing.T) { {name: "literal", template: `{{"x"}}`}, } { t.Run(tt.name, func(t *testing.T) { - if got := callsMethodOnTemplateData(firstCommand(t, tt.template)); got != tt.want { - t.Errorf("callsMethodOnTemplateData(%s) = %t, want %t", tt.template, got, tt.want) - } + assert.Equal(t, tt.want, callsMethodOnTemplateData(firstCommand(t, tt.template)), "callsMethodOnTemplateData(%s)", tt.template) }) } - if callsMethodOnTemplateData(nil) { - t.Error("callsMethodOnTemplateData(nil) = true, want false") - } + + t.Run("no command", func(t *testing.T) { + assert.False(t, callsMethodOnTemplateData(nil), "callsMethodOnTemplateData(nil)") + }) } func TestChainStartsAtTemplateData(t *testing.T) { @@ -85,9 +84,7 @@ func TestChainStartsAtTemplateData(t *testing.T) { {name: "literal", node: &parse.StringNode{}, dot: true}, } { t.Run(tt.name, func(t *testing.T) { - if got := chainStartsAtTemplateData(&parse.ChainNode{Node: tt.node}, tt.dot); got != tt.want { - t.Errorf("chainStartsAtTemplateData(%s, dot=%t) = %t, want %t", tt.name, tt.dot, got, tt.want) - } + assert.Equal(t, tt.want, chainStartsAtTemplateData(&parse.ChainNode{Node: tt.node}, tt.dot), "chainStartsAtTemplateData(%s, dot=%t)", tt.name, tt.dot) }) } } diff --git a/internal/muxt/redirect_walk_internal_test.go b/internal/muxt/redirect_walk_internal_test.go index 7adb532a..fb49421c 100644 --- a/internal/muxt/redirect_walk_internal_test.go +++ b/internal/muxt/redirect_walk_internal_test.go @@ -1,9 +1,11 @@ package muxt import ( - "slices" "testing" "text/template/parse" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestWalkTemplateCommands(t *testing.T) { @@ -28,17 +30,14 @@ func TestWalkTemplateCommands(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { ts, page := pageIn(t, tt.template) - if _, err := ts.Parse(`{{define "inner"}}{{.Inner}}{{end}}`); err != nil { - t.Fatal(err) - } + _, err := ts.Parse(`{{define "inner"}}{{.Inner}}{{end}}`) + require.NoError(t, err) var got []string walkTemplateCommands(page.Tree.Root, ts, make(map[string]bool), true, func(cmd *parse.CommandNode, dot bool) bool { got = append(got, cmd.String()+" "+boolWord(dot)) return false }) - if !slices.Equal(got, tt.want) { - t.Errorf("walkTemplateCommands(%s) visited %q, want %q", tt.template, got, tt.want) - } + assert.Equal(t, tt.want, got, "walkTemplateCommands(%s) visited", tt.template) }) } } @@ -50,12 +49,8 @@ func TestWalkTemplateCommandsStopsAtTheFirstAccepted(t *testing.T) { visited = append(visited, cmd.String()) return cmd.String() == ".C" }) - if !found { - t.Error("walkTemplateCommands did not report the accepted command") - } - if want := []string{".A", ".B", ".C"}; !slices.Equal(visited, want) { - t.Errorf("walkTemplateCommands visited %q, want %q", visited, want) - } + assert.True(t, found, "walkTemplateCommands reported the accepted command") + assert.Equal(t, []string{".A", ".B", ".C"}, visited, "walkTemplateCommands visited") } func boolWord(b bool) string { diff --git a/internal/muxt/response_state_internal_test.go b/internal/muxt/response_state_internal_test.go index bca381d5..3af2cd02 100644 --- a/internal/muxt/response_state_internal_test.go +++ b/internal/muxt/response_state_internal_test.go @@ -3,9 +3,11 @@ package muxt import ( "go/ast" "html/template" - "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/source" ) @@ -27,18 +29,13 @@ func TestCheckResponseWriterConflicts(t *testing.T) { def := Definition{name: "GET /a A(response)", handler: "A(response)", hasResponseWriterArg: true} err := checkResponseWriterConflicts(ts, []Definition{def}) if tt.want == "" { - if err != nil { - t.Fatalf("checkResponseWriterConflicts() = %v, want nil", err) - } + require.NoError(t, err, "checkResponseWriterConflicts()") return } conflict, ok := err.(*ResponseWriterTemplateStateError) - if !ok { - t.Fatalf("checkResponseWriterConflicts() = %v, want a *ResponseWriterTemplateStateError", err) - } - if conflict.Method != tt.want || conflict.Template != def.name { - t.Errorf("checkResponseWriterConflicts() = %+v, want method %s of template %s", conflict, tt.want, def.name) - } + require.True(t, ok, "checkResponseWriterConflicts() = %v, want a *ResponseWriterTemplateStateError", err) + assert.Equal(t, tt.want, conflict.Method, "conflict method") + assert.Equal(t, def.name, conflict.Template, "conflict template") }) } } @@ -56,9 +53,7 @@ func TestCheckResponseWriterConflictsSkips(t *testing.T) { {name: "no tree", def: Definition{name: "GET /b B(response)", hasResponseWriterArg: true}}, } { t.Run(tt.name, func(t *testing.T) { - if err := checkResponseWriterConflicts(ts, []Definition{tt.def}); err != nil { - t.Errorf("checkResponseWriterConflicts() = %v, want nil", err) - } + assert.NoError(t, checkResponseWriterConflicts(ts, []Definition{tt.def}), "checkResponseWriterConflicts()") }) } } @@ -76,39 +71,35 @@ func TestCheckResponseWriterConflictsNamesTheFunction(t *testing.T) { t.Run(tt.name, func(t *testing.T) { def := Definition{name: "GET /a save(response)", handler: "save(response)", fun: tt.fun, hasResponseWriterArg: true} conflict, ok := checkResponseWriterConflicts(ts, []Definition{def}).(*ResponseWriterTemplateStateError) - if !ok { - t.Fatal("checkResponseWriterConflicts() is not a *ResponseWriterTemplateStateError") - } - if conflict.Function != tt.want { - t.Errorf("Function = %q, want %q", conflict.Function, tt.want) - } + require.True(t, ok, "checkResponseWriterConflicts() is not a *ResponseWriterTemplateStateError") + assert.Equal(t, tt.want, conflict.Function, "Function") }) } } func TestResponseWriterTemplateStateErrorMessage(t *testing.T) { - e := &ResponseWriterTemplateStateError{Template: "GET /a A(response)", Method: "StatusCode", Function: "A"} const want = `template "GET /a A(response)" calls StatusCode but A takes the http.ResponseWriter, so muxt writes no status code or redirect for this route: either drop the response argument or call response.WriteHeader in the method` - if got := e.Error(); got != want { - t.Errorf("Error() = %q, want %q", got, want) - } - e.Location = "a.gohtml:1:2" - if got := e.Error(); got != "a.gohtml:1:2: "+want { - t.Errorf("Error() with a location = %q, want it prefixed with the location", got) - } - e.Method = "Redirect" - if got := e.Error(); !strings.Contains(got, "or call http.Redirect in the method") { - t.Errorf("Error() for a redirect = %q, want the http.Redirect remedy", got) - } + e := &ResponseWriterTemplateStateError{Template: "GET /a A(response)", Method: "StatusCode", Function: "A"} + + t.Run("without a location", func(t *testing.T) { + assert.Equal(t, want, e.Error(), "Error()") + }) + t.Run("with a location", func(t *testing.T) { + located := *e + located.Location = "a.gohtml:1:2" + assert.Equal(t, "a.gohtml:1:2: "+want, located.Error(), "Error() with a location") + }) + t.Run("for a redirect", func(t *testing.T) { + redirect := *e + redirect.Method = "Redirect" + assert.Contains(t, redirect.Error(), "or call http.Redirect in the method", "Error() for a redirect") + }) } func TestDefinitionsReportsResponseWriterConflicts(t *testing.T) { ts := template.Must(template.New("root").Parse(`{{define "GET /a A(response)"}}{{.StatusCode}}{{end}}`)) defs, err := Definitions(source.Variable{Name: "templates", Set: ts}) - if _, ok := err.(*ResponseWriterTemplateStateError); !ok { - t.Fatalf("Definitions() error = %v, want a *ResponseWriterTemplateStateError", err) - } - if len(defs) != 1 { - t.Errorf("Definitions() returned %d definitions with the error, want 1", len(defs)) - } + _, ok := err.(*ResponseWriterTemplateStateError) + require.True(t, ok, "Definitions() error = %v, want a *ResponseWriterTemplateStateError", err) + assert.Len(t, defs, 1, "Definitions() definitions returned with the error") } From 0378b4385919eb239967b6f426ca3483bfca900a Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:35:10 -0700 Subject: [PATCH 18/23] use testify in the result shape, segment, unmarshal and validation tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/muxt/result_shape_test.go | 48 +++++++++++------------- internal/muxt/segment_test.go | 41 ++++++-------------- internal/muxt/unmarshal_internal_test.go | 6 +-- internal/muxt/validation_test.go | 22 ++++------- 4 files changed, 43 insertions(+), 74 deletions(-) diff --git a/internal/muxt/result_shape_test.go b/internal/muxt/result_shape_test.go index 02be6326..bff55974 100644 --- a/internal/muxt/result_shape_test.go +++ b/internal/muxt/result_shape_test.go @@ -6,28 +6,36 @@ import ( "go/token" "go/types" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func checkSource(t *testing.T, src string) *types.Package { t.Helper() fset := token.NewFileSet() file, err := parser.ParseFile(fset, "p.go", src, parser.SkipObjectResolution) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) pkg, err := new(types.Config).Check("example.com/p", fset, []*ast.File{file}, nil) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) return pkg } +// assertErrorMessage asserts err is nil when wantErr is empty and carries +// exactly the message wantErr otherwise. +func assertErrorMessage(t *testing.T, wantErr string, err error, msgAndArgs ...any) { + t.Helper() + if wantErr == "" { + assert.NoError(t, err, msgAndArgs...) + return + } + assert.EqualError(t, err, wantErr, msgAndArgs...) +} + func funcSignature(t *testing.T, pkg *types.Package, name string) *types.Signature { t.Helper() obj := pkg.Scope().Lookup(name) - if obj == nil { - t.Fatalf("package declares no %s", name) - } + require.NotNil(t, obj, "package declares no %s", name) return obj.Type().(*types.Signature) } @@ -99,15 +107,8 @@ func TestClassifyResultShape(t *testing.T) { Arguments: tt.arguments, } got, err := classifyResultShape(def, typeQualifier(pkg)) - if got != tt.want { - t.Errorf("classifyResultShape(%s) = %v, want %v", tt.fn, got, tt.want) - } - switch { - case tt.wantErr == "" && err != nil: - t.Errorf("classifyResultShape(%s) error = %v, want none", tt.fn, err) - case tt.wantErr != "" && (err == nil || err.Error() != tt.wantErr): - t.Errorf("classifyResultShape(%s) error = %v, want %q", tt.fn, err, tt.wantErr) - } + assert.Equal(t, tt.want, got, "classifyResultShape(%s)", tt.fn) + assertErrorMessage(t, tt.wantErr, err, "classifyResultShape(%s)", tt.fn) }) } } @@ -128,15 +129,8 @@ func TestClassifyNestedCallResultShape(t *testing.T) { } { t.Run(tt.fn, func(t *testing.T) { got, err := classifyNestedCallResultShape(tt.fn, funcSignature(t, pkg, tt.fn), typeQualifier(pkg)) - if got != tt.want { - t.Errorf("classifyNestedCallResultShape(%s) = %v, want %v", tt.fn, got, tt.want) - } - switch { - case tt.wantErr == "" && err != nil: - t.Errorf("classifyNestedCallResultShape(%s) error = %v, want none", tt.fn, err) - case tt.wantErr != "" && (err == nil || err.Error() != tt.wantErr): - t.Errorf("classifyNestedCallResultShape(%s) error = %v, want %q", tt.fn, err, tt.wantErr) - } + assert.Equal(t, tt.want, got, "classifyNestedCallResultShape(%s)", tt.fn) + assertErrorMessage(t, tt.wantErr, err, "classifyNestedCallResultShape(%s)", tt.fn) }) } } diff --git a/internal/muxt/segment_test.go b/internal/muxt/segment_test.go index 0050eb8b..d0f1cc71 100644 --- a/internal/muxt/segment_test.go +++ b/internal/muxt/segment_test.go @@ -7,6 +7,7 @@ import ( "html/template" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/typelate/muxt/internal/fake" @@ -151,9 +152,7 @@ func TestPathValueTypes(t *testing.T) { require.ErrorContains(t, err, tt.wantErr) return } - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) segment, ok := segmentByName(def.Segments, tt.param) require.True(t, ok, "path parameter %q not found", tt.param) @@ -184,18 +183,12 @@ func TestPathValueParsing(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ts := template.Must(template.New("").Parse(`{{define "` + tt.template + `"}}{{end}}`)) defs, err := muxt.Definitions(source.Variable{Name: "templates", Set: ts}) - if err != nil { - t.Fatal(err) - } - if err := muxt.ResolveCall(&defs[0], source.Package{Fset: fake.FileSet, Types: pkg}, receiver, checker); err != nil { - t.Fatal(err) - } + require.NoError(t, err) + require.NoError(t, muxt.ResolveCall(&defs[0], source.Package{Fset: fake.FileSet, Types: pkg}, receiver, checker)) argument := defs[0].Arguments[0] - if got := argument.Direct(); got != tt.wantDirect { - t.Errorf("Direct() = %t, want %t", got, tt.wantDirect) - } - if got := argument.UnmarshalMethod(); !tt.wantDirect && got != tt.wantMethod { - t.Errorf("UnmarshalMethod() = %v, want %v", got, tt.wantMethod) + assert.Equal(t, tt.wantDirect, argument.Direct(), "Direct()") + if !tt.wantDirect { + assert.Equal(t, tt.wantMethod, argument.UnmarshalMethod(), "UnmarshalMethod()") } }) } @@ -220,22 +213,12 @@ func TestPathValueTextMarshaler(t *testing.T) { t.Run(tt.template, func(t *testing.T) { ts := template.Must(template.New("").Parse(`{{define "` + tt.template + `"}}{{end}}`)) defs, err := muxt.Definitions(source.Variable{Name: "templates", Set: ts}) - if err != nil { - t.Fatal(err) - } - if err := muxt.ResolveCall(&defs[0], source.Package{Fset: fake.FileSet, Types: pkg}, receiver, checker); err != nil { - t.Fatal(err) - } + require.NoError(t, err) + require.NoError(t, muxt.ResolveCall(&defs[0], source.Package{Fset: fake.FileSet, Types: pkg}, receiver, checker)) segment, ok := segmentByName(defs[0].Segments, tt.param) - if !ok { - t.Fatalf("path parameter %q not found", tt.param) - } - if got := pathParameterTextMarshaler(segment); got != tt.want { - t.Errorf("path parameter %q marshals as text = %t, want %t", tt.param, got, tt.want) - } - if got := defs[0].Arguments[0].TextMarshaler(); got != tt.want { - t.Errorf("Arguments[0].TextMarshaler() = %t, want %t", got, tt.want) - } + require.True(t, ok, "path parameter %q not found", tt.param) + assert.Equal(t, tt.want, pathParameterTextMarshaler(segment), "path parameter %q marshals as text", tt.param) + assert.Equal(t, tt.want, defs[0].Arguments[0].TextMarshaler(), "Arguments[0].TextMarshaler()") }) } } diff --git a/internal/muxt/unmarshal_internal_test.go b/internal/muxt/unmarshal_internal_test.go index 96eea54b..624c06ab 100644 --- a/internal/muxt/unmarshal_internal_test.go +++ b/internal/muxt/unmarshal_internal_test.go @@ -3,6 +3,8 @@ package muxt import ( "go/types" "testing" + + "github.com/stretchr/testify/assert" ) type textUnmarshalerChecker struct { @@ -70,9 +72,7 @@ var ( {name: "pointer", tp: lookup("Pointer"), want: UnmarshalUnsupported}, } { t.Run(tt.name, func(t *testing.T) { - if got := unmarshalMethodFor(checker, tt.tp); got != tt.want { - t.Errorf("unmarshalMethodFor(%s) = %d, want %d", tt.tp, got, tt.want) - } + assert.Equal(t, tt.want, unmarshalMethodFor(checker, tt.tp), "unmarshalMethodFor(%s)", tt.tp) }) } } diff --git a/internal/muxt/validation_test.go b/internal/muxt/validation_test.go index 22adb132..818d8584 100644 --- a/internal/muxt/validation_test.go +++ b/internal/muxt/validation_test.go @@ -6,6 +6,8 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/typelate/dom" "github.com/typelate/dom/spec" "golang.org/x/net/html" @@ -21,13 +23,9 @@ func inputElement(t *testing.T, markup string) spec.Element { DataAtom: atom.Body, Data: atom.Body.String(), }) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) element := dom.NewDocumentFragment(nodes).QuerySelector("[name=field]") - if element == nil { - t.Fatalf("%q has no element named field", markup) - } + require.NotNil(t, element, "%q has no element named field", markup) return element } @@ -77,17 +75,11 @@ func TestParseInputValidations(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := muxt.ParseInputValidations("field", inputElement(t, tt.markup), tt.tp) if tt.wantErr != "" { - if err == nil || !strings.Contains(err.Error(), tt.wantErr) { - t.Fatalf("ParseInputValidations(%s) error = %v, want it to contain %q", tt.markup, err, tt.wantErr) - } + require.ErrorContains(t, err, tt.wantErr, "ParseInputValidations(%s)", tt.markup) return } - if err != nil { - t.Fatalf("ParseInputValidations(%s) error = %v", tt.markup, err) - } - if described := describeValidations(got); strings.Join(described, "; ") != strings.Join(tt.want, "; ") { - t.Errorf("ParseInputValidations(%s) = %q, want %q", tt.markup, described, tt.want) - } + require.NoError(t, err, "ParseInputValidations(%s)", tt.markup) + assert.Equal(t, tt.want, describeValidations(got), "ParseInputValidations(%s)", tt.markup) }) } } From 127f8cbf440c966edac6d5c5a3a1250423cff962 Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:42:06 -0700 Subject: [PATCH 19/23] use testify in the fakeserver tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/fakeserver/fakeserver_test.go | 110 ++++++++++++------------- 1 file changed, 52 insertions(+), 58 deletions(-) diff --git a/internal/fakeserver/fakeserver_test.go b/internal/fakeserver/fakeserver_test.go index 402f982e..992a483c 100644 --- a/internal/fakeserver/fakeserver_test.go +++ b/internal/fakeserver/fakeserver_test.go @@ -6,6 +6,8 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/tools/go/packages" "github.com/typelate/muxt/internal/load" @@ -30,14 +32,11 @@ func TestLibraryPackage(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := libraryPackage(pl, tt.path) if tt.wantErr != "" { - if err == nil || err.Error() != tt.wantErr { - t.Fatalf("libraryPackage(%q) error = %v, want %q", tt.path, err, tt.wantErr) - } + require.EqualError(t, err, tt.wantErr, "libraryPackage(%q)", tt.path) return } - if err != nil || got != tt.want { - t.Fatalf("libraryPackage(%q) = %v, %v, want %v", tt.path, got, err, tt.want) - } + require.NoError(t, err, "libraryPackage(%q)", tt.path) + require.Same(t, tt.want, got, "libraryPackage(%q)", tt.path) }) } } @@ -108,18 +107,12 @@ func TestRenderMain(t *testing.T) { tt.change(&c) } got, err := renderMain(c, tt.packageName) - if err != nil { - t.Fatalf("renderMain() error = %v", err) - } + require.NoError(t, err, "renderMain()") for _, want := range tt.want { - if !strings.Contains(string(got), want) { - t.Errorf("renderMain() = %s\nwant containing %q", got, want) - } + assert.Contains(t, string(got), want, "renderMain() want containing %q", want) } for _, notWant := range tt.notWant { - if strings.Contains(string(got), notWant) { - t.Errorf("renderMain() = %s\nwant without %q", got, notWant) - } + assert.NotContains(t, string(got), notWant, "renderMain() want without %q", notWant) } }) } @@ -127,25 +120,31 @@ func TestRenderMain(t *testing.T) { func TestRenderMainRejectsAnInvalidImportPath(t *testing.T) { _, err := renderMain(Config{RoutesFunction: "Routes", ReceiverInterface: "Receiver", PackagePath: "not valid \n"}, "routes") - if err == nil || !strings.HasPrefix(err.Error(), "formatting main.go: ") { - t.Fatalf("renderMain() error = %v, want a formatting error", err) - } + require.Error(t, err, "renderMain()") + require.True(t, strings.HasPrefix(err.Error(), "formatting main.go: "), "renderMain() error = %v, want a formatting error", err) } func TestPreloadedCache(t *testing.T) { pl := []*packages.Package{{PkgPath: "example.com/a"}} cache := &preloadedCache{pkgPath: "example.com/a", packages: pl} - if got, ok := cache.Load("example.com/a"); !ok || len(got) != 1 { - t.Errorf("Load(its package) = %v, %v, want the loaded packages", got, ok) - } - if got, ok := cache.Load("example.com/b"); ok || got != nil { - t.Errorf("Load(another package) = %v, %v, want nothing", got, ok) - } - cache.Store("example.com/b", nil) - if _, ok := cache.Load("example.com/b"); ok { - t.Error("Store made another package loadable, want it ignored") - } + t.Run("its package", func(t *testing.T) { + got, ok := cache.Load("example.com/a") + assert.True(t, ok, "Load(its package) ok") + assert.Len(t, got, 1, "Load(its package) want the loaded packages") + }) + + t.Run("another package", func(t *testing.T) { + got, ok := cache.Load("example.com/b") + assert.False(t, ok, "Load(another package) ok") + assert.Nil(t, got, "Load(another package) want nothing") + }) + + t.Run("store is ignored", func(t *testing.T) { + cache.Store("example.com/b", nil) + _, ok := cache.Load("example.com/b") + assert.False(t, ok, "Store made another package loadable, want it ignored") + }) } func TestGenerate(t *testing.T) { @@ -163,40 +162,35 @@ type RoutesReceiver interface { func TemplateRoutes(mux *http.ServeMux, receiver RoutesReceiver) {} `, } { - if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil { - t.Fatal(err) - } + require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644)) } _, pl, err := load.Packages(dir) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - files, err := Generate(Config{ - PackagePath: "example.com/app", - PackageDir: dir, - RoutesFunction: "TemplateRoutes", - ReceiverInterface: "RoutesReceiver", - FakeImportPath: "example.com/app/cmd/explore/internal/fake", - }, pl) - if err != nil { - t.Fatalf("Generate() error = %v", err) - } - for _, want := range []string{`routes "example.com/app"`, "routes.TemplateRoutes(mux, receiver)"} { - if !strings.Contains(string(files.Main), want) { - t.Errorf("Main = %s\nwant containing %q", files.Main, want) + t.Run("a library with a receiver interface", func(t *testing.T) { + files, err := Generate(Config{ + PackagePath: "example.com/app", + PackageDir: dir, + RoutesFunction: "TemplateRoutes", + ReceiverInterface: "RoutesReceiver", + FakeImportPath: "example.com/app/cmd/explore/internal/fake", + }, pl) + require.NoError(t, err, "Generate()") + for _, want := range []string{`routes "example.com/app"`, "routes.TemplateRoutes(mux, receiver)"} { + assert.Contains(t, string(files.Main), want, "Main want containing %q", want) } - } - for _, want := range []string{"package fake", "type RoutesReceiver struct", "func (fake *RoutesReceiver) Home() any"} { - if !strings.Contains(string(files.Fake), want) { - t.Errorf("Fake = %s\nwant containing %q", files.Fake, want) + for _, want := range []string{"package fake", "type RoutesReceiver struct", "func (fake *RoutesReceiver) Home() any"} { + assert.Contains(t, string(files.Fake), want, "Fake want containing %q", want) } - } + }) - if _, err := Generate(Config{PackagePath: "example.com/missing"}, pl); err == nil { - t.Error("Generate(missing package) = nil error, want one") - } - if _, err := Generate(Config{PackagePath: "example.com/app", PackageDir: dir, ReceiverInterface: "Missing"}, pl); err == nil { - t.Error("Generate(missing interface) = nil error, want one") - } + t.Run("a missing package", func(t *testing.T) { + _, err := Generate(Config{PackagePath: "example.com/missing"}, pl) + assert.Error(t, err, "Generate(missing package)") + }) + + t.Run("a missing interface", func(t *testing.T) { + _, err := Generate(Config{PackagePath: "example.com/app", PackageDir: dir, ReceiverInterface: "Missing"}, pl) + assert.Error(t, err, "Generate(missing interface)") + }) } From a6152b1a753f90ea6521d2c15ba621c691a07593 Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:42:06 -0700 Subject: [PATCH 20/23] use testify in the load package and loadtest tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/load/loadtest/loadtest_test.go | 51 ++++++------ internal/load/package_test.go | 104 +++++++++++------------- 2 files changed, 73 insertions(+), 82 deletions(-) diff --git a/internal/load/loadtest/loadtest_test.go b/internal/load/loadtest/loadtest_test.go index 166119b3..d58a0ee2 100644 --- a/internal/load/loadtest/loadtest_test.go +++ b/internal/load/loadtest/loadtest_test.go @@ -5,6 +5,9 @@ import ( "slices" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/load" "github.com/typelate/muxt/internal/load/loadtest" ) @@ -35,30 +38,28 @@ func Render(w io.Writer, name string) error { }) pkg, err := load.Package(dir, pl, []string{"templates", "inline"}) - if err != nil { - t.Fatal(err) - } - if pkg.Types.Path() != "example.com/server" { - t.Errorf("package %s, want example.com/server", pkg.Types.Path()) - } - sets := pkg.Variables + require.NoError(t, err) + assert.Equal(t, "example.com/server", pkg.Types.Path(), "package path") + require.Len(t, pkg.Variables, 2) + templates, inline := pkg.Variables[0], pkg.Variables[1] + + t.Run("ParseFS reads the embedded page", func(t *testing.T) { + assert.NotNil(t, templates.Set.Lookup("page"), "templates does not hold the page ParseFS read") + definition, ok := templates.Definitions["page"] + assert.True(t, ok, "page has a definition") + assert.Equal(t, filepath.Join(dir, "page.gohtml"), definition.Define.Filename, "page defined at %+v, want in page.gohtml", definition.Define) + }) - if sets[0].Set.Lookup("page") == nil { - t.Error("templates does not hold the page ParseFS read") - } - definition, ok := sets[0].Definitions["page"] - if !ok || definition.Define.Filename != filepath.Join(dir, "page.gohtml") { - t.Errorf("page defined at %+v, want in page.gohtml", definition.Define) - } - if got := len(sets[0].Calls); got != 1 { - t.Errorf("%d ExecuteTemplate calls, want the one Render makes", got) - } - var names []string - for _, tmpl := range sets[1].Set.Templates() { - names = append(names, tmpl.Name()) - } - slices.Sort(names) - if !slices.Equal(names, []string{"inline", "note"}) { - t.Errorf("inline holds %q, want inline and note", names) - } + t.Run("the one ExecuteTemplate call Render makes", func(t *testing.T) { + assert.Len(t, templates.Calls, 1) + }) + + t.Run("an inline template with custom delimiters", func(t *testing.T) { + var names []string + for _, tmpl := range inline.Set.Templates() { + names = append(names, tmpl.Name()) + } + slices.Sort(names) + assert.Equal(t, []string{"inline", "note"}, names, "inline holds inline and note") + }) } diff --git a/internal/load/package_test.go b/internal/load/package_test.go index 7e1fb7c5..18bb5121 100644 --- a/internal/load/package_test.go +++ b/internal/load/package_test.go @@ -4,21 +4,30 @@ import ( "errors" "os" "path/filepath" - "slices" "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/tools/go/packages" ) -func TestTestVariantsFirst(t *testing.T) { - ids := func(pl []*packages.Package) []string { - var got []string - for _, pkg := range pl { - got = append(got, pkg.ID) - } - return got +func writeFiles(t *testing.T, dir string, files map[string]string) { + t.Helper() + for name, content := range files { + require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644)) + } +} + +func packageIDs(pl []*packages.Package) []string { + var ids []string + for _, pkg := range pl { + ids = append(ids, pkg.ID) } + return ids +} + +func TestTestVariantsFirst(t *testing.T) { in := []*packages.Package{ {ID: "example.com/a"}, {ID: "example.com/b [example.com/b.test]"}, @@ -33,43 +42,37 @@ func TestTestVariantsFirst(t *testing.T) { "example.com/c", "example.com/b.test", } - if got := ids(testVariantsFirst(in)); !slices.Equal(got, want) { - t.Errorf("testVariantsFirst() = %q, want %q", got, want) - } - if got := ids(in); got[0] != "example.com/a" { - t.Errorf("testVariantsFirst reordered its argument: %q", got) - } + + assert.Equal(t, want, packageIDs(testVariantsFirst(in)), "testVariantsFirst()") + assert.Equal(t, "example.com/a", packageIDs(in)[0], "testVariantsFirst reordered its argument") } func TestPackagesWithTests(t *testing.T) { dir := t.TempDir() - for name, content := range map[string]string{ + writeFiles(t, dir, map[string]string{ "go.mod": "module example.com/p\n\ngo 1.24\n", "p.go": "package p\n\nfunc F() int { return 1 }\n", "p_test.go": "package p\n\nimport \"testing\"\n\nfunc TestF(t *testing.T) { _ = F() }\n", - } { - if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil { - t.Fatal(err) - } - } + }) pl, err := PackagesWithTests(dir, nil) - if err != nil { - t.Fatalf("PackagesWithTests() error = %v", err) - } - if len(pl) == 0 || !strings.HasSuffix(pl[0].ID, ".test]") { - t.Fatalf("PackagesWithTests()[0] = %v, want the package compiled with its tests first", pl) - } - var files []string - for _, f := range pl[0].GoFiles { - files = append(files, filepath.Base(f)) - } - if !slices.Contains(files, "p_test.go") { - t.Errorf("first package files = %q, want p_test.go among them", files) - } - if got, ok := PackageInDirectory(pl, dir); !ok || got != pl[0] { - t.Errorf("PackageInDirectory() = %v, %v, want the test variant", got, ok) - } + require.NoError(t, err, "PackagesWithTests()") + require.NotEmpty(t, pl, "PackagesWithTests()") + require.True(t, strings.HasSuffix(pl[0].ID, ".test]"), "PackagesWithTests()[0] = %v, want the package compiled with its tests first", pl) + + t.Run("the first package holds the test files", func(t *testing.T) { + var files []string + for _, f := range pl[0].GoFiles { + files = append(files, filepath.Base(f)) + } + assert.Contains(t, files, "p_test.go", "first package files") + }) + + t.Run("the directory resolves to the test variant", func(t *testing.T) { + got, ok := PackageInDirectory(pl, dir) + assert.True(t, ok, "PackageInDirectory() ok") + assert.Same(t, pl[0], got, "PackageInDirectory() want the test variant") + }) } func loadError(kind packages.ErrorKind, pos, msg string) packages.Error { @@ -136,35 +139,25 @@ func TestParseErrors(t *testing.T) { for _, e := range ParseErrors(tt.pl) { got = append(got, e.Error()) } - if !slices.Equal(got, tt.want) { - t.Errorf("ParseErrors = %q, want %q", got, tt.want) - } + assert.Equal(t, tt.want, got, "ParseErrors") }) } } func TestPackagesWithEnvLoadsExtraPatterns(t *testing.T) { dir := t.TempDir() - for name, content := range map[string]string{ + writeFiles(t, dir, map[string]string{ "go.mod": "module example.com/p\n\ngo 1.24\n", "p.go": "package p\n", - } { - if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil { - t.Fatal(err) - } - } + }) _, pl, err := PackagesWithEnv(dir, nil, "", "os") - if err != nil { - t.Fatalf("PackagesWithEnv() error = %v", err) - } + require.NoError(t, err, "PackagesWithEnv()") var paths []string for _, pkg := range pl { paths = append(paths, pkg.PkgPath) } for _, want := range []string{"example.com/p", "fmt", "os"} { - if !slices.Contains(paths, want) { - t.Errorf("PackagesWithEnv() loaded %q, want it to include %q", paths, want) - } + assert.Contains(t, paths, want, "PackagesWithEnv() loaded %q, want it to include %q", paths, want) } } @@ -181,12 +174,9 @@ func TestLoadFailedError(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { e, ok := loadFailedError(t.TempDir(), errors.New(tt.msg)).(*PackageLookupError) - if !ok { - t.Fatal("loadFailedError() is not a *PackageLookupError") - } - if len(e.Details) != 2 || e.Details[0] != tt.want { - t.Errorf("loadFailedError() details = %q, want the message %q then the environment note", e.Details, tt.want) - } + require.True(t, ok, "loadFailedError() is not a *PackageLookupError") + require.Len(t, e.Details, 2, "loadFailedError() details = %q, want the message then the environment note", e.Details) + assert.Equal(t, tt.want, e.Details[0], "loadFailedError() details = %q, want the message %q then the environment note", e.Details, tt.want) }) } } From 402b144724dfd56ac5a2effab15d530826828b69 Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:42:30 -0700 Subject: [PATCH 21/23] use testify in the analysis tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/analysis/analysis_test.go | 33 ++++------ internal/analysis/check_test.go | 74 ++++++++-------------- internal/analysis/mutants_internal_test.go | 4 +- internal/analysis/snapshot_test.go | 43 +++++-------- 4 files changed, 55 insertions(+), 99 deletions(-) diff --git a/internal/analysis/analysis_test.go b/internal/analysis/analysis_test.go index 22168db2..e30c6ea1 100644 --- a/internal/analysis/analysis_test.go +++ b/internal/analysis/analysis_test.go @@ -5,6 +5,9 @@ import ( "go/types" "regexp" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewReferences(t *testing.T) { @@ -43,25 +46,16 @@ func TestNewReferences(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { got := names(newReferences("example.com/p", refs(), tt.filter)) - if len(got) != len(tt.want) { - t.Fatalf("newReferences() names = %v, want %v", got, tt.want) - } - for i := range got { - if got[i] != tt.want[i] { - t.Fatalf("newReferences() names = %v, want %v", got, tt.want) - } - } + assert.Equal(t, tt.want, got, "newReferences() names") }) } t.Run("references are sorted by position and typed", func(t *testing.T) { got := newReferences("example.com/p", refs(), nil)[1] - if len(got.References) != 2 || got.References[0].Position.Offset != 1 || got.References[1].Position.Offset != 9 { - t.Fatalf("references of b = %+v, want offsets 1 then 9", got.References) - } - if got.References[0].Data != "string" { - t.Errorf("Data = %q, want %q", got.References[0].Data, "string") - } + require.Len(t, got.References, 2, "references of b = %+v, want offsets 1 then 9", got.References) + assert.Equal(t, 1, got.References[0].Position.Offset, "references of b = %+v, want offsets 1 then 9", got.References) + assert.Equal(t, 9, got.References[1].Position.Offset, "references of b = %+v, want offsets 1 then 9", got.References) + assert.Equal(t, "string", got.References[0].Data, "Data") }) } @@ -70,10 +64,11 @@ func TestNewNamedReferencesOrdersKindsAtOnePosition(t *testing.T) { parse := TemplateReference{Name: "x", Kind: ParseTemplateNode, Position: pos, data: types.Typ[types.Int]} execute := TemplateReference{Name: "x", Kind: ExecuteTemplateNode, Position: pos, data: types.Typ[types.Int]} for _, in := range [][]TemplateReference{{parse, execute}, {execute, parse}} { - got := NewNamedReferences("example.com/p", "x", in).References - if len(got) != 2 || got[0].Kind != ExecuteTemplateNode || got[1].Kind != ParseTemplateNode { - t.Errorf("NewNamedReferences(%v) kinds = %v, want execute_template then template", in, got) + var kinds []TemplateReferenceKind + for _, ref := range NewNamedReferences("example.com/p", "x", in).References { + kinds = append(kinds, ref.Kind) } + assert.Equal(t, []TemplateReferenceKind{ExecuteTemplateNode, ParseTemplateNode}, kinds, "NewNamedReferences(%v) kinds, want execute_template then template", in) } } @@ -100,9 +95,7 @@ func TestNewNamedReferences(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { got := NewNamedReferences("example.com/p", "x", tt.refs) - if len(got.References) != tt.want { - t.Errorf("NewNamedReferences() has %d references, want %d: %+v", len(got.References), tt.want, got.References) - } + assert.Len(t, got.References, tt.want, "NewNamedReferences() references: %+v", got.References) }) } } diff --git a/internal/analysis/check_test.go b/internal/analysis/check_test.go index 2b59f41b..35593c44 100644 --- a/internal/analysis/check_test.go +++ b/internal/analysis/check_test.go @@ -3,10 +3,12 @@ package analysis import ( "html/template" "log" - "slices" "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/muxt" "github.com/typelate/muxt/internal/source" ) @@ -17,9 +19,7 @@ import ( func parseTemplates(t *testing.T, text string) *template.Template { t.Helper() ts, err := template.New("set").Parse(text) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) return ts } @@ -73,9 +73,7 @@ func TestFindUnusedTemplates(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { got := findUnusedTemplates(parseTemplates(t, tt.templates), tt.executed) - if !slices.Equal(got, tt.want) { - t.Errorf("findUnusedTemplates = %q, want %q", got, tt.want) - } + assert.Equal(t, tt.want, got, "findUnusedTemplates") }) } } @@ -86,9 +84,7 @@ func TestFindUnusedTemplates(t *testing.T) { // template already renders is waiting on that route rather than unused. func TestPartitionUnusedTemplates(t *testing.T) { const route = "GET / Home()" - if !muxt.IsRouteDefinitionName(route) { - t.Fatalf("the premise of this test is wrong: %q does not name a route", route) - } + require.True(t, muxt.IsRouteDefinitionName(route), "the premise of this test is wrong: %q does not name a route", route) for _, tt := range []struct { name string @@ -135,12 +131,8 @@ func TestPartitionUnusedTemplates(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { routes, partials := partitionUnusedTemplates(parseTemplates(t, tt.templates), tt.unused) - if !slices.Equal(routes, tt.wantRoutes) { - t.Errorf("routes = %q, want %q", routes, tt.wantRoutes) - } - if !slices.Equal(partials, tt.wantPartials) { - t.Errorf("partials = %q, want %q", partials, tt.wantPartials) - } + assert.Equal(t, tt.wantRoutes, routes, "routes") + assert.Equal(t, tt.wantPartials, partials, "partials") }) } } @@ -183,16 +175,12 @@ func TestReportUnusedTemplates(t *testing.T) { for _, err := range errs { got = append(got, err.Error()) } - if !slices.Equal(got, tt.wantErrors) { - t.Errorf("errors = %q, want %q", got, tt.wantErrors) - } - if tt.wantSilent && logs.Len() != 0 { - t.Errorf("log = %q, want nothing", logs.String()) + assert.Equal(t, tt.wantErrors, got, "errors") + if tt.wantSilent { + assert.Empty(t, logs.String(), "log, want nothing") } for _, part := range tt.wantLogParts { - if !strings.Contains(logs.String(), part) { - t.Errorf("log = %q, want containing %q", logs.String(), part) - } + assert.Contains(t, logs.String(), part, "log, want containing %q", part) } }) } @@ -203,13 +191,9 @@ func TestCollectTemplateReferencesFollowsEveryBranch(t *testing.T) { seen := make(map[string]bool) collectTemplateReferences(ts, ts.Lookup("root").Tree.Root, seen) for _, name := range []string{"in-range", "in-range-else", "in-with", "in-with-else", "loop", "leaf"} { - if !seen[name] { - t.Errorf("collectTemplateReferences did not reach %q; reached %v", name, seen) - } - } - if seen["root"] { - t.Errorf("collectTemplateReferences reached the root template itself") + assert.True(t, seen[name], "collectTemplateReferences did not reach %q; reached %v", name, seen) } + assert.False(t, seen["root"], "collectTemplateReferences reached the root template itself") } // declaredTemplates has a template the set knows by name but that was never @@ -219,9 +203,9 @@ func declaredTemplates(t *testing.T) *template.Template { ts := parseTemplates(t, `{{define "GET / Home()"}}{{template "declared"}}{{template "undefined"}}{{end}}`) ts.New("declared") ts.New("GET /bare Bare()") - if ts.Lookup("declared") == nil || ts.Lookup("declared").Tree != nil { - t.Fatal("the premise of this test is wrong: declared should exist without a tree") - } + declared := ts.Lookup("declared") + require.NotNil(t, declared, "the premise of this test is wrong: declared should exist without a tree") + require.Nil(t, declared.Tree, "the premise of this test is wrong: declared should exist without a tree") return ts } @@ -231,21 +215,17 @@ func TestTemplatesWithoutATreeAreSkipped(t *testing.T) { t.Run("collectTemplateReferences", func(t *testing.T) { seen := make(map[string]bool) collectTemplateReferences(ts, ts.Lookup("GET / Home()").Tree.Root, seen) - if !seen["declared"] || !seen["undefined"] { - t.Errorf("collectTemplateReferences reached %v, want declared and undefined", seen) - } + assert.True(t, seen["declared"], "collectTemplateReferences reached %v, want declared", seen) + assert.True(t, seen["undefined"], "collectTemplateReferences reached %v, want undefined", seen) }) t.Run("partitionUnusedTemplates", func(t *testing.T) { routes, partials := partitionUnusedTemplates(ts, []string{"GET / Home()"}) - if len(routes) != 1 || len(partials) != 0 { - t.Errorf("partitionUnusedTemplates() = %q, %q, want the route only", routes, partials) - } + assert.Len(t, routes, 1, "partitionUnusedTemplates() routes, want the route only") + assert.Empty(t, partials, "partitionUnusedTemplates() partials, want the route only") }) t.Run("findUnusedTemplates", func(t *testing.T) { got := findUnusedTemplates(ts, executed()) - if !slices.Equal(got, []string{"GET / Home()"}) { - t.Errorf("findUnusedTemplates() = %q, want only the route", got) - } + assert.Equal(t, []string{"GET / Home()"}, got, "findUnusedTemplates() want only the route") }) t.Run("executeTemplateTree", func(t *testing.T) { // A nil global is never touched when there is no tree to walk. @@ -256,9 +236,9 @@ func TestTemplatesWithoutATreeAreSkipped(t *testing.T) { func TestReportDefinitionErrorsIsSilentWithoutErrors(t *testing.T) { var logs strings.Builder - if err := reportDefinitionErrors(log.New(&logs, "", 0), source.Variable{Set: parseTemplates(t, `{{define "footer"}}x{{end}}`)}); err != nil || logs.Len() != 0 { - t.Errorf("reportDefinitionErrors() = %v with log %q, want neither", err, logs.String()) - } + err := reportDefinitionErrors(log.New(&logs, "", 0), source.Variable{Set: parseTemplates(t, `{{define "footer"}}x{{end}}`)}) + assert.NoError(t, err, "reportDefinitionErrors()") + assert.Empty(t, logs.String(), "reportDefinitionErrors() log") } // TestIsEmptyTemplate states what counts as a template with nothing to @@ -278,9 +258,7 @@ func TestIsEmptyTemplate(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { ts := parseTemplates(t, `{{define "t"}}`+tt.template+`{{end}}`) - if got := isEmptyTemplate(ts.Lookup("t").Tree.Root); got != tt.want { - t.Errorf("isEmptyTemplate(%q) = %t, want %t", tt.template, got, tt.want) - } + assert.Equal(t, tt.want, isEmptyTemplate(ts.Lookup("t").Tree.Root), "isEmptyTemplate(%q)", tt.template) }) } } diff --git a/internal/analysis/mutants_internal_test.go b/internal/analysis/mutants_internal_test.go index 1a57ff04..69d50ed0 100644 --- a/internal/analysis/mutants_internal_test.go +++ b/internal/analysis/mutants_internal_test.go @@ -20,9 +20,7 @@ func TestIndent(t *testing.T) { {in: "a\nb", want: "> a\n> b"}, {in: "a\n", want: "> a\n> "}, } { - if got := indent("> ", tt.in); got != tt.want { - t.Errorf("indent(%q) = %q, want %q", tt.in, got, tt.want) - } + assert.Equal(t, tt.want, indent("> ", tt.in), "indent(%q)", tt.in) } } diff --git a/internal/analysis/snapshot_test.go b/internal/analysis/snapshot_test.go index b2824897..be00acfa 100644 --- a/internal/analysis/snapshot_test.go +++ b/internal/analysis/snapshot_test.go @@ -15,6 +15,8 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/tools/txtar" "github.com/typelate/muxt/internal/analysis" @@ -68,27 +70,21 @@ var templates = template.Must(template.ParseFS(templateFiles, "*.gohtml")) // written to. Run with -update to rewrite the want/ files, then read the // diff. func TestSnapshots(t *testing.T) { - if stray, _ := filepath.Glob(filepath.Join("testdata", "*.txtar")); len(stray) > 0 { - t.Fatalf("%s is not in a command's directory", stray[0]) - } + stray, _ := filepath.Glob(filepath.Join("testdata", "*.txtar")) + require.Empty(t, stray, "a testdata archive is not in a command's directory") directories, err := os.ReadDir("testdata") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) for _, directory := range directories { if !directory.IsDir() { continue } command := directory.Name() newConfiguration, ok := commands[command] - if !ok { - t.Errorf("testdata/%s names no command this package snapshots", command) + if !assert.True(t, ok, "testdata/%s names no command this package snapshots", command) { continue } archives, err := filepath.Glob(filepath.Join("testdata", command, "*.txtar")) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) t.Run(command, func(t *testing.T) { for _, archivePath := range archives { runSnapshot(t, archivePath, newConfiguration) @@ -112,9 +108,7 @@ func runSnapshot(t *testing.T, archivePath string, newConfiguration func() any) t.Helper() t.Run(strings.TrimSuffix(filepath.Base(archivePath), ".txtar"), func(t *testing.T) { archive, err := txtar.ParseFile(archivePath) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) got := snapshot(t, configuration(t, archive, newConfiguration()), archive) if *update { files := slices.DeleteFunc(slices.Clone(archive.Files), func(file txtar.File) bool { @@ -124,9 +118,7 @@ func runSnapshot(t *testing.T, archivePath string, newConfiguration func() any) files = append(files, txtar.File{Name: "want/" + name, Data: []byte(got[name])}) } archive.Files = files - if err := os.WriteFile(archivePath, txtar.Format(archive), 0o644); err != nil { - t.Fatal(err) - } + require.NoError(t, os.WriteFile(archivePath, txtar.Format(archive), 0o644)) return } want := make(map[string]string) @@ -136,9 +128,7 @@ func runSnapshot(t *testing.T, archivePath string, newConfiguration func() any) } } for _, name := range sortedKeys(got, want) { - if got[name] != want[name] { - t.Errorf("want/%s differs (run go test -run TestSnapshots -update to rewrite):\n--- got\n%s\n--- want\n%s", name, got[name], want[name]) - } + assert.Equal(t, want[name], got[name], "want/%s differs (run go test -run TestSnapshots -update to rewrite)", name) } }) } @@ -152,12 +142,10 @@ func configuration(t *testing.T, archive *txtar.Archive, config any) any { if file.Name != "config.json" { continue } - if err := json.Unmarshal(file.Data, config, configjson.Options()); err != nil { - t.Fatalf("config.json: %v", err) - } + require.NoError(t, json.Unmarshal(file.Data, config, configjson.Options()), "config.json") return reflect.ValueOf(config).Elem().Interface() } - t.Fatal("the archive has no config.json") + require.Fail(t, "the archive has no config.json") return nil } @@ -226,7 +214,7 @@ func snapshot(t *testing.T, config any, archive *txtar.Archive) map[string]strin writeTo(t, &stdout, result) } default: - t.Fatalf("no analysis runs with a %T", config) + require.Failf(t, "unsupported configuration", "no analysis runs with a %T", config) } if stdout.Len() > 0 { got["stdout.txt"] = relative(stdout.String()) @@ -243,9 +231,8 @@ func snapshot(t *testing.T, config any, archive *txtar.Archive) map[string]strin func writeTo(t *testing.T, w io.Writer, result io.WriterTo) { t.Helper() - if _, err := result.WriteTo(w); err != nil { - t.Fatal(err) - } + _, err := result.WriteTo(w) + require.NoError(t, err) } func sortedKeys(maps ...map[string]string) []string { From d61a51470df7cc5119040fb904843313b8748f2f Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:43:12 -0700 Subject: [PATCH 22/23] use testify in the cli flag and validation tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/cli/analysis_commands_test.go | 36 ++++++++++++-------------- internal/cli/commands_test.go | 28 +++++--------------- internal/cli/generate_args_test.go | 25 +++++++----------- internal/cli/validate_test.go | 13 +++++----- 4 files changed, 38 insertions(+), 64 deletions(-) diff --git a/internal/cli/analysis_commands_test.go b/internal/cli/analysis_commands_test.go index 5a18ed57..8a1e491b 100644 --- a/internal/cli/analysis_commands_test.go +++ b/internal/cli/analysis_commands_test.go @@ -1,29 +1,28 @@ package cli -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) func TestCompilePatterns(t *testing.T) { t.Run("none stays nil", func(t *testing.T) { got, err := compilePatterns(nil) - if err != nil || got != nil { - t.Fatalf("compilePatterns(nil) = %v, %v, want nil, nil", got, err) - } + require.NoError(t, err, "compilePatterns(nil)") + require.Nil(t, got, "compilePatterns(nil)") }) t.Run("in order", func(t *testing.T) { got, err := compilePatterns([]string{`^GET `, `Home$`}) - if err != nil { - t.Fatalf("compilePatterns() error = %v", err) - } - if len(got) != 2 || got[0].String() != `^GET ` || got[1].String() != `Home$` { - t.Fatalf("compilePatterns() = %v, want both patterns in order", got) - } + require.NoError(t, err, "compilePatterns()") + require.Len(t, got, 2, "compilePatterns() want both patterns in order") + assert.Equal(t, `^GET `, got[0].String()) + assert.Equal(t, `Home$`, got[1].String()) }) t.Run("invalid", func(t *testing.T) { _, err := compilePatterns([]string{`ok`, `(`}) - want := "error parsing regexp: missing closing ): `(`" - if err == nil || err.Error() != want { - t.Fatalf("compilePatterns() error = %v, want %q", err, want) - } + require.EqualError(t, err, "error parsing regexp: missing closing ): `(`", "compilePatterns()") }) } @@ -41,12 +40,11 @@ func TestCheckTemplatesVariables(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { err := checkTemplatesVariables(tt.in) - switch { - case tt.wantErr == "" && err != nil: - t.Fatalf("checkTemplatesVariables(%q) = %v, want no error", tt.in, err) - case tt.wantErr != "" && (err == nil || err.Error() != tt.wantErr): - t.Fatalf("checkTemplatesVariables(%q) = %v, want %q", tt.in, err, tt.wantErr) + if tt.wantErr == "" { + require.NoError(t, err, "checkTemplatesVariables(%q)", tt.in) + return } + require.EqualError(t, err, tt.wantErr, "checkTemplatesVariables(%q)", tt.in) }) } } diff --git a/internal/cli/commands_test.go b/internal/cli/commands_test.go index ccc5dcdd..d9dd5d97 100644 --- a/internal/cli/commands_test.go +++ b/internal/cli/commands_test.go @@ -95,20 +95,11 @@ func TestMultipartMaxMemoryFlag_Set(t *testing.T) { f := &multipartMaxMemoryFlag{cfg: cfg} err := f.Set(tc.input) if tc.wantErr != "" { - if err == nil { - t.Fatalf("Set(%q) = nil, want error containing %q", tc.input, tc.wantErr) - } - if !strings.Contains(err.Error(), tc.wantErr) { - t.Fatalf("Set(%q) error = %q, want containing %q", tc.input, err.Error(), tc.wantErr) - } + require.ErrorContains(t, err, tc.wantErr, "Set(%q)", tc.input) return } - if err != nil { - t.Fatalf("Set(%q) = %v, want no error", tc.input, err) - } - if cfg.MultipartMaxMemory != tc.want { - t.Fatalf("Set(%q) stored %d, want %d", tc.input, cfg.MultipartMaxMemory, tc.want) - } + require.NoError(t, err, "Set(%q)", tc.input) + require.Equal(t, tc.want, cfg.MultipartMaxMemory, "Set(%q) stored", tc.input) }) } } @@ -117,23 +108,16 @@ func TestMultipartMaxMemoryFlag_String(t *testing.T) { t.Run("zero shows default", func(t *testing.T) { f := &multipartMaxMemoryFlag{cfg: &generate.RoutesFileConfiguration{}} got := f.String() - if !strings.Contains(got, "MiB") && !strings.Contains(got, "MB") { - t.Fatalf("String() = %q, want a human-readable size", got) - } + require.True(t, strings.Contains(got, "MiB") || strings.Contains(got, "MB"), "String() = %q, want a human-readable size", got) }) t.Run("a nil flag or configuration shows default", func(t *testing.T) { want := (&multipartMaxMemoryFlag{cfg: &generate.RoutesFileConfiguration{}}).String() for name, f := range map[string]*multipartMaxMemoryFlag{"nil flag": nil, "nil configuration": {}} { - if got := f.String(); got != want { - t.Errorf("String() of a %s = %q, want %q", name, got, want) - } + assert.Equal(t, want, f.String(), "String() of a %s", name) } }) t.Run("override shows override", func(t *testing.T) { f := &multipartMaxMemoryFlag{cfg: &generate.RoutesFileConfiguration{MultipartMaxMemory: 64 << 20}} - got := f.String() - if !strings.Contains(got, "64") { - t.Fatalf("String() = %q, want containing 64", got) - } + require.Contains(t, f.String(), "64", "String()") }) } diff --git a/internal/cli/generate_args_test.go b/internal/cli/generate_args_test.go index a386266c..7d1e88e2 100644 --- a/internal/cli/generate_args_test.go +++ b/internal/cli/generate_args_test.go @@ -2,10 +2,11 @@ package cli import ( "io" - "reflect" "testing" "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/typelate/muxt/internal/generate" ) @@ -27,9 +28,7 @@ func parseGeneratedHeader(t *testing.T, args []string) generate.RoutesFileConfig set := pflag.NewFlagSet("header", pflag.ContinueOnError) set.SetOutput(io.Discard) addGenerateFlags(set, &config, &deprecated) - if err := set.Parse(args); err != nil { - t.Fatalf("parsing the header %q: %v", args, err) - } + require.NoError(t, set.Parse(args), "parsing the header %q", args) return config } @@ -106,9 +105,7 @@ func TestGeneratedHeaderRoundTrip(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { got := parseGeneratedHeader(t, configToArgs(tt.config)) - if !reflect.DeepEqual(recorded(got), recorded(tt.want)) { - t.Errorf("the header read back as\n%+v\nwant\n%+v", recorded(got), recorded(tt.want)) - } + assert.Equal(t, recorded(tt.want), recorded(got), "the header read back") }) } } @@ -159,18 +156,16 @@ func TestConfigToArgsOrder(t *testing.T) { "--output-muxt-version=false", "--output-multipart-max-memory=1024", } - if got := configToArgs(config); !reflect.DeepEqual(got, want) { - t.Errorf("configToArgs = %q, want %q", got, want) - } + assert.Equal(t, want, configToArgs(config), "configToArgs") } // TestConfigToArgsRecordsWhatDiffersFromTheDefaults states that the header // stays as short as the run was ordinary: a flag left at its default is // not written into it, and one that was passed is. func TestConfigToArgsRecordsWhatDiffersFromTheDefaults(t *testing.T) { - if got := configToArgs(defaultsConfig()); len(got) != 0 { - t.Errorf("configToArgs(defaults) = %q, want nothing recorded", got) - } + t.Run("the defaults record nothing", func(t *testing.T) { + assert.Empty(t, configToArgs(defaultsConfig()), "configToArgs(defaults)") + }) for _, tt := range []struct { name string @@ -206,9 +201,7 @@ func TestConfigToArgsRecordsWhatDiffersFromTheDefaults(t *testing.T) { t.Run(tt.name, func(t *testing.T) { config := defaultsConfig() tt.change(&config) - if got := configToArgs(config); !reflect.DeepEqual(got, tt.want) { - t.Errorf("configToArgs = %q, want %q", got, tt.want) - } + assert.Equal(t, tt.want, configToArgs(config), "configToArgs") }) } } diff --git a/internal/cli/validate_test.go b/internal/cli/validate_test.go index fd390e6e..c466aa51 100644 --- a/internal/cli/validate_test.go +++ b/internal/cli/validate_test.go @@ -3,6 +3,8 @@ package cli import ( "testing" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/generate" ) @@ -33,14 +35,11 @@ func TestValidateGenerateConfiguration(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { err := validateGenerateConfiguration(tt.config) - switch { - case tt.wantErr == "" && err != nil: - t.Fatalf("validateGenerateConfiguration() = %v, want no error", err) - case tt.wantErr != "" && err == nil: - t.Fatalf("validateGenerateConfiguration() = nil, want %q", tt.wantErr) - case tt.wantErr != "" && err.Error() != tt.wantErr: - t.Fatalf("validateGenerateConfiguration() = %q, want %q", err, tt.wantErr) + if tt.wantErr == "" { + require.NoError(t, err, "validateGenerateConfiguration()") + return } + require.EqualError(t, err, tt.wantErr, "validateGenerateConfiguration()") }) } } From 583b7387159dc18f5ec1101eddc9e20b18b182dd Mon Sep 17 00:00:00 2001 From: Christopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:43:12 -0700 Subject: [PATCH 23/23] use testify in the cli command and file tests Assisted-by: Claude:claude-sonnet-5-5 gofumpt goimports staticcheck gremlins --- internal/cli/generated_files_test.go | 118 ++++++++++----------------- internal/cli/module_commands_test.go | 96 +++++++++------------- internal/cli/output_test.go | 99 +++++++++------------- internal/cli/run_test.go | 103 ++++++++++------------- 4 files changed, 162 insertions(+), 254 deletions(-) diff --git a/internal/cli/generated_files_test.go b/internal/cli/generated_files_test.go index db28de86..a75a0469 100644 --- a/internal/cli/generated_files_test.go +++ b/internal/cli/generated_files_test.go @@ -6,11 +6,13 @@ import ( "log" "os" "path/filepath" - "reflect" "slices" "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/generate" "github.com/typelate/muxt/internal/header" ) @@ -18,9 +20,7 @@ import ( func writeTestFile(t *testing.T, dir, name, content string) string { t.Helper() path := filepath.Join(dir, name) - if err := os.WriteFile(path, []byte(content), 0o644); err != nil { - t.Fatal(err) - } + require.NoError(t, os.WriteFile(path, []byte(content), 0o644)) return path } @@ -34,9 +34,7 @@ func TestPlural(t *testing.T) { {2, "2 routes"}, {12, "12 routes"}, } { - if got := plural(tt.n, "route"); got != tt.want { - t.Errorf("plural(%d, %q) = %q, want %q", tt.n, "route", got, tt.want) - } + assert.Equal(t, tt.want, plural(tt.n, "route"), "plural(%d, %q)", tt.n, "route") } } @@ -52,22 +50,18 @@ func TestOwnedGeneratedFiles(t *testing.T) { writeTestFile(t, dir, "handwritten.go", "package main\n") writeTestFile(t, dir, "notes.txt", header.Format(nil, "")) - got, err := ownedGeneratedFiles(dir, "TemplateRoutes", log.New(io.Discard, "", 0)) - if err != nil { - t.Fatalf("ownedGeneratedFiles() error = %v", err) - } - want := map[string]bool{current: true, explicitDefault: true, deprecatedFlag: true} - if !reflect.DeepEqual(got, want) { - t.Errorf("ownedGeneratedFiles(%q) = %v, want %v (not %s)", "TemplateRoutes", got, want, other) - } - - got, err = ownedGeneratedFiles(dir, "AdminRoutes", log.New(io.Discard, "", 0)) - if err != nil { - t.Fatalf("ownedGeneratedFiles() error = %v", err) - } - if want := (map[string]bool{other: true}); !reflect.DeepEqual(got, want) { - t.Errorf("ownedGeneratedFiles(%q) = %v, want %v", "AdminRoutes", got, want) - } + t.Run("the default routes function", func(t *testing.T) { + got, err := ownedGeneratedFiles(dir, "TemplateRoutes", log.New(io.Discard, "", 0)) + require.NoError(t, err, "ownedGeneratedFiles()") + want := map[string]bool{current: true, explicitDefault: true, deprecatedFlag: true} + assert.Equal(t, want, got, "ownedGeneratedFiles(%q) (not %s)", "TemplateRoutes", other) + }) + + t.Run("another routes function", func(t *testing.T) { + got, err := ownedGeneratedFiles(dir, "AdminRoutes", log.New(io.Discard, "", 0)) + require.NoError(t, err, "ownedGeneratedFiles()") + assert.Equal(t, map[string]bool{other: true}, got, "ownedGeneratedFiles(%q)", "AdminRoutes") + }) } // A header this version cannot read is not shown to belong to the current @@ -78,22 +72,15 @@ func TestOwnedGeneratedFilesIgnoresUnreadableHeaders(t *testing.T) { var stderr bytes.Buffer got, err := ownedGeneratedFiles(dir, "TemplateRoutes", log.New(&stderr, "", 0)) - if err != nil { - t.Fatalf("ownedGeneratedFiles() error = %v", err) - } - if len(got) != 0 { - t.Errorf("ownedGeneratedFiles() = %v, want the unreadable file ignored", got) - } + require.NoError(t, err, "ownedGeneratedFiles()") + assert.Empty(t, got, "ownedGeneratedFiles() want the unreadable file ignored") want := "WARNING: ignored generated file " + unreadable + " because arguments failed to parse: unknown flag: --no-such-flag\n" - if stderr.String() != want { - t.Errorf("log = %q, want %q", stderr.String(), want) - } + assert.Equal(t, want, stderr.String(), "log") } func TestOwnedGeneratedFilesMissingDirectory(t *testing.T) { - if _, err := ownedGeneratedFiles(filepath.Join(t.TempDir(), "missing"), "TemplateRoutes", log.New(io.Discard, "", 0)); err == nil { - t.Fatal("ownedGeneratedFiles(missing directory) = nil error, want one") - } + _, err := ownedGeneratedFiles(filepath.Join(t.TempDir(), "missing"), "TemplateRoutes", log.New(io.Discard, "", 0)) + require.Error(t, err, "ownedGeneratedFiles(missing directory)") } func TestWriteGeneratedFiles(t *testing.T) { @@ -116,22 +103,13 @@ func TestWriteGeneratedFiles(t *testing.T) { } var stdout bytes.Buffer written, err := writeGeneratedFiles(&stdout, files, config) - if err != nil { - t.Fatalf("writeGeneratedFiles() error = %v", err) - } - if want := "wrote a.go: 1 route\nwrote b.go: 3 routes\n"; stdout.String() != want { - t.Errorf("stdout = %q, want %q", stdout.String(), want) - } - if want := (map[string]bool{files[0].Path: true, files[1].Path: true}); !reflect.DeepEqual(written, want) { - t.Errorf("written = %v, want %v", written, want) - } + require.NoError(t, err, "writeGeneratedFiles()") + + assert.Equal(t, "wrote a.go: 1 route\nwrote b.go: 3 routes\n", stdout.String(), "stdout") + assert.Equal(t, map[string]bool{files[0].Path: true, files[1].Path: true}, written, "written") content, err := os.ReadFile(files[0].Path) - if err != nil { - t.Fatal(err) - } - if want := header.Format(nil, "v9") + "package a\n"; string(content) != want { - t.Errorf("a.go = %q, want %q", content, want) - } + require.NoError(t, err) + assert.Equal(t, header.Format(nil, "v9")+"package a\n", string(content), "a.go") } func TestWriteGeneratedFilesRollsBack(t *testing.T) { @@ -141,15 +119,10 @@ func TestWriteGeneratedFilesRollsBack(t *testing.T) { {Path: filepath.Join(dir, "missing", "b.go"), Content: "package b\n"}, } written, err := writeGeneratedFiles(&bytes.Buffer{}, files, generate.RoutesFileConfiguration{}) - if err == nil { - t.Fatal("writeGeneratedFiles() = nil error, want the failed write") - } - if written != nil { - t.Errorf("written = %v, want nil after a failure", written) - } - if _, statErr := os.Stat(files[0].Path); !os.IsNotExist(statErr) { - t.Errorf("a.go still exists after rollback: %v", statErr) - } + require.Error(t, err, "writeGeneratedFiles() want the failed write") + assert.Nil(t, written, "written after a failure") + _, statErr := os.Stat(files[0].Path) + assert.ErrorIs(t, statErr, os.ErrNotExist, "a.go still exists after rollback") } func TestWriteGeneratedFilesReportsRollbackFailure(t *testing.T) { @@ -161,9 +134,9 @@ func TestWriteGeneratedFilesReportsRollbackFailure(t *testing.T) { {Path: filepath.Join(dir, "missing", "b.go")}, } _, err := writeGeneratedFiles(&bytes.Buffer{}, files, generate.RoutesFileConfiguration{}) - if err == nil || !strings.Contains(err.Error(), "b.go") || !strings.Contains(err.Error(), "a.go") { - t.Fatalf("writeGeneratedFiles() error = %v, want both the failed write and the failed removal", err) - } + require.Error(t, err, "writeGeneratedFiles() want both the failed write and the failed removal") + require.ErrorContains(t, err, "b.go", "the failed write") + require.ErrorContains(t, err, "a.go", "the failed removal") } func TestRemoveOrphans(t *testing.T) { @@ -174,32 +147,23 @@ func TestRemoveOrphans(t *testing.T) { gone := filepath.Join(dir, "gone.go") owned := map[string]bool{kept: true, orphan: true, gone: true} - if err := removeOrphans(owned, map[string]bool{kept: true}); err != nil { - t.Fatalf("removeOrphans() error = %v", err) - } + require.NoError(t, removeOrphans(owned, map[string]bool{kept: true}), "removeOrphans()") var left []string entries, err := os.ReadDir(dir) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) for _, e := range entries { left = append(left, e.Name()) } slices.Sort(left) - if want := []string{"kept.go", "unowned.go"}; !slices.Equal(left, want) { - t.Errorf("files left = %v, want %v", left, want) - } + assert.Equal(t, []string{"kept.go", "unowned.go"}, left, "files left") } func TestRemoveOrphansReportsFailure(t *testing.T) { dir := t.TempDir() stuck := filepath.Join(dir, "stuck") - if err := os.Mkdir(stuck, 0o755); err != nil { - t.Fatal(err) - } + require.NoError(t, os.Mkdir(stuck, 0o755)) writeTestFile(t, stuck, "child", "") err := removeOrphans(map[string]bool{stuck: true}, nil) - if err == nil || !strings.HasPrefix(err.Error(), "failed to remove orphaned file "+stuck+": ") { - t.Fatalf("removeOrphans() = %v, want a failed to remove orphaned file error", err) - } + require.Error(t, err, "removeOrphans() want a failed to remove orphaned file error") + require.True(t, strings.HasPrefix(err.Error(), "failed to remove orphaned file "+stuck+": "), "removeOrphans() = %v, want a failed to remove orphaned file error", err) } diff --git a/internal/cli/module_commands_test.go b/internal/cli/module_commands_test.go index aad23693..b1a06d16 100644 --- a/internal/cli/module_commands_test.go +++ b/internal/cli/module_commands_test.go @@ -3,10 +3,12 @@ package cli import ( "os" "path/filepath" - "reflect" "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/analysis" "github.com/typelate/muxt/internal/fakeserver" ) @@ -17,9 +19,7 @@ func TestAbsoluteDir(t *testing.T) { {"/work", "/other", "/other"}, {"/work", "./a/../b", "/work/b"}, } { - if got := absoluteDir(tt.wd, tt.dir); got != tt.want { - t.Errorf("absoluteDir(%q, %q) = %q, want %q", tt.wd, tt.dir, got, tt.want) - } + assert.Equal(t, tt.want, absoluteDir(tt.wd, tt.dir), "absoluteDir(%q, %q)", tt.wd, tt.dir) } } @@ -30,9 +30,8 @@ func TestFakeImportPath(t *testing.T) { {"/work/x", "example.com/app/x/internal/fake"}, } { got, err := fakeImportPath(mod, tt.outDir) - if err != nil || got != tt.want { - t.Errorf("fakeImportPath(%q) = %q, %v, want %q", tt.outDir, got, err, tt.want) - } + assert.NoError(t, err, "fakeImportPath(%q)", tt.outDir) + assert.Equal(t, tt.want, got, "fakeImportPath(%q)", tt.outDir) } } @@ -41,14 +40,17 @@ func TestPackageInDirectory(t *testing.T) { {Path: "example.com/a", Dir: "/work/a"}, {Path: "example.com/b", Dir: "/work/b"}, }} - got, err := packageInDirectory(mod, "/work/b") - if err != nil || got.Path != "example.com/b" { - t.Fatalf("packageInDirectory(/work/b) = %+v, %v, want example.com/b", got, err) - } - _, err = packageInDirectory(mod, "/work/c") - if want := "no muxt-generated package found at /work/c"; err == nil || err.Error() != want { - t.Fatalf("packageInDirectory(/work/c) error = %v, want %q", err, want) - } + + t.Run("a generated package", func(t *testing.T) { + got, err := packageInDirectory(mod, "/work/b") + require.NoError(t, err, "packageInDirectory(/work/b)") + assert.Equal(t, "example.com/b", got.Path, "packageInDirectory(/work/b)") + }) + + t.Run("no generated package", func(t *testing.T) { + _, err := packageInDirectory(mod, "/work/c") + require.EqualError(t, err, "no muxt-generated package found at /work/c", "packageInDirectory(/work/c)") + }) } func TestNewFakeServerConfig(t *testing.T) { @@ -73,33 +75,27 @@ func TestNewFakeServerConfig(t *testing.T) { Middleware: true, FakeImportPath: "example.com/app/out/internal/fake", } - if got := newFakeServerConfig(pkg, "example.com/app/out/internal/fake"); !reflect.DeepEqual(got, want) { - t.Errorf("newFakeServerConfig() = %+v, want %+v", got, want) - } + assert.Equal(t, want, newFakeServerConfig(pkg, "example.com/app/out/internal/fake"), "newFakeServerConfig()") } func TestWriteFakeServer(t *testing.T) { out := filepath.Join(t.TempDir(), "explore") files := &fakeserver.Files{Main: []byte("package main\n"), Fake: []byte("package fake\n")} - if err := writeFakeServer(out, files); err != nil { - t.Fatalf("writeFakeServer() error = %v", err) - } + require.NoError(t, writeFakeServer(out, files), "writeFakeServer()") for path, want := range map[string]string{ filepath.Join(out, "main.go"): "package main\n", filepath.Join(out, "internal", "fake", "receiver.go"): "package fake\n", } { got, err := os.ReadFile(path) - if err != nil || string(got) != want { - t.Errorf("%s = %q, %v, want %q", path, got, err, want) - } + assert.NoError(t, err, path) + assert.Equal(t, want, string(got), path) } } func TestWriteFakeServerFailsUnderAFile(t *testing.T) { blocker := writeTestFile(t, t.TempDir(), "file", "") - if err := writeFakeServer(filepath.Join(blocker, "out"), &fakeserver.Files{}); err == nil { - t.Fatal("writeFakeServer() under a regular file = nil error, want one") - } + err := writeFakeServer(filepath.Join(blocker, "out"), &fakeserver.Files{}) + require.Error(t, err, "writeFakeServer() under a regular file") } func newTwoPackageModule(t *testing.T) string { @@ -107,9 +103,7 @@ func newTwoPackageModule(t *testing.T) string { dir := t.TempDir() writeTestFile(t, dir, "go.mod", "module example.com\n\ngo 1.24\n") for _, name := range []string{"a", "b"} { - if err := os.Mkdir(filepath.Join(dir, name), 0o755); err != nil { - t.Fatal(err) - } + require.NoError(t, os.Mkdir(filepath.Join(dir, name), 0o755)) writeTestFile(t, filepath.Join(dir, name), "template.go", `package `+name+` import ( @@ -137,45 +131,33 @@ func (s *Server) Home() any { return nil } func TestGenerateFakeServerWithSeveralPackagesKeepsTheLast(t *testing.T) { wd := newTwoPackageModule(t) for _, name := range []string{"a", "b"} { - if _, _, err := execute(t, wd, "-C", name, "generate", "--receiver-type=Server"); err != nil { - t.Fatalf("generate in %s error = %v", name, err) - } + _, _, err := execute(t, wd, "-C", name, "generate", "--receiver-type=Server") + require.NoError(t, err, "generate in %s", name) } stdout, _, err := execute(t, wd, "generate-fake-server", "a", "b", "-o", "out") - if err != nil { - t.Fatalf("generate-fake-server error = %v", err) - } - if want := "Run: go run ./out\nRun: go run ./out\n"; stdout != want { - t.Errorf("stdout = %q, want %q", stdout, want) - } + require.NoError(t, err, "generate-fake-server") + assert.Equal(t, "Run: go run ./out\nRun: go run ./out\n", stdout, "stdout") main, err := os.ReadFile(filepath.Join(wd, "out", "main.go")) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(string(main), `b "example.com/b"`) || strings.Contains(string(main), `"example.com/a"`) { - t.Errorf("main.go = %s\nwant only package b, the last argument", main) - } + require.NoError(t, err) + assert.Contains(t, string(main), `b "example.com/b"`, "main.go want package b, the last argument") + assert.NotContains(t, string(main), `"example.com/a"`, "main.go want only package b, the last argument") } func TestExploreModuleListsGeneratedPackages(t *testing.T) { wd := newTwoPackageModule(t) - if _, _, err := execute(t, wd, "-C", "a", "generate"); err != nil { - t.Fatalf("generate error = %v", err) - } + _, _, err := execute(t, wd, "-C", "a", "generate") + require.NoError(t, err, "generate") stdout, _, err := execute(t, wd, "explore-module", "--format=json") - if err != nil { - t.Fatalf("explore-module error = %v", err) - } - if !strings.Contains(stdout, `"path": "example.com/a"`) || strings.Contains(stdout, `"path": "example.com/b"`) { - t.Errorf("explore-module output = %s\nwant only package a", stdout) - } + require.NoError(t, err, "explore-module") + assert.Contains(t, stdout, `"path": "example.com/a"`, "explore-module output want package a") + assert.NotContains(t, stdout, `"path": "example.com/b"`, "explore-module output want only package a") } func TestGenerateFakeServerRejectsADirectoryWithoutRoutes(t *testing.T) { wd := newTwoPackageModule(t) _, _, err := execute(t, wd, "generate-fake-server", "a") - if want := "no muxt-generated package found at " + filepath.Join(wd, "a"); err == nil || !strings.HasSuffix(err.Error(), want) { - t.Fatalf("generate-fake-server error = %v, want ending %q", err, want) - } + require.Error(t, err, "generate-fake-server") + want := "no muxt-generated package found at " + filepath.Join(wd, "a") + require.True(t, strings.HasSuffix(err.Error(), want), "generate-fake-server error = %v, want ending %q", err, want) } diff --git a/internal/cli/output_test.go b/internal/cli/output_test.go index 65e4d455..a32bbd2d 100644 --- a/internal/cli/output_test.go +++ b/internal/cli/output_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/tools/go/packages" ) @@ -48,9 +50,7 @@ func TestWarnPartialAST(t *testing.T) { logger = nil } warnPartialAST(logger, tt.pl) - if buf.String() != tt.want { - t.Errorf("warnPartialAST() wrote %q, want %q", buf.String(), tt.want) - } + assert.Equal(t, tt.want, buf.String(), "warnPartialAST() wrote") }) } } @@ -60,26 +60,17 @@ func TestPrintMultiLineError(t *testing.T) { var stderr bytes.Buffer cmd := &cobra.Command{} cmd.SetErr(&stderr) - if !printMultiLineError(cmd, fmt.Errorf("wrapped: %w", multiLineError{})) { - t.Fatal("printMultiLineError() = false, want true") - } - if want := "Error:\nline one\nline two\n"; stderr.String() != want { - t.Errorf("stderr = %q, want %q", stderr.String(), want) - } - if !cmd.SilenceErrors { - t.Error("SilenceErrors = false, want cobra's inline error silenced") - } + require.True(t, printMultiLineError(cmd, fmt.Errorf("wrapped: %w", multiLineError{})), "printMultiLineError()") + assert.Equal(t, "Error:\nline one\nline two\n", stderr.String(), "stderr") + assert.True(t, cmd.SilenceErrors, "SilenceErrors, want cobra's inline error silenced") }) t.Run("any other error", func(t *testing.T) { var stderr bytes.Buffer cmd := &cobra.Command{} cmd.SetErr(&stderr) - if printMultiLineError(cmd, errors.New("plain")) { - t.Fatal("printMultiLineError() = true, want false") - } - if stderr.Len() != 0 || cmd.SilenceErrors { - t.Errorf("stderr = %q, SilenceErrors = %v, want untouched", stderr.String(), cmd.SilenceErrors) - } + require.False(t, printMultiLineError(cmd, errors.New("plain")), "printMultiLineError()") + assert.Empty(t, stderr.String(), "stderr, want untouched") + assert.False(t, cmd.SilenceErrors, "SilenceErrors, want untouched") }) } @@ -89,24 +80,16 @@ func TestCheckFailure(t *testing.T) { cmd := &cobra.Command{} cmd.SetErr(&stderr) in := multiLineError{} - if got := checkFailure(cmd, in); got != error(in) { - t.Errorf("checkFailure() = %v, want the error it was given", got) - } - if want := "Error:\nline one\nline two\n"; stderr.String() != want { - t.Errorf("stderr = %q, want %q", stderr.String(), want) - } + assert.Equal(t, error(in), checkFailure(cmd, in), "checkFailure() want the error it was given") + assert.Equal(t, "Error:\nline one\nline two\n", stderr.String(), "stderr") }) t.Run("any other error is a fail line", func(t *testing.T) { var stderr bytes.Buffer cmd := &cobra.Command{} cmd.SetErr(&stderr) got := checkFailure(cmd, errors.New("no such variable")) - if want := "fail: no such variable"; got == nil || got.Error() != want { - t.Errorf("checkFailure() = %v, want %q", got, want) - } - if stderr.Len() != 0 { - t.Errorf("stderr = %q, want nothing", stderr.String()) - } + assert.EqualError(t, got, "fail: no such variable", "checkFailure()") + assert.Empty(t, stderr.String(), "stderr, want nothing") }) } @@ -133,56 +116,54 @@ func TestWriteResultFormats(t *testing.T) { var buf bytes.Buffer err := writeResult(newCmd(tt.format), &buf, textResult("hello")) if tt.wantErr != "" { - if err == nil || err.Error() != tt.wantErr { - t.Fatalf("writeResult() error = %v, want %q", err, tt.wantErr) - } + require.EqualError(t, err, tt.wantErr, "writeResult()") return } - if err != nil || buf.String() != tt.want { - t.Fatalf("writeResult() = %q, %v, want %q", buf.String(), err, tt.want) - } + require.NoError(t, err, "writeResult()") + require.Equal(t, tt.want, buf.String(), "writeResult()") }) } } func TestVersionCommand(t *testing.T) { for _, args := range [][]string{{"version"}, {"v"}, {"version", "--verbose"}} { - var stdout bytes.Buffer - err := Commands(t.TempDir(), args, func(string) string { return "" }, &stdout, io.Discard) - v, ok := cliVersion() - switch { - case !ok && (err == nil || err.Error() != "missing CLI version"): - t.Errorf("muxt %v = %v, want the missing CLI version error", args, err) - case ok && !strings.HasPrefix(stdout.String(), v+"\n"): - t.Errorf("muxt %v printed %q, want it to start with %q", args, stdout.String(), v+"\n") - } + t.Run(strings.Join(args, " "), func(t *testing.T) { + var stdout bytes.Buffer + err := Commands(t.TempDir(), args, func(string) string { return "" }, &stdout, io.Discard) + v, ok := cliVersion() + if !ok { + assert.EqualError(t, err, "missing CLI version", "muxt %v, want the missing CLI version error", args) + return + } + assert.True(t, strings.HasPrefix(stdout.String(), v+"\n"), "muxt %v printed %q, want it to start with %q", args, stdout.String(), v+"\n") + }) } } func TestCommandsAreWired(t *testing.T) { + help := func(t *testing.T, arg string) string { + t.Helper() + var stdout bytes.Buffer + err := Commands(t.TempDir(), []string{arg, "--help"}, func(string) string { return "" }, &stdout, io.Discard) + assert.NoError(t, err, "muxt %s --help", arg) + return stdout.String() + } + for _, name := range []string{ generateCommandName, versionCommandName, checkCommandName, listTemplateCallersCommandName, listTemplateCallsCommandName, exploreModuleCommandName, generateFakeServerCommandName, testTemplateMutationsName, } { - var stdout bytes.Buffer - if err := Commands(t.TempDir(), []string{name, "--help"}, func(string) string { return "" }, &stdout, io.Discard); err != nil { - t.Errorf("muxt %s --help error = %v", name, err) - } - if !strings.Contains(stdout.String(), "muxt "+name) { - t.Errorf("muxt %s --help = %q, want its usage", name, stdout.String()) - } + t.Run(name, func(t *testing.T) { + assert.Contains(t, help(t, name), "muxt "+name, "muxt %s --help want its usage", name) + }) } for alias, name := range map[string]string{ "g": generateCommandName, "gen": generateCommandName, "v": versionCommandName, "c": checkCommandName, "callers": listTemplateCallersCommandName, "calls": listTemplateCallsCommandName, "explore": exploreModuleCommandName, } { - var stdout bytes.Buffer - if err := Commands(t.TempDir(), []string{alias, "--help"}, func(string) string { return "" }, &stdout, io.Discard); err != nil { - t.Errorf("muxt %s --help error = %v", alias, err) - } - if !strings.Contains(stdout.String(), "muxt "+name) { - t.Errorf("muxt %s --help = %q, want the usage of %s", alias, stdout.String(), name) - } + t.Run("alias "+alias, func(t *testing.T) { + assert.Contains(t, help(t, alias), "muxt "+name, "muxt %s --help want the usage of %s", alias, name) + }) } } diff --git a/internal/cli/run_test.go b/internal/cli/run_test.go index d77d3b3f..bc3c0745 100644 --- a/internal/cli/run_test.go +++ b/internal/cli/run_test.go @@ -7,6 +7,9 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/typelate/muxt/internal/header" ) @@ -50,69 +53,49 @@ func execute(t *testing.T, wd string, args ...string) (stdout, stderr string, er return out.String(), errOut.String(), err } -func exists(path string) bool { - _, err := os.Stat(path) - return err == nil -} - +// TestRunGenerate runs generate twice in one module: the second run sees +// the files the first wrote, and removes the ones a renamed template left +// behind. func TestRunGenerate(t *testing.T) { wd := newModule(t) - stdout, _, err := execute(t, wd, "generate", "--output-multiple-files") - if err != nil { - t.Fatalf("generate error = %v", err) - } - for _, want := range []string{"wrote old_template_routes_gen.go: 1 route\n", "wrote template_routes.go: 0 routes\n"} { - if !strings.Contains(stdout, want) { - t.Errorf("generate output = %q, want containing %q", stdout, want) + t.Run("the first run writes a file per template file", func(t *testing.T) { + stdout, _, err := execute(t, wd, "generate", "--output-multiple-files") + require.NoError(t, err, "generate") + for _, want := range []string{"wrote old_template_routes_gen.go: 1 route\n", "wrote template_routes.go: 0 routes\n"} { + assert.Contains(t, stdout, want, "generate output") } - } - generated, err := header.Scan(wd) - if err != nil { - t.Fatal(err) - } - if h, ok := generated[filepath.Join(wd, "template_routes.go")]; !ok || h.Args()[0] != "--output-multiple-files" { - t.Errorf("template_routes.go header = %+v (found %v), want it to record --output-multiple-files", h, ok) - } + generated, err := header.Scan(wd) + require.NoError(t, err) + h, ok := generated[filepath.Join(wd, "template_routes.go")] + if assert.True(t, ok, "template_routes.go has a header") { + assert.Equal(t, "--output-multiple-files", h.Args()[0], "template_routes.go header = %+v, want it to record --output-multiple-files", h) + } + }) unreadable := writeTestFile(t, wd, "unreadable.go", header.Format([]string{"--no-such-flag"}, "")+"package main\n") otherRoutes := writeTestFile(t, wd, "other.go", header.Format([]string{"--output-routes-func=AdminRoutes"}, "")+"package main\n") - if err := os.Rename(filepath.Join(wd, "old.gohtml"), filepath.Join(wd, "new.gohtml")); err != nil { - t.Fatal(err) - } - _, stderr, err := execute(t, wd, "generate", "--output-multiple-files") - if err != nil { - t.Fatalf("second generate error = %v", err) - } - if exists(filepath.Join(wd, "old_template_routes_gen.go")) { - t.Error("old_template_routes_gen.go survived the template rename") - } - if !exists(filepath.Join(wd, "new_template_routes_gen.go")) { - t.Error("new_template_routes_gen.go was not written") - } - for _, kept := range []string{unreadable, otherRoutes} { - if !exists(kept) { - t.Errorf("%s was deleted, want it left alone", kept) + t.Run("the second run after a template rename", func(t *testing.T) { + require.NoError(t, os.Rename(filepath.Join(wd, "old.gohtml"), filepath.Join(wd, "new.gohtml"))) + _, stderr, err := execute(t, wd, "generate", "--output-multiple-files") + require.NoError(t, err, "second generate") + assert.NoFileExists(t, filepath.Join(wd, "old_template_routes_gen.go"), "old_template_routes_gen.go survived the template rename") + assert.FileExists(t, filepath.Join(wd, "new_template_routes_gen.go"), "new_template_routes_gen.go was not written") + for _, kept := range []string{unreadable, otherRoutes} { + assert.FileExists(t, kept, "%s was deleted, want it left alone", kept) } - } - if want := "WARNING: ignored generated file " + unreadable; !strings.Contains(stderr, want) { - t.Errorf("stderr = %q, want containing %q", stderr, want) - } + assert.Contains(t, stderr, "WARNING: ignored generated file "+unreadable, "stderr") + }) } func TestRunCheck(t *testing.T) { wd := newModule(t) - if _, _, err := execute(t, wd, "generate"); err != nil { - t.Fatalf("generate error = %v", err) - } + _, _, err := execute(t, wd, "generate") + require.NoError(t, err, "generate") stdout, _, err := execute(t, wd, "check") - if err != nil { - t.Fatalf("check error = %v", err) - } - if want := "ok: 2 templates\n"; stdout != want { - t.Errorf("check output = %q, want %q", stdout, want) - } + require.NoError(t, err, "check") + assert.Equal(t, "ok: 2 templates\n", stdout, "check output") } func TestRunListings(t *testing.T) { @@ -128,12 +111,8 @@ func TestRunListings(t *testing.T) { } { t.Run(tt.name, func(t *testing.T) { stdout, _, err := execute(t, wd, tt.args...) - if err != nil { - t.Fatalf("muxt %v error = %v", tt.args, err) - } - if !strings.Contains(stdout, tt.want) { - t.Errorf("muxt %v output = %q, want containing %q", tt.args, stdout, tt.want) - } + require.NoError(t, err, "muxt %v", tt.args) + assert.Contains(t, stdout, tt.want, "muxt %v output", tt.args) }) } } @@ -141,16 +120,18 @@ func TestRunListings(t *testing.T) { func TestRunListingsRejectAnUnknownFormat(t *testing.T) { wd := newModule(t) for _, args := range [][]string{{"--format=yaml"}, {"list-template-callers", "--format=yaml"}, {"list-template-calls", "--format=yaml"}} { - if _, _, err := execute(t, wd, args...); err == nil || err.Error() != "unknown format: yaml" { - t.Errorf("muxt %v error = %v, want unknown format: yaml", args, err) - } + t.Run(strings.Join(args, " "), func(t *testing.T) { + _, _, err := execute(t, wd, args...) + assert.EqualError(t, err, "unknown format: yaml", "muxt %v", args) + }) } } func TestRunFailsOutsideAModule(t *testing.T) { for _, args := range [][]string{{"check"}, {"generate"}, {"list-template-callers"}, {testTemplateMutationsName}} { - if _, _, err := execute(t, t.TempDir(), args...); err == nil { - t.Errorf("muxt %v outside a module = nil error, want one", args) - } + t.Run(strings.Join(args, " "), func(t *testing.T) { + _, _, err := execute(t, t.TempDir(), args...) + assert.Error(t, err, "muxt %v outside a module", args) + }) } }