From eaaa873fd1aea5f3dd8e6755b7fdabecc565da95 Mon Sep 17 00:00:00 2001 From: Tim Ebert Date: Wed, 24 Apr 2024 13:24:59 +0200 Subject: [PATCH] Fix linking submodule of go workspace (#27) --- internal/link/link.go | 13 ++++- internal/link/link_test.go | 117 ++++++++++++++++++++++++++++++++++--- 2 files changed, 121 insertions(+), 9 deletions(-) diff --git a/internal/link/link.go b/internal/link/link.go index b73b19b..1c32fd5 100644 --- a/internal/link/link.go +++ b/internal/link/link.go @@ -12,8 +12,9 @@ import ( "sort" "strings" - "github.com/ironcore-dev/vgopath/internal/module" "github.com/spf13/pflag" + + "github.com/ironcore-dev/vgopath/internal/module" ) type Node struct { @@ -261,7 +262,17 @@ func linkNode(dir string, node Node) error { return err } + childNames := make(map[string]struct{}, len(node.Children)) + for _, child := range node.Children { + childNames[child.Segment] = struct{}{} + } + for _, entry := range entries { + // skip linking directories of the module hierarchy, they will be handled by a dedicated call + if _, ok := childNames[entry.Name()]; ok { + continue + } + srcPath := filepath.Join(srcDir, entry.Name()) dstPath := filepath.Join(dstDir, entry.Name()) if err := os.Symlink(srcPath, dstPath); err != nil { diff --git a/internal/link/link_test.go b/internal/link/link_test.go index 00031b6..8cade49 100644 --- a/internal/link/link_test.go +++ b/internal/link/link_test.go @@ -8,52 +8,64 @@ import ( "go/build" "os" "path/filepath" + "slices" - . "github.com/ironcore-dev/vgopath/internal/link" - "github.com/ironcore-dev/vgopath/internal/module" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/onsi/gomega/format" "github.com/onsi/gomega/types" + + . "github.com/ironcore-dev/vgopath/internal/link" + "github.com/ironcore-dev/vgopath/internal/module" ) var _ = Describe("Internal", func() { var ( tmpDir string moduleA, moduleB, moduleB1, moduleB11, moduleB2, moduleC, moduleD module.Module + allModules []module.Module ) BeforeEach(func() { var err error tmpDir, err = os.MkdirTemp("", "test") Expect(err).NotTo(HaveOccurred()) + allModules = []module.Module{} moduleA = module.Module{ Path: "a", - Dir: "/tmp/a", + Dir: filepath.Join("a"), Main: true, } + allModules = append(allModules, moduleA) moduleB = module.Module{ Path: "example.org/b", - Dir: "/tmp/example.org/b", + Dir: filepath.Join("example.org", "b"), } + allModules = append(allModules, moduleB) moduleB1 = module.Module{ Path: "example.org/b/1", - Dir: "/tmp/example.org/b/1", + Dir: filepath.Join("example.org", "b", "1"), } + allModules = append(allModules, moduleB1) moduleB11 = module.Module{ Path: "example.org/b/1/1", - Dir: "/tmp/example.org/b/1/1", + Dir: filepath.Join("example.org", "b", "1", "1"), } + allModules = append(allModules, moduleB11) moduleB2 = module.Module{ Path: "example.org/b/2", - Dir: "/tmp/example.org/b/2", + Dir: filepath.Join("example.org", "b", "2"), } + allModules = append(allModules, moduleB2) moduleC = module.Module{ Path: "example.org/user/c", - Dir: "/tmp/example.org/user/c", + Dir: filepath.Join("example.org", "user", "c"), } + allModules = append(allModules, moduleC) moduleD = module.Module{ Path: "example.org/d", } + allModules = append(allModules, moduleD) }) AfterEach(func() { if tmpDir != "" { @@ -138,6 +150,28 @@ var _ = Describe("Internal", func() { Expect(os.MkdirAll(dstGopathDir, 0777)).To(Succeed()) }) + Describe("Nodes", func() { + It("should correctly handle submodules", func() { + Expect(makeModules(srcGopathDir, &moduleB, &moduleB1, &moduleB11, &moduleB2)).NotTo(HaveOccurred()) + + nodes, err := BuildModuleNodes([]module.Module{moduleB, moduleB1, moduleB11, moduleB2}) + Expect(err).NotTo(HaveOccurred()) + + Expect(Nodes(dstGopathDir, nodes)).To(Succeed()) + + Expect(dstGopathDir).To(HaveEntries(map[string]types.GomegaMatcher{ + filepath.Join("example.org", "b"): BeADirectory(), + filepath.Join("example.org", "b", "go.mod"): BeASymlinkTo(filepath.Join(moduleB.Dir, "go.mod")), + filepath.Join("example.org", "b", "1"): BeADirectory(), + filepath.Join("example.org", "b", "1", "go.mod"): BeASymlinkTo(filepath.Join(moduleB1.Dir, "go.mod")), + filepath.Join("example.org", "b", "1", "1"): BeADirectory(), + filepath.Join("example.org", "b", "1", "1", "go.mod"): BeASymlinkTo(filepath.Join(moduleB11.Dir, "go.mod")), + filepath.Join("example.org", "b", "2"): BeADirectory(), + filepath.Join("example.org", "b", "2", "go.mod"): BeASymlinkTo(filepath.Join(moduleB2.Dir, "go.mod")), + })) + }) + }) + Describe("GoBin", func() { var ( srcGoBinDir string @@ -190,6 +224,73 @@ var _ = Describe("Internal", func() { }) }) +func makeModules(gopath string, mods ...*module.Module) error { + for _, mod := range mods { + // update dir to include gopath prefix + mod.Dir = filepath.Join(gopath, mod.Dir) + + if err := os.MkdirAll(mod.Dir, 0777); err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(mod.Dir, "go.mod"), []byte("module "+mod.Path+"\n"), 0666); err != nil { + return err + } + } + return nil +} + +func HaveEntries(expected map[string]types.GomegaMatcher) types.GomegaMatcher { + return &haveEntriesMatcher{matchers: expected} +} + +// haveEntriesMatcher is very similar to matchers.AndMatcher. +type haveEntriesMatcher struct { + matchers map[string]types.GomegaMatcher + + // state + baseDir string + firstFailedFilename string +} + +func (m *haveEntriesMatcher) Match(actual interface{}) (success bool, err error) { + m.firstFailedFilename = "" + + var ok bool + m.baseDir, ok = actual.(string) + if !ok { + return false, fmt.Errorf("HaveEntries matcher expects a string but got %T", actual) + } + + // sort matchers by filename for stable test results even though maps are unsorted + filenames := make([]string, 0, len(m.matchers)) + for filename := range m.matchers { + filenames = append(filenames, filename) + } + slices.Sort(filenames) + + for _, filename := range filenames { + matcher := m.matchers[filename] + + success, err := matcher.Match(filepath.Join(m.baseDir, filename)) + if !success || err != nil { + m.firstFailedFilename = filename + return false, err + } + } + + return true, nil +} + +func (m *haveEntriesMatcher) FailureMessage(actual interface{}) (message string) { + return m.matchers[m.firstFailedFilename].FailureMessage(filepath.Join(m.baseDir, m.firstFailedFilename)) +} + +func (m *haveEntriesMatcher) NegatedFailureMessage(actual interface{}) (message string) { + // not the most beautiful list of matchers, but not bad either... + return format.Message(actual, "not to have these entries: %s", m.matchers) +} + func BeASymlinkTo(filename string) types.GomegaMatcher { return &beASymlinkToMatcher{filename} }