From 4bd70e7768028117b5b86187e468426ca98728a2 Mon Sep 17 00:00:00 2001 From: Bart Van Bos Date: Sat, 7 Nov 2026 12:05:50 +0100 Subject: [PATCH] test: cover the changed lines of goapi, code and api checks Add goapi tests for Load's recording of dependency imports (version suffix, alias, blank and dot imports, dedup) and test functions (external test packages, receivers, unparsable files), Index.IsTest and ExportedNames. Add check unit tests for bestPackage, hasAny, isInternal and stripSpans. Co-Authored-By: Claude Opus 5.5 --- internal/check/unit_test.go | 82 ++++++++++++++++ internal/goapi/goapi_test.go | 176 +++++++++++++++++++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 internal/goapi/goapi_test.go diff --git a/internal/check/unit_test.go b/internal/check/unit_test.go index 5d51061..8efa493 100644 --- a/internal/check/unit_test.go +++ b/internal/check/unit_test.go @@ -6,6 +6,8 @@ import ( "slices" "strings" "testing" + + "github.com/otfabric/docsync/internal/goapi" ) func TestShellWords(t *testing.T) { @@ -117,3 +119,83 @@ func TestNameListings(t *testing.T) { t.Errorf("got %q", got) } } + +func TestBestPackage(t *testing.T) { + pkg := func(path string, names ...string) *goapi.Package { + p := &goapi.Package{ImportPath: path, Funcs: map[string]*goapi.Func{}} + for _, n := range names { + p.Funcs[n] = &goapi.Func{Name: n} + } + return p + } + for _, tc := range []struct { + name string + pkgs []*goapi.Package + names []string + want string // import path, "" for none + }{ + {"none", nil, []string{"New"}, ""}, + {"more names win over a shorter path", []*goapi.Package{pkg("m/errors"), pkg("m/a/b/errors", "New")}, []string{"New"}, "m/a/b/errors"}, + {"fewer names lose", []*goapi.Package{pkg("m/a/b/errors", "New"), pkg("m/errors")}, []string{"New"}, "m/a/b/errors"}, + {"public over internal", []*goapi.Package{pkg("m/internal/errors", "New"), pkg("m/x/errors", "New")}, []string{"New"}, "m/x/errors"}, + {"internal after public loses", []*goapi.Package{pkg("m/x/errors", "New"), pkg("m/internal/errors", "New")}, []string{"New"}, "m/x/errors"}, + {"a directory named internal", []*goapi.Package{pkg("m/x/internal", "New"), pkg("m/long/path/internal2", "New")}, []string{"New"}, "m/long/path/internal2"}, + {"shorter path on a tie", []*goapi.Package{pkg("m/a/b/errors", "New"), pkg("m/errors", "New")}, []string{"New"}, "m/errors"}, + {"longer path on a tie loses", []*goapi.Package{pkg("m/errors"), pkg("m/a/errors")}, nil, "m/errors"}, + } { + got := "" + if p := bestPackage(tc.pkgs, tc.names); p != nil { + got = p.ImportPath + } + if got != tc.want { + t.Errorf("%s: got %q, want %q", tc.name, got, tc.want) + } + } +} + +func TestHasAny(t *testing.T) { + p := &goapi.Package{Funcs: map[string]*goapi.Func{"New": {Name: "New"}}} + for _, tc := range []struct { + names []string + want bool + }{ + {nil, false}, + {[]string{"Wrap"}, false}, + {[]string{"Wrap", "New"}, true}, + } { + if got := hasAny(p, tc.names); got != tc.want { + t.Errorf("hasAny(%q) = %v, want %v", tc.names, got, tc.want) + } + } +} + +func TestIsInternal(t *testing.T) { + for path, want := range map[string]bool{ + "m/internal": true, + "m/internal/wire": true, + "m/x/internal/y": true, + "m/internalwire": false, + "m/transport/wire": false, + "internal": false, + } { + if got := isInternal(path); got != want { + t.Errorf("isInternal(%q) = %v, want %v", path, got, want) + } + } +} + +func TestStripSpans(t *testing.T) { + for in, want := range map[string]string{ + "no code": "no code", + "a `ErrX` b": "a b", + "a ``x ` y`` b": "a " + strings.Repeat(" ", len("``x ` y``")) + " b", + "open ` ErrX": "open ` ErrX", + "`a` and ``open ErrX": " and ``open ErrX", + "``` ErrX": "``` ErrX", + "`a``b` c": " c", + } { + if got := stripSpans(in); got != want { + t.Errorf("stripSpans(%q) = %q, want %q", in, got, want) + } + } +} diff --git a/internal/goapi/goapi_test.go b/internal/goapi/goapi_test.go new file mode 100644 index 0000000..9dc3697 --- /dev/null +++ b/internal/goapi/goapi_test.go @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: MIT + +package goapi + +import ( + "maps" + "os" + "path/filepath" + "slices" + "testing" +) + +// writeFiles writes name -> content under dir. +func writeFiles(t *testing.T, dir string, files map[string]string) { + t.Helper() + for name, content := range files { + p := filepath.Join(dir, filepath.FromSlash(name)) + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(p, []byte(content), 0o600); err != nil { + t.Fatal(err) + } + } +} + +func TestLoadImportsAndTests(t *testing.T) { + dir := t.TempDir() + writeFiles(t, dir, map[string]string{ + "go.mod": "module example.com/m\n\ngo 1.22\n", + "m.go": `package m + +import ( + "fmt" + + "example.com/m/sub" + x "github.com/dep/x/v2" + _ "github.com/dep/blank" + . "github.com/dep/dot" + "github.com/dep/v2x" + "github.com/dep/v" +) + +var _ = fmt.Sprint(sub.A, x.B) +`, + "n.go": `package m + +import "github.com/dep/x/v2" +`, + "sub/sub.go": "package sub\n\nconst A = 1\n", + "m_test.go": `package m + +import ( + "testing" + + "github.com/dep/testonly" +) + +type T struct{} + +func (T) TestMethod(t *testing.T) {} +func TestA(t *testing.T) {} +func Test_b(t *testing.T) {} +func Testify() {} +func FuzzB(f *testing.F) {} +func BenchmarkC(b *testing.B) {} +func ExampleD() {} +func Example() {} +func helper() {} + +var _ = testonly.X +`, + "ext_test.go": `package m_test + +import "testing" + +func TestExt(t *testing.T) {} +`, + "bad_test.go": "package m\n\nfunc TestBroken( {\n", + }) + m, err := Load(dir) + if err != nil { + t.Fatal(err) + } + + wantImports := map[string][]string{ + "x": {"github.com/dep/x/v2"}, // the path's v2 element is skipped; the alias is the same name + "blank": {"github.com/dep/blank"}, + "dot": {"github.com/dep/dot"}, + "v2x": {"github.com/dep/v2x"}, + "v": {"github.com/dep/v"}, + "testonly": {"github.com/dep/testonly"}, + } + if !maps.EqualFunc(m.Imports, wantImports, slices.Equal) { + t.Errorf("Imports = %v, want %v", m.Imports, wantImports) + } + + wantTests := map[string]map[string]bool{ + "m": {"TestA": true, "Test_b": true, "FuzzB": true, "BenchmarkC": true, "ExampleD": true, "Example": true, "TestExt": true}, + } + if !maps.EqualFunc(m.Tests, wantTests, maps.Equal) { + t.Errorf("Tests = %v, want %v", m.Tests, wantTests) + } + + ix := NewIndex(m) + for _, tc := range []struct { + pkg, name string + want bool + }{ + {"m", "TestA", true}, + {"m", "TestExt", true}, + {"m", "helper", false}, + {"m", "TestMethod", false}, + {"other", "TestA", false}, + } { + if got := ix.IsTest(tc.pkg, tc.name); got != tc.want { + t.Errorf("IsTest(%q, %q) = %v, want %v", tc.pkg, tc.name, got, tc.want) + } + } + if got := ix.Imports["x"]; !slices.Equal(got, []string{"github.com/dep/x/v2"}) { + t.Errorf("Index.Imports[x] = %v", got) + } +} + +func TestLoadAlias(t *testing.T) { + dir := t.TempDir() + writeFiles(t, dir, map[string]string{ + "go.mod": "module example.com/m\n", + "m.go": "package m\n\nimport y \"github.com/dep/x\"\n\nvar _ = y.A\n", + }) + m, err := Load(dir) + if err != nil { + t.Fatal(err) + } + want := map[string][]string{"x": {"github.com/dep/x"}, "y": {"github.com/dep/x"}} + if !maps.EqualFunc(m.Imports, want, slices.Equal) { + t.Errorf("Imports = %v, want %v", m.Imports, want) + } +} + +func TestExportedNames(t *testing.T) { + dir := t.TempDir() + writeFiles(t, dir, map[string]string{ + "a.go": `package dep + +const C = 1 + +var V, unexported = 1, 2 + +func F() {} +func f() {} + +type T struct{ Field int } + +func (T) M() {} + +type I interface{ IM() } +`, + "a_test.go": "package dep\n\nfunc TestOnly() {}\n\nvar X = 1\n", + "bad.go": "package dep\n\nfunc Broken( {\n", + }) + got := ExportedNames(dir) + for _, n := range []string{"C", "V", "F", "T", "M", "I", "IM"} { + if !got[n] { + t.Errorf("%s is missing from %v", n, got) + } + } + for _, n := range []string{"unexported", "f", "TestOnly", "X", "Broken"} { + if got[n] { + t.Errorf("%s is in %v", n, got) + } + } + if got := ExportedNames(filepath.Join(dir, "missing")); len(got) != 0 { + t.Errorf("a missing directory: %v", got) + } +}