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) + } +}