From d6245dd0bc3c936b88772363cb9b69ae931f3a40 Mon Sep 17 00:00:00 2001 From: plastikfan Date: Wed, 9 Oct 2024 13:16:07 +0100 Subject: [PATCH] ref: replace extendio storage with traverse.fs (#216) --- .vscode/settings.json | 3 + go.mod | 8 +- go.sum | 12 +- src/app/cfg/cfg-suite_test.go | 5 + src/app/cfg/config-runner.go | 16 +- src/app/cfg/config-runner_test.go | 35 +- src/app/command/bootstrap.go | 3 + src/app/command/bootstrap_test.go | 17 +- src/app/command/magick-cmd_test.go | 14 +- src/app/command/root-cmd_test.go | 14 +- src/app/command/shrink-cmd_test.go | 37 +- src/app/proxy/filing/path-finder_test.go | 11 +- src/app/proxy/pixa-legacy_test.go | 21 +- src/app/proxy/pixa_test.go | 17 +- src/internal/laboratory/assert.go | 111 ++++++ src/internal/laboratory/callbacks.go | 93 +++++ .../laboratory/directory-tree-builder.go | 335 ++++++++++++++++++ src/internal/laboratory/helper-defs.go | 83 +++++ src/internal/laboratory/matchers.go | 255 +++++++++++++ src/internal/laboratory/test-utilities.go | 208 +++++++++++ src/internal/laboratory/traverse-fs.go | 94 +++++ 21 files changed, 1318 insertions(+), 74 deletions(-) create mode 100644 src/internal/laboratory/assert.go create mode 100644 src/internal/laboratory/callbacks.go create mode 100644 src/internal/laboratory/directory-tree-builder.go create mode 100644 src/internal/laboratory/helper-defs.go create mode 100644 src/internal/laboratory/matchers.go create mode 100644 src/internal/laboratory/test-utilities.go create mode 100644 src/internal/laboratory/traverse-fs.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 323e879..8d5da6e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -36,6 +36,7 @@ "faydeaudeau", "fieldalignment", "flif", + "fsys", "GOARCH", "goconst", "gocritic", @@ -66,6 +67,7 @@ "mockgen", "modcache", "mohae", + "Musico", "nakedret", "natefinch", "navi", @@ -92,6 +94,7 @@ "thelper", "toplevel", "tparallel", + "tsys", "typecheck", "unconvert", "unparam", diff --git a/go.mod b/go.mod index 7ca4c16..772b66e 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/charmbracelet/bubbletea v1.1.1 github.com/charmbracelet/lipgloss v0.13.0 github.com/muesli/go-app-paths v0.2.2 + github.com/onsi/ginkgo v1.16.5 github.com/onsi/ginkgo/v2 v2.20.2 github.com/onsi/gomega v1.34.2 github.com/pkg/errors v0.9.1 @@ -13,12 +14,14 @@ require ( github.com/snivilised/extendio v0.7.0 github.com/snivilised/li18ngo v0.1.4 github.com/snivilised/lorax v0.5.2 + github.com/snivilised/pants v0.1.2 + github.com/snivilised/traverse v0.1.2 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.18.2 go.uber.org/mock v0.4.0 go.uber.org/zap v1.27.0 go.uber.org/zap/exp v0.2.0 - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 + golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa ) require ( @@ -41,7 +44,7 @@ require ( github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.15.2 // indirect - github.com/onsi/ginkgo v1.16.5 // indirect + github.com/nxadm/tail v1.4.8 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect @@ -50,6 +53,7 @@ require ( golang.org/x/sync v0.8.0 // indirect golang.org/x/tools v0.24.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect ) require ( diff --git a/go.sum b/go.sum index c685d22..21668a9 100644 --- a/go.sum +++ b/go.sum @@ -112,8 +112,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= @@ -129,6 +129,10 @@ github.com/snivilised/li18ngo v0.1.4 h1:y6EECoVFpmkud2yDEeBnMnebPmSvdrEZ/LAq1PoP github.com/snivilised/li18ngo v0.1.4/go.mod h1:Or3qUhpR6AM1X51i82RtyCvORWy2/hrxY9lg1i1gFTE= github.com/snivilised/lorax v0.5.2 h1:iReIJl63tydiPSSD0YzsNQFX1CctmvMkYx0aSxoZJKo= github.com/snivilised/lorax v0.5.2/go.mod h1:7H1JPgSn4h4p8NSqfl64raacYefdm/FiFkfcZ51PVHY= +github.com/snivilised/pants v0.1.2 h1:6Abj02gV5rFYyKfCsmeEiOi1pLdRyITKUY5oDoRgYuU= +github.com/snivilised/pants v0.1.2/go.mod h1:BOZa24yLxVjjnTCFWQeCzUWL8eK4TLtXtkz3pMdEFQM= +github.com/snivilised/traverse v0.1.2 h1:cg0AtPAu40Us+sKhVtIi+q9e0SpovfM2dXz2fM0FHe8= +github.com/snivilised/traverse v0.1.2/go.mod h1:yDe4oJLGLPvC0BGob1UlXRjPZ0bsPVl0HDl9PhGEjVw= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -165,8 +169,8 @@ go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= diff --git a/src/app/cfg/cfg-suite_test.go b/src/app/cfg/cfg-suite_test.go index 1a21afa..0edb292 100644 --- a/src/app/cfg/cfg-suite_test.go +++ b/src/app/cfg/cfg-suite_test.go @@ -11,3 +11,8 @@ func TestCfg(t *testing.T) { gomega.RegisterFailHandler(ginkgo.Fail) ginkgo.RunSpecs(t, "Cfg Suite") } + +const ( + NoOverwrite = false + home = "/home" +) diff --git a/src/app/cfg/config-runner.go b/src/app/cfg/config-runner.go index 702e4df..2940f67 100644 --- a/src/app/cfg/config-runner.go +++ b/src/app/cfg/config-runner.go @@ -14,6 +14,7 @@ import ( "github.com/snivilised/extendio/xfs/storage" "github.com/snivilised/li18ngo" "github.com/snivilised/pixa/src/app/proxy/common" + "github.com/snivilised/traverse/lfs" "github.com/spf13/viper" "golang.org/x/text/language" ) @@ -35,7 +36,8 @@ func New( ci *common.ConfigInfo, sourceID string, applicationName string, - vfs storage.VirtualFS, + tfs lfs.TraverseFS, + vfsL storage.VirtualFS, ) (common.ConfigRunner, error) { home, err := os.UserHomeDir() @@ -45,7 +47,8 @@ func New( sourceID: sourceID, applicationName: applicationName, home: home, - vfs: vfs, + tfs: tfs, + vfsL: vfsL, useXDG: common.IsUsingXDG(ci.Viper), }, err } @@ -56,7 +59,8 @@ type configRunner struct { sourceID string applicationName string home string - vfs storage.VirtualFS + tfs lfs.TraverseFS + vfsL storage.VirtualFS useXDG bool } @@ -191,12 +195,12 @@ func (c *configRunner) export() error { file := filepath.Join(path, common.Definitions.Pixa.ConfigType) content := []byte(defaultConfig) - if !c.vfs.FileExists(file) { - if err := c.vfs.MkdirAll(path, common.Permissions.Write); err != nil { + if !c.vfsL.FileExists(file) { + if err := c.vfsL.MkdirAll(path, common.Permissions.Write); err != nil { return err } - return c.vfs.WriteFile(file, content, common.Permissions.Write) + return c.vfsL.WriteFile(file, content, common.Permissions.Write) } return nil diff --git a/src/app/cfg/config-runner_test.go b/src/app/cfg/config-runner_test.go index 3502f59..38f6c90 100644 --- a/src/app/cfg/config-runner_test.go +++ b/src/app/cfg/config-runner_test.go @@ -3,19 +3,22 @@ package cfg_test import ( "errors" "fmt" + "os" "path/filepath" + "testing/fstest" . "github.com/onsi/ginkgo/v2" //nolint:revive // foo . "github.com/onsi/gomega" //nolint:revive // foo - "github.com/spf13/viper" - "go.uber.org/mock/gomock" - "github.com/snivilised/cobrass/src/assistant/mocks" "github.com/snivilised/extendio/xfs/storage" - "github.com/snivilised/li18ngo" + _ "github.com/snivilised/pants" "github.com/snivilised/pixa/src/app/cfg" "github.com/snivilised/pixa/src/app/proxy/common" "github.com/snivilised/pixa/src/internal/helpers" + lab "github.com/snivilised/pixa/src/internal/laboratory" + "github.com/snivilised/traverse/lfs" + "github.com/spf13/viper" + "go.uber.org/mock/gomock" ) var ( @@ -63,22 +66,32 @@ var _ = Describe("ConfigRunner", Ordered, func() { var ( repo string configPath string - vfs storage.VirtualFS + vfsL storage.VirtualFS + FS lfs.TraverseFS ctrl *gomock.Controller mock *mocks.MockViperConfig ) BeforeAll(func() { - Expect(li18ngo.Use()).To(Succeed()) + repo = helpers.Repo("") + Expect(lab.UseI18n(lab.Path(repo, "test/data/l10n"))).To(Succeed()) + _ = FS }) BeforeEach(func() { viper.Reset() - vfs = storage.UseMemFS() + vfsL = storage.UseMemFS() + FS = &lab.TestTraverseFS{ + MapFS: fstest.MapFS{ + home: &fstest.MapFile{ + Mode: os.ModeDir, + }, + }, + } ctrl = gomock.NewController(GinkgoT()) mock = mocks.NewMockViperConfig(ctrl) - repo = helpers.Repo("") - configPath = helpers.Path(repo, "test/data/configuration") + + configPath = lab.Path(repo, "test/data/configuration") }) AfterEach(func() { @@ -107,7 +120,7 @@ var _ = Describe("ConfigRunner", Ordered, func() { mock.EXPECT().InConfig(gomock.Any()).AnyTimes() mock.EXPECT().GetString(gomock.Any()).AnyTimes() - runner, err := cfg.New(&ci, sourceID, common.Definitions.Pixa.AppName, vfs) + runner, err := cfg.New(&ci, sourceID, common.Definitions.Pixa.AppName, FS, vfsL) if entry.created != nil { entry.created(entry, runner) } @@ -192,7 +205,7 @@ var _ = Describe("ConfigRunner", Ordered, func() { path := filepath.Join(runner.DefaultPath(), name) content := []byte(cfg.GetDefaultConfigContent()) - _ = vfs.WriteFile(path, content, common.Permissions.Write) + _ = vfsL.WriteFile(path, content, common.Permissions.Write) }, assert: func(_ *runnerTE, runner common.ConfigRunner, err error) { Expect(err).Error().To(BeNil()) diff --git a/src/app/command/bootstrap.go b/src/app/command/bootstrap.go index c8bb099..7fa8e31 100644 --- a/src/app/command/bootstrap.go +++ b/src/app/command/bootstrap.go @@ -24,6 +24,7 @@ import ( "github.com/snivilised/pixa/src/app/proxy" "github.com/snivilised/pixa/src/app/proxy/common" "github.com/snivilised/pixa/src/locale" + "github.com/snivilised/traverse/lfs" ) type LocaleDetector interface { @@ -65,6 +66,7 @@ type Bootstrap struct { OptionsInfo ConfigureOptionsInfo Configs *common.Configs Vfs storage.VirtualFS + FS lfs.TraverseFS Logger *slog.Logger Presentation common.PresentationOptions Observers common.Observers @@ -105,6 +107,7 @@ func (b *Bootstrap) Root(options ...ConfigureOptionFn) *cobra.Command { ci, common.Definitions.Pixa.SourceID, common.Definitions.Pixa.AppName, + b.FS, b.Vfs, ) diff --git a/src/app/command/bootstrap_test.go b/src/app/command/bootstrap_test.go index 25ac78c..ad5b046 100644 --- a/src/app/command/bootstrap_test.go +++ b/src/app/command/bootstrap_test.go @@ -8,6 +8,8 @@ import ( "github.com/snivilised/pixa/src/app/command" "github.com/snivilised/pixa/src/app/proxy/common" "github.com/snivilised/pixa/src/internal/helpers" + lab "github.com/snivilised/pixa/src/internal/laboratory" + "github.com/snivilised/traverse/lfs" "golang.org/x/text/language" ) @@ -35,30 +37,29 @@ func (j *DetectorStub) Scan() language.Tag { } var _ = Describe("Bootstrap", Ordered, func() { - var ( repo string l10nPath string configPath string - vfs storage.VirtualFS + FS lfs.TraverseFS + vfsL storage.VirtualFS ) BeforeAll(func() { repo = helpers.Repo("") - l10nPath = helpers.Path(repo, "test/data/l10n") - configPath = helpers.Path(repo, "test/data/configuration") + l10nPath = lab.Path(repo, "test/data/l10n") + configPath = lab.Path(repo, "test/data/configuration") }) BeforeEach(func() { - vfs, _ = helpers.SetupTest( - "nasa-scientist-index.xml", configPath, l10nPath, helpers.Silent, - ) + FS, _ = lab.SetupTest("nasa-scientist-index.xml", configPath, l10nPath, lab.Silent) }) Context("given: root defined with magick sub-command", func() { It("๐Ÿงช should: setup command without error", func() { bootstrap := command.Bootstrap{ - Vfs: vfs, + Vfs: vfsL, + FS: FS, } rootCmd := bootstrap.Root(func(co *command.ConfigureOptionsInfo) { co.Detector = &DetectorStub{} diff --git a/src/app/command/magick-cmd_test.go b/src/app/command/magick-cmd_test.go index 2421d69..4ed1d5e 100644 --- a/src/app/command/magick-cmd_test.go +++ b/src/app/command/magick-cmd_test.go @@ -6,10 +6,11 @@ import ( "github.com/snivilised/cobrass/src/assistant/configuration" "github.com/snivilised/extendio/xfs/storage" - "github.com/snivilised/li18ngo" "github.com/snivilised/pixa/src/app/command" "github.com/snivilised/pixa/src/app/proxy/common" "github.com/snivilised/pixa/src/internal/helpers" + lab "github.com/snivilised/pixa/src/internal/laboratory" + "github.com/snivilised/traverse/lfs" ) var _ = Describe("MagickCmd", Ordered, func() { @@ -17,19 +18,21 @@ var _ = Describe("MagickCmd", Ordered, func() { repo string l10nPath string configPath string + FS lfs.TraverseFS vfs storage.VirtualFS ) BeforeAll(func() { vfs = storage.UseNativeFS() repo = helpers.Repo("") - l10nPath = helpers.Path(repo, "test/data/l10n") - configPath = helpers.Path(repo, "test/data/configuration") + l10nPath = lab.Path(repo, "test/data/l10n") + configPath = lab.Path(repo, "test/data/configuration") }) BeforeEach(func() { - Expect(li18ngo.Use()).To(Succeed()) - vfs, _ = helpers.SetupTest( + Expect(lab.UseI18n(l10nPath)).To(Succeed()) + + FS, _ = lab.SetupTest( "nasa-scientist-index.xml", configPath, l10nPath, helpers.Silent, ) }) @@ -38,6 +41,7 @@ var _ = Describe("MagickCmd", Ordered, func() { It("๐Ÿงช should: execute without error", func() { bootstrap := command.Bootstrap{ Vfs: vfs, + FS: FS, } tester := helpers.CommandTester{ Args: []string{"mag", "--no-tui"}, diff --git a/src/app/command/root-cmd_test.go b/src/app/command/root-cmd_test.go index f45a7ce..3908bcc 100644 --- a/src/app/command/root-cmd_test.go +++ b/src/app/command/root-cmd_test.go @@ -7,10 +7,11 @@ import ( . "github.com/onsi/gomega" //nolint:revive // foo "github.com/snivilised/extendio/xfs/storage" - "github.com/snivilised/li18ngo" "github.com/snivilised/pixa/src/app/command" "github.com/snivilised/pixa/src/app/proxy/common" "github.com/snivilised/pixa/src/internal/helpers" + lab "github.com/snivilised/pixa/src/internal/laboratory" + "github.com/snivilised/traverse/lfs" ) type rootTE struct { @@ -24,24 +25,27 @@ var _ = Describe("RootCmd", Ordered, func() { l10nPath string configPath string vfs storage.VirtualFS + FS lfs.TraverseFS tester helpers.CommandTester ) BeforeAll(func() { - Expect(li18ngo.Use()).To(Succeed()) + Expect(lab.UseI18n(l10nPath)).To(Succeed()) + repo = helpers.Repo("") - l10nPath = helpers.Path(repo, "test/data/l10n") - configPath = helpers.Path(repo, "test/data/configuration") + l10nPath = lab.Path(repo, "test/data/l10n") + configPath = lab.Path(repo, "test/data/configuration") }) BeforeEach(func() { - vfs, _ = helpers.SetupTest( + FS, _ = lab.SetupTest( "nasa-scientist-index.xml", configPath, l10nPath, helpers.Silent, ) bootstrap := command.Bootstrap{ Vfs: vfs, + FS: FS, } tester = helpers.CommandTester{ Root: bootstrap.Root(func(co *command.ConfigureOptionsInfo) { diff --git a/src/app/command/shrink-cmd_test.go b/src/app/command/shrink-cmd_test.go index 97f630d..c2ee40e 100644 --- a/src/app/command/shrink-cmd_test.go +++ b/src/app/command/shrink-cmd_test.go @@ -6,11 +6,12 @@ import ( . "github.com/onsi/ginkgo/v2" //nolint:revive // foo . "github.com/onsi/gomega" //nolint:revive // foo "github.com/snivilised/cobrass/src/assistant/configuration" - "github.com/snivilised/li18ngo" "github.com/snivilised/pixa/src/app/cfg" "github.com/snivilised/pixa/src/app/command" "github.com/snivilised/pixa/src/app/proxy/common" "github.com/snivilised/pixa/src/internal/helpers" + lab "github.com/snivilised/pixa/src/internal/laboratory" + "github.com/snivilised/traverse/lfs" "github.com/snivilised/extendio/xfs/storage" ) @@ -40,23 +41,29 @@ type shrinkTE struct { directory string } -func assertShrinkCmdInvocation(vfs storage.VirtualFS, entry *shrinkTE, root string) { +func assertShrinkCmdInvocation(tfs lfs.TraverseFS, vfs storage.VirtualFS, + entry *shrinkTE, root string, +) { + if tfs == nil { + panic("FS not created") + } bootstrap := command.Bootstrap{ Vfs: vfs, + FS: tfs, } - directory := helpers.Path(root, entry.directory) + directory := lab.Path(root, entry.directory) args := append([]string{common.Definitions.Commands.Shrink, directory}, []string{ "--dry-run", "--no-tui", }...) if entry.outputFlag != "" && entry.outputValue != "" { - output := helpers.Path(root, entry.outputValue) + output := lab.Path(root, entry.outputValue) args = append(args, entry.outputFlag, output) } if entry.trashFlag != "" && entry.trashValue != "" { - trash := helpers.Path(root, entry.trashValue) + trash := lab.Path(root, entry.trashValue) args = append(args, entry.trashFlag, trash) } @@ -89,18 +96,20 @@ var _ = Describe("ShrinkCmd", Ordered, func() { l10nPath string configPath string root string + FS lfs.TraverseFS vfs storage.VirtualFS ) BeforeAll(func() { - Expect(li18ngo.Use()).To(Succeed()) + Expect(lab.UseI18n(l10nPath)).To(Succeed()) + repo = helpers.Repo("") - l10nPath = helpers.Path(repo, "test/data/l10n") - configPath = helpers.Path(repo, "test/data/configuration") + l10nPath = lab.Path(repo, "test/data/l10n") + configPath = lab.Path(repo, "test/data/configuration") }) BeforeEach(func() { - vfs, root = helpers.SetupTest( + FS, root = lab.SetupTest( "nasa-scientist-index.xml", configPath, l10nPath, helpers.Silent, ) }) @@ -110,7 +119,7 @@ var _ = Describe("ShrinkCmd", Ordered, func() { entry.directory = BackyardWorldsPlanet9Scan01 entry.configPath = configPath - assertShrinkCmdInvocation(vfs, entry, root) + assertShrinkCmdInvocation(FS, vfs, entry, root) }, func(entry *shrinkTE) string { return fmt.Sprintf("๐Ÿงช ===> given: '%v'", entry.message) @@ -291,7 +300,7 @@ var _ = Describe("ShrinkCmd", Ordered, func() { }, } - assertShrinkCmdInvocation(vfs, entry, root) + assertShrinkCmdInvocation(FS, vfs, entry, root) }) It("๐Ÿงช should: execute successfully", func() { @@ -306,7 +315,7 @@ var _ = Describe("ShrinkCmd", Ordered, func() { }, } - assertShrinkCmdInvocation(vfs, entry, root) + assertShrinkCmdInvocation(FS, vfs, entry, root) }) }) @@ -323,7 +332,7 @@ var _ = Describe("ShrinkCmd", Ordered, func() { }, } - assertShrinkCmdInvocation(vfs, entry, root) + assertShrinkCmdInvocation(FS, vfs, entry, root) }) It("๐Ÿงช should: execute successfully", func() { @@ -338,7 +347,7 @@ var _ = Describe("ShrinkCmd", Ordered, func() { }, } - assertShrinkCmdInvocation(vfs, entry, root) + assertShrinkCmdInvocation(FS, vfs, entry, root) }) }) }) diff --git a/src/app/proxy/filing/path-finder_test.go b/src/app/proxy/filing/path-finder_test.go index ff3e45e..aceac29 100644 --- a/src/app/proxy/filing/path-finder_test.go +++ b/src/app/proxy/filing/path-finder_test.go @@ -8,12 +8,11 @@ import ( . "github.com/onsi/ginkgo/v2" //nolint:revive // foo . "github.com/onsi/gomega" //nolint:revive // foo "github.com/samber/lo" - "github.com/snivilised/extendio/xfs/nav" - "github.com/snivilised/li18ngo" "github.com/snivilised/pixa/src/app/cfg" "github.com/snivilised/pixa/src/app/proxy/common" "github.com/snivilised/pixa/src/app/proxy/filing" + lab "github.com/snivilised/pixa/src/internal/laboratory" ) type reasons struct { @@ -21,7 +20,9 @@ type reasons struct { file string } -type asserter func(folder, file string, pi *common.PathInfo, statics *common.StaticInfo, entry *pfTE) +type asserter func(folder, file string, + pi *common.PathInfo, statics *common.StaticInfo, entry *pfTE, +) type pfTE struct { given string @@ -50,10 +51,12 @@ var _ = Describe("PathFinder", Ordered, func() { var ( advanced *cfg.MsAdvancedConfig schemes *cfg.MsSchemesConfig + repo string ) BeforeAll(func() { - Expect(li18ngo.Use()).To(Succeed()) + repo = lab.Repo("") + Expect(lab.UseI18n(lab.Path(repo, "test/data/l10n"))).To(Succeed()) schemes = &cfg.MsSchemesConfig{ "blur-sf": &cfg.MsSchemeConfig{ diff --git a/src/app/proxy/pixa-legacy_test.go b/src/app/proxy/pixa-legacy_test.go index 1538933..37de75d 100644 --- a/src/app/proxy/pixa-legacy_test.go +++ b/src/app/proxy/pixa-legacy_test.go @@ -11,11 +11,12 @@ import ( "github.com/snivilised/cobrass/src/assistant/configuration" "github.com/snivilised/extendio/xfs/storage" "github.com/snivilised/extendio/xfs/utils" - "github.com/snivilised/li18ngo" "github.com/snivilised/pixa/src/app/command" "github.com/snivilised/pixa/src/app/proxy/common" + "github.com/snivilised/traverse/lfs" "github.com/snivilised/pixa/src/internal/helpers" + lab "github.com/snivilised/pixa/src/internal/laboratory" ) func openInputTTY() (*os.File, error) { @@ -74,12 +75,12 @@ func augmentL(entry *samplerTE, } if entry.outputFlag != "" { - output := helpers.Path(root, entry.outputFlag) + output := lab.Path(root, entry.outputFlag) result = append(result, "--output", output) } if entry.trashFlag != "" { - trash := helpers.Path(root, entry.trashFlag) + trash := lab.Path(root, entry.trashFlag) result = append(result, "--trash", trash) } @@ -104,16 +105,17 @@ var _ = Describe("pixa-legacy", Ordered, func() { l10nPath string configPath string root string + FS lfs.TraverseFS vfs storage.VirtualFS withoutRenderer bool ) BeforeAll(func() { - Expect(li18ngo.Use()).To(Succeed()) - repo = helpers.Repo("") - l10nPath = helpers.Path(repo, "test/data/l10n") - configPath = helpers.Path(repo, "test/data/configuration") + l10nPath = lab.Path(repo, "test/data/l10n") + Expect(lab.UseI18n(l10nPath)).To(Succeed()) + + configPath = lab.Path(repo, "test/data/configuration") var ( err error @@ -127,20 +129,21 @@ var _ = Describe("pixa-legacy", Ordered, func() { }) BeforeEach(func() { - vfs, root = helpers.SetupTest( + FS, root = lab.SetupTest( "nasa-scientist-index.xml", configPath, l10nPath, helpers.Silent, ) }) DescribeTable("interactive", func(entry *samplerTE) { - origin := helpers.Path(root, entry.relative) + origin := lab.Path(root, entry.relative) args := augmentL(entry, []string{ common.Definitions.Commands.Shrink, origin, }, vfs, root, origin, ) + _ = FS observer := &testPathFinderObserver{ transfers: make(observerAssertions, 6), diff --git a/src/app/proxy/pixa_test.go b/src/app/proxy/pixa_test.go index 1a02869..9899e67 100644 --- a/src/app/proxy/pixa_test.go +++ b/src/app/proxy/pixa_test.go @@ -15,7 +15,9 @@ import ( "github.com/snivilised/pixa/src/app/proxy/common" "github.com/snivilised/pixa/src/app/proxy/filing" "github.com/snivilised/pixa/src/internal/helpers" + lab "github.com/snivilised/pixa/src/internal/laboratory" "github.com/snivilised/pixa/src/internal/matchers" + "github.com/snivilised/traverse/lfs" ) type reasons struct { @@ -159,12 +161,12 @@ func augment(entry *pixaTE, } if entry.output != "" { - output := helpers.Path(root, entry.output) + output := lab.Path(root, entry.output) result = append(result, "--output", output) } if entry.trash != "" { - trash := helpers.Path(root, entry.trash) + trash := lab.Path(root, entry.trash) entry.trash = trash result = append(result, "--trash", trash) } @@ -197,11 +199,12 @@ type coreTest struct { root string configPath string vfs storage.VirtualFS + tfs lfs.TraverseFS withoutRenderer bool } func (t *coreTest) run() { - origin := helpers.Path(t.root, t.entry.relative) + origin := lab.Path(t.root, t.entry.relative) args := augment(t.entry, []string{ common.Definitions.Commands.Shrink, origin, @@ -262,14 +265,15 @@ var _ = Describe("pixa", Ordered, func() { l10nPath string configPath string root string + FS lfs.TraverseFS vfs storage.VirtualFS withoutRenderer bool ) BeforeAll(func() { repo = helpers.Repo("") - l10nPath = helpers.Path(repo, "test/data/l10n") - configPath = helpers.Path(repo, "test/data/configuration") + l10nPath = lab.Path(repo, "test/data/l10n") + configPath = lab.Path(repo, "test/data/configuration") var ( err error @@ -283,7 +287,7 @@ var _ = Describe("pixa", Ordered, func() { }) BeforeEach(func() { - vfs, root = helpers.SetupTest( + FS, root = lab.SetupTest( "nasa-scientist-index.xml", configPath, l10nPath, helpers.Silent, ) }) @@ -295,6 +299,7 @@ var _ = Describe("pixa", Ordered, func() { root: root, configPath: configPath, vfs: vfs, + tfs: FS, withoutRenderer: withoutRenderer, } core.run() diff --git a/src/internal/laboratory/assert.go b/src/internal/laboratory/assert.go new file mode 100644 index 0000000..f888cfa --- /dev/null +++ b/src/internal/laboratory/assert.go @@ -0,0 +1,111 @@ +package lab + +import ( + "io/fs" + "path/filepath" + "strings" + "testing/fstest" + + . "github.com/onsi/gomega" //nolint:revive,stylecheck // ok + "github.com/samber/lo" + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/enums" +) + +type TestOptions struct { + FS *TestTraverseFS + Recording RecordingMap + Path string + Result core.TraverseResult + Err error + ExpectedErr error + Every func(p string) bool +} + +func AssertNavigation(entry *NaviTE, to *TestOptions) { + if to.ExpectedErr != nil { + Expect(to.Err).To(MatchError(to.ExpectedErr)) + return + } + + Expect(to.Err).To(Succeed()) + + visited := []string{} + _ = to.Result.Session().StartedAt() + _ = to.Result.Session().Elapsed() + + if entry.Visit && to.FS != nil { + for path, file := range to.FS.MapFS { + if strings.HasPrefix(path, to.Path) { + if subscribes(entry.Subscription, file) { + visited = append(visited, path) + } + } + } + + every := lo.EveryBy(visited, + lo.Ternary(to.Every != nil, to.Every, func(p string) bool { + segments := strings.Split(p, string(filepath.Separator)) + name, ok := lo.Last(segments) + + if ok { + _, found := to.Recording[name] + return found + } + + return false + }), + ) + + Expect(every).To(BeTrue(), "Not all expected items were invoked") + } + + for name, expected := range entry.ExpectedNoOf.Children { + Expect(to.Recording).To(HaveChildCountOf(ExpectedCount{ + Name: name, + Count: expected, + })) + } + + if entry.Mandatory != nil { + for _, name := range entry.Mandatory { + Expect(to.Recording).To(HaveInvokedNode(name)) + } + } + + if entry.Prohibited != nil { + for _, name := range entry.Prohibited { + Expect(to.Recording).To(HaveNotInvokedNode(name)) + } + } + + assertMetrics(entry, to) +} + +func assertMetrics(entry *NaviTE, to *TestOptions) { + Expect(to.Result).To( + And( + HaveMetricCountOf(ExpectedMetric{ + Type: enums.MetricNoFilesInvoked, + Count: entry.ExpectedNoOf.Files, + }), + HaveMetricCountOf(ExpectedMetric{ + Type: enums.MetricNoFoldersInvoked, + Count: entry.ExpectedNoOf.Folders, + }), + HaveMetricCountOf(ExpectedMetric{ + Type: enums.MetricNoChildFilesFound, + Count: uint(lo.Sum(lo.Values(entry.ExpectedNoOf.Children))), + }), + ), + ) +} + +func subscribes(subscription enums.Subscription, mapFile *fstest.MapFile) bool { + isUniversalSubscription := (subscription == enums.SubscribeUniversal) + files := mapFile != nil && (subscription == enums.SubscribeFiles) && ((mapFile.Mode | fs.ModeDir) == 0) + folders := mapFile != nil && ((subscription == enums.SubscribeFolders) || + subscription == enums.SubscribeFoldersWithFiles) && ((mapFile.Mode | fs.ModeDir) > 0) + + return isUniversalSubscription || files || folders +} diff --git a/src/internal/laboratory/callbacks.go b/src/internal/laboratory/callbacks.go new file mode 100644 index 0000000..8848f79 --- /dev/null +++ b/src/internal/laboratory/callbacks.go @@ -0,0 +1,93 @@ +package lab + +import ( + "fmt" + "strings" + + . "github.com/onsi/ginkgo/v2" //nolint:revive,stylecheck // ok + . "github.com/onsi/gomega" //nolint:revive,stylecheck // ok + "github.com/samber/lo" + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/cycle" +) + +func Begin(em string) cycle.BeginHandler { + return func(state *cycle.BeginState) { + GinkgoWriter.Printf( + "---> %v [traverse-navigator-test:BEGIN], root: '%v'\n", em, state.Root, + ) + } +} + +func End(em string) cycle.EndHandler { + return func(result core.TraverseResult) { + GinkgoWriter.Printf( + "---> %v [traverse-navigator-test:END], err: '%v'\n", em, result.Error(), + ) + } +} + +func UniversalCallback(name string) core.Client { + return func(node *core.Node) error { + depth := node.Extension.Depth + GinkgoWriter.Printf( + "---> ๐ŸŒŠ UNIVERSAL//%v-CALLBACK: (depth:%v) '%v'\n", name, depth, node.Path, + ) + Expect(node.Extension).NotTo(BeNil(), Reason(node.Path)) + + return nil + } +} + +func FoldersCallback(name string) core.Client { + return func(node *core.Node) error { + depth := node.Extension.Depth + actualNoChildren := len(node.Children) + GinkgoWriter.Printf( + "---> ๐Ÿ”† FOLDERS//CALLBACK%v: (depth:%v, children:%v) '%v'\n", + name, depth, actualNoChildren, node.Path, + ) + Expect(node.IsFolder()).To(BeTrue(), + Because(node.Path, "node expected to be folder"), + ) + Expect(node.Extension).NotTo(BeNil(), Reason(node.Path)) + + return nil + } +} + +func FilesCallback(name string) core.Client { + return func(node *core.Node) error { + GinkgoWriter.Printf("---> ๐ŸŒ™ FILES//%v-CALLBACK: '%v'\n", name, node.Path) + Expect(node.IsFolder()).To(BeFalse(), + Because(node.Path, "node expected to be file"), + ) + Expect(node.Extension).NotTo(BeNil(), Reason(node.Path)) + + return nil + } +} + +func FoldersCaseSensitiveCallback(first, second string) core.Client { + recording := make(RecordingMap) + + return func(node *core.Node) error { + recording[node.Path] = len(node.Children) + + GinkgoWriter.Printf("---> ๐Ÿ”† CASE-SENSITIVE-CALLBACK: '%v'\n", node.Path) + Expect(node.IsFolder()).To(BeTrue()) + + if strings.HasSuffix(node.Path, second) { + GinkgoWriter.Printf("---> ๐Ÿ’ง FIRST: '%v', ๐Ÿ’ง SECOND: '%v'\n", first, second) + + paths := lo.Keys(recording) + _, found := lo.Find(paths, func(s string) bool { + return strings.HasSuffix(s, first) + }) + + Expect(found).To(BeTrue(), fmt.Sprintf("for node: '%v'", node.Extension.Name)) + } + + return nil + } +} diff --git a/src/internal/laboratory/directory-tree-builder.go b/src/internal/laboratory/directory-tree-builder.go new file mode 100644 index 0000000..c836aa8 --- /dev/null +++ b/src/internal/laboratory/directory-tree-builder.go @@ -0,0 +1,335 @@ +package lab + +import ( + "bytes" + "encoding/xml" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "testing/fstest" + + "github.com/samber/lo" + "github.com/snivilised/traverse/collections" +) + +const ( + offset = 2 + tabSize = 2 + doWrite = true +) + +// should this be called science? +func Musico(verbose bool, portions ...string) (tsys *TestTraverseFS, root string) { + tsys = &TestTraverseFS{ + fstest.MapFS{ + ".": &fstest.MapFile{ + Mode: os.ModeDir, + }, + }, + } + + return tsys, Provision( + NewMemWriteProvider(tsys, os.ReadFile, portions...), + verbose, + portions..., + ) +} + +func Provision(provider *IOProvider, verbose bool, portions ...string) (root string) { + repo := Repo(filepath.Join("test", "data", "MUSICO")) + + if ensure(repo, + "test/data/research/citizen-scientist-index.xml", + provider.tfs, verbose, + ) != nil { + return "" + } + + if verbose { + fmt.Printf("\n๐Ÿค– re-generated tree at '%v' (filters: '%v')\n\n", + repo, strings.Join(portions, ", "), + ) + } + + return repo +} + +func TrimRoot(root string) string { + // omit leading '/', because test-fs stupidly doesn't like it, + // so we have to jump through hoops + if strings.HasPrefix(root, string(filepath.Separator)) { + return root[1:] + } + + pattern := `^[a-zA-Z]:[\\/]*` + re := regexp.MustCompile(pattern) + + return re.ReplaceAllString(root, "") +} + +func NewMemWriteProvider(tfs *TestTraverseFS, + indexReader readFile, + portions ...string, +) *IOProvider { + filter := lo.Ternary(len(portions) > 0, + matcher(func(path string) bool { + for _, portion := range portions { + if strings.Contains(path, portion) { + return true + } + } + + return false + }), + matcher(func(string) bool { + return true + }), + ) + + // PS: to check the existence of a path in an fs in production + // code, use fs.Stat(fsys, path) instead of os.Stat/os.Lstat + + return &IOProvider{ + tfs: tfs, + filter: filter, + file: fileHandler{ + in: indexReader, + out: writeFile(func(name string, data []byte, mode os.FileMode, show display) error { + if name == "" { + return nil + } + + if filter(name) { + trimmed := TrimRoot(name) + tfs.MapFS[trimmed] = &fstest.MapFile{ + Data: data, + Mode: mode, + } + show(trimmed, func(path string) bool { + entry, ok := tfs.MapFS[path] + return ok && !entry.Mode.IsDir() + }) + } + + return nil + }), + }, + folder: folderHandler{ + out: writeFolder(func(path string, mode os.FileMode, show display, isRoot bool) error { + if path == "" { + return nil + } + + if isRoot || filter(path) { + trimmed := TrimRoot(path) + tfs.MapFS[trimmed] = &fstest.MapFile{ + Mode: mode | os.ModeDir, + } + show(trimmed, func(path string) bool { + entry, ok := tfs.MapFS[path] + return ok && entry.Mode.IsDir() + }) + } + + return nil + }), + }, + } +} + +type ( + entryExists interface { + exists(path string) bool + } + + existsEntry func(path string) bool + + display func(path string, exists existsEntry) + + fileReader interface { + read(name string) ([]byte, error) + } + + readFile func(name string) ([]byte, error) + + fileWriter interface { + write(name string, data []byte, perm os.FileMode, show display) error + } + + writeFile func(name string, data []byte, perm os.FileMode, show display) error + + folderWriter interface { + write(path string, perm os.FileMode, show display, isRoot bool) error + } + + writeFolder func(path string, perm os.FileMode, show display, isRoot bool) error + + filter interface { + match(portion string) bool + } + + matcher func(portion string) bool + + fileHandler struct { + in fileReader + out fileWriter + } + + folderHandler struct { + out folderWriter + } + + IOProvider struct { + tfs *TestTraverseFS + filter filter + file fileHandler + folder folderHandler + } + + Tree struct { + XMLName xml.Name `xml:"tree"` + Root Directory `xml:"directory"` + } + + Directory struct { + XMLName xml.Name `xml:"directory"` + Name string `xml:"name,attr"` + Files []File `xml:"file"` + Directories []Directory `xml:"directory"` + } + + File struct { + XMLName xml.Name `xml:"file"` + Name string `xml:"name,attr"` + Text string `xml:",chardata"` + } +) + +func (fn readFile) read(name string) ([]byte, error) { + return fn(name) +} + +func (fn writeFile) write(name string, data []byte, perm os.FileMode, show display) error { + return fn(name, data, perm, show) +} + +func (fn existsEntry) exists(path string) bool { + return fn(path) +} + +func (fn writeFolder) write(path string, perm os.FileMode, show display, isRoot bool) error { + return fn(path, perm, show, isRoot) +} + +func (fn matcher) match(portion string) bool { + return fn(portion) +} + +// directoryTreeBuilder +type directoryTreeBuilder struct { + root string + full string + stack *collections.Stack[string] + index string + doWrite bool + depth int + padding string + provider *IOProvider + verbose bool + show display +} + +func (r *directoryTreeBuilder) read() (*Directory, error) { + data, err := r.provider.file.in.read(r.index) + + if err != nil { + return nil, err + } + + var tree Tree + + if ue := xml.Unmarshal(data, &tree); ue != nil { + return nil, ue + } + + return &tree.Root, nil +} + +func (r *directoryTreeBuilder) pad() string { + return string(bytes.Repeat([]byte{' '}, (r.depth+offset)*tabSize)) +} + +func (r *directoryTreeBuilder) refill() string { + segments := r.stack.Content() + return filepath.Join(segments...) +} + +func (r *directoryTreeBuilder) inc(name string) { + r.stack.Push(name) + r.full = r.refill() + + r.depth++ + r.padding = r.pad() +} + +func (r *directoryTreeBuilder) dec() { + _, _ = r.stack.Pop() + r.full = r.refill() + + r.depth-- + r.padding = r.pad() +} + +func (r *directoryTreeBuilder) walk() error { + top, err := r.read() + + if err != nil { + return err + } + + r.full = r.root + + return r.dir(*top, true) +} + +func (r *directoryTreeBuilder) dir(dir Directory, isRoot bool) error { //nolint:gocritic // performance is not a concern + r.inc(dir.Name) + + if r.doWrite { + if err := r.provider.folder.out.write( + r.full, + os.ModePerm, + r.show, + isRoot, + ); err != nil { + return err + } + } + + for _, directory := range dir.Directories { + if err := r.dir(directory, false); err != nil { + return err + } + } + + for _, file := range dir.Files { + full := Path(r.full, file.Name) + + if r.doWrite { + if err := r.provider.file.out.write( + full, + []byte(file.Text), + os.ModePerm, + r.show, + ); err != nil { + return err + } + } + } + + r.dec() + + return nil +} diff --git a/src/internal/laboratory/helper-defs.go b/src/internal/laboratory/helper-defs.go new file mode 100644 index 0000000..3edc97b --- /dev/null +++ b/src/internal/laboratory/helper-defs.go @@ -0,0 +1,83 @@ +package lab + +import ( + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/enums" + "github.com/snivilised/traverse/pref" +) + +const ( + Silent = true + Verbose = false +) + +type ( + NaviTE struct { + Given string + Should string + Relative string + Once bool + Visit bool + CaseSensitive bool + Subscription enums.Subscription + Callback core.Client + Mandatory []string + Prohibited []string + ExpectedNoOf Quantities + ExpectedErr error + } + + FilterTE struct { + NaviTE + Description string + Pattern string + Scope enums.FilterScope + Negate bool + ErrorContains string + IfNotApplicable enums.TriStateBool + Custom core.TraverseFilter + Type enums.FilterType + Sample core.SampleTraverseFilter + } + + HybridFilterTE struct { + NaviTE + NodeDef core.FilterDef + ChildDef core.ChildFilterDef + } + + PolyTE struct { + NaviTE + File core.FilterDef + Folder core.FilterDef + } + + SampleTE struct { + NaviTE + SampleType enums.SampleType + Reverse bool + Filter *FilterTE + NoOf pref.EntryQuantities + Each pref.EachDirectoryEntryPredicate + While pref.WhileDirectoryPredicate + } + + Quantities struct { + Files uint + Folders uint + Children map[string]int + } + + MatcherExpectation[T comparable] struct { + Expected T + Actual T + } + + RecordingMap map[string]int + RecordingScopeMap map[string]enums.FilterScope + RecordingOrderMap map[string]int +) + +func (x MatcherExpectation[T]) IsEqual() bool { + return x.Actual == x.Expected +} diff --git a/src/internal/laboratory/matchers.go b/src/internal/laboratory/matchers.go new file mode 100644 index 0000000..a5e251b --- /dev/null +++ b/src/internal/laboratory/matchers.go @@ -0,0 +1,255 @@ +package lab + +import ( + "fmt" + "io/fs" + "slices" + "strings" + + . "github.com/onsi/gomega/types" //nolint:stylecheck,revive // ok + "github.com/samber/lo" + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/enums" +) + +type DirectoryContentsMatcher struct { + expected interface{} + expectedNames []string + actualNames []string +} + +func HaveDirectoryContents(expected interface{}) GomegaMatcher { + return &DirectoryContentsMatcher{ + expected: expected, + } +} + +func (m *DirectoryContentsMatcher) Match(actual interface{}) (bool, error) { + entries, entriesOk := actual.([]fs.DirEntry) + if !entriesOk { + return false, fmt.Errorf("๐Ÿ”ฅ matcher expected []fs.DirEntry (%T)", entries) + } + + m.actualNames = lo.Map(entries, func(entry fs.DirEntry, _ int) string { + return entry.Name() + }) + + expected, expectedOk := m.expected.([]string) + if !expectedOk { + return false, fmt.Errorf("๐Ÿ”ฅ matcher expected []string (%T)", expected) + } + m.expectedNames = expected + + return slices.Compare(m.actualNames, m.expectedNames) == 0, nil +} + +func (m *DirectoryContentsMatcher) FailureMessage(_ interface{}) string { + return fmt.Sprintf( + "โŒ DirectoryContentsMatcher Expected\n\t%v\nto match contents\n\t%v\n", + strings.Join(m.expectedNames, ", "), strings.Join(m.actualNames, ", "), + ) +} + +func (m *DirectoryContentsMatcher) NegatedFailureMessage(_ interface{}) string { + return fmt.Sprintf( + "โŒ DirectoryContentsMatcher Expected\n\t%v\nNOT to match contents\n\t%v\n", + strings.Join(m.expectedNames, ", "), strings.Join(m.actualNames, ", "), + ) +} + +type InvokeNodeMatcher struct { + expected interface{} + mandatory string +} + +func HaveInvokedNode(expected interface{}) GomegaMatcher { + return &InvokeNodeMatcher{ + expected: expected, + } +} + +func (m *InvokeNodeMatcher) Match(actual interface{}) (bool, error) { + recording, ok := actual.(RecordingMap) + if !ok { + return false, fmt.Errorf( + "InvokeNodeMatcher expected actual to be a RecordingMap (%T)", + actual, + ) + } + + mandatory, ok := m.expected.(string) + if !ok { + return false, fmt.Errorf("InvokeNodeMatcher expected string (%T)", actual) + } + m.mandatory = mandatory + + _, found := recording[m.mandatory] + + return found, nil +} + +func (m *InvokeNodeMatcher) FailureMessage(_ interface{}) string { + return fmt.Sprintf("โŒ Expected\n\t%v\nnode to be invoked\n", + m.mandatory, + ) +} + +func (m *InvokeNodeMatcher) NegatedFailureMessage(_ interface{}) string { + return fmt.Sprintf("โŒ Expected\n\t%v\nnode NOT to be invoked\n", + m.mandatory, + ) +} + +type NotInvokeNodeMatcher struct { + expected interface{} + mandatory string +} + +func HaveNotInvokedNode(expected interface{}) GomegaMatcher { + return &NotInvokeNodeMatcher{ + expected: expected, + } +} + +func (m *NotInvokeNodeMatcher) Match(actual interface{}) (bool, error) { + recording, ok := actual.(RecordingMap) + if !ok { + return false, fmt.Errorf("matcher expected actual to be a RecordingMap (%T)", actual) + } + + mandatory, ok := m.expected.(string) + if !ok { + return false, fmt.Errorf("matcher expected string (%T)", actual) + } + m.mandatory = mandatory + + _, found := recording[m.mandatory] + + return !found, nil +} + +func (m *NotInvokeNodeMatcher) FailureMessage(_ interface{}) string { + return fmt.Sprintf("โŒ Expected\n\t%v\nnode to NOT be invoked\n", + m.mandatory, + ) +} + +func (m *NotInvokeNodeMatcher) NegatedFailureMessage(_ interface{}) string { + return fmt.Sprintf("โŒ Expected\n\t%v\nnode to be invoked\n", + m.mandatory, + ) +} + +type ( + ExpectedCount struct { + Name string + Count int + } + + ChildCountMatcher struct { + expected interface{} + expectation MatcherExpectation[uint] + name string + } +) + +func HaveChildCountOf(expected interface{}) GomegaMatcher { + return &ChildCountMatcher{ + expected: expected, + } +} + +func (m *ChildCountMatcher) Match(actual interface{}) (bool, error) { + recording, ok := actual.(RecordingMap) + if !ok { + return false, fmt.Errorf("ChildCountMatcher expected actual to be a RecordingMap (%T)", actual) + } + + expected, ok := m.expected.(ExpectedCount) + if !ok { + return false, fmt.Errorf("ChildCountMatcher expected ExpectedCount (%T)", actual) + } + + count, ok := recording[expected.Name] + if !ok { + return false, fmt.Errorf("๐Ÿ”ฅ not found: '%v'", expected.Name) + } + + m.expectation = MatcherExpectation[uint]{ + Expected: uint(expected.Count), + Actual: uint(count), + } + m.name = expected.Name + + return m.expectation.IsEqual(), nil +} + +func (m *ChildCountMatcher) FailureMessage(_ interface{}) string { + return fmt.Sprintf( + "โŒ Expected child count for node: '%v' to be equal; expected: '%v', actual: '%v'\n", + m.name, m.expectation.Expected, m.expectation.Actual, + ) +} + +func (m *ChildCountMatcher) NegatedFailureMessage(_ interface{}) string { + return fmt.Sprintf( + "โŒ Expected child count for node: '%v' NOT to be equal; expected: '%v', actual: '%v'\n", + m.name, m.expectation.Expected, m.expectation.Actual, + ) +} + +type ( + ExpectedMetric struct { + Type enums.Metric + Count uint + } + + MetricMatcher struct { + expected interface{} + expectation MatcherExpectation[uint] + typ enums.Metric + } +) + +func HaveMetricCountOf(expected interface{}) GomegaMatcher { + return &MetricMatcher{ + expected: expected, + } +} + +func (m *MetricMatcher) Match(actual interface{}) (bool, error) { + result, ok := actual.(core.TraverseResult) + if !ok { + return false, fmt.Errorf( + "๐Ÿ”ฅ MetricMatcher expected actual to be a core.TraverseResult (%T)", + actual, + ) + } + + expected, ok := m.expected.(ExpectedMetric) + if !ok { + return false, fmt.Errorf("๐Ÿ”ฅ MetricMatcher expected ExpectedMetric (%T)", actual) + } + + m.expectation = MatcherExpectation[uint]{ + Expected: expected.Count, + Actual: result.Metrics().Count(expected.Type), + } + m.typ = expected.Type + + return m.expectation.IsEqual(), nil +} + +func (m *MetricMatcher) FailureMessage(_ interface{}) string { + return fmt.Sprintf( + "โŒ Expected metric '%v' to be equal; expected:'%v', actual: '%v'\n", + m.typ.String(), m.expectation.Expected, m.expectation.Actual, + ) +} + +func (m *MetricMatcher) NegatedFailureMessage(_ interface{}) string { + return fmt.Sprintf( + "โŒ Expected metric '%v' NOT to be equal; expected:'%v', actual: '%v'\n", + m.typ.String(), m.expectation.Expected, m.expectation.Actual, + ) +} diff --git a/src/internal/laboratory/test-utilities.go b/src/internal/laboratory/test-utilities.go new file mode 100644 index 0000000..8e7cdcb --- /dev/null +++ b/src/internal/laboratory/test-utilities.go @@ -0,0 +1,208 @@ +package lab + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing/fstest" + + "github.com/onsi/ginkgo" + "github.com/onsi/gomega" + "github.com/pkg/errors" + "github.com/snivilised/cobrass/src/assistant/configuration" + ci18n "github.com/snivilised/cobrass/src/assistant/i18n" + "github.com/snivilised/extendio/xfs/utils" + "github.com/snivilised/li18ngo" + "github.com/snivilised/pixa/src/app/proxy/common" + "github.com/snivilised/pixa/src/internal/matchers" + "github.com/snivilised/pixa/src/locale" + "github.com/snivilised/traverse/collections" + "github.com/snivilised/traverse/lfs" + "github.com/spf13/viper" +) + +// Path creates a path from the parent combined with the relative path. The relative +// path is a file system path so should only contain forward slashes, not the standard +// file path separator as denoted by filepath.Separator, typically used when interacting +// with the local file system. Do not use trailing "/". +func Path(parent, relative string) string { + if relative == "" { + return parent + } + + return parent + "/" + relative +} + +// Repo gets the path of the repo with relative joined on +func Repo(relative string) string { + cmd := exec.Command("git", "rev-parse", "--show-toplevel") + output, _ := cmd.Output() + repo := strings.TrimSpace(string(output)) + + return Path(repo, relative) +} + +func Normalise(p string) string { + return strings.ReplaceAll(p, "/", string(filepath.Separator)) +} + +func Because(name, because string) string { + return fmt.Sprintf("โŒ for item named: '%v', because: '%v'", name, because) +} + +func Reason(name string) string { + return fmt.Sprintf("โŒ for item named: '%v'", name) +} + +func Log() string { + return Repo("Test/test.log") +} + +func UseI18n(l10nPath string) error { + return li18ngo.Use(func(uo *li18ngo.UseOptions) { + uo.From = li18ngo.LoadFrom{ + Path: l10nPath, + Sources: li18ngo.TranslationFiles{ + locale.PixaSourceID: li18ngo.TranslationSource{ + Name: "dummy-cobrass", + }, + + ci18n.CobrassSourceID: li18ngo.TranslationSource{ + Name: "dummy-cobrass", + }, + }, + } + }) +} + +func ReadGlobalConfig(configPath string) error { + var ( + err error + ) + + config := &configuration.GlobalViperConfig{} + + config.SetConfigType(common.Definitions.Pixa.ConfigType) + config.SetConfigName(common.Definitions.Pixa.ConfigTestFilename) + config.AddConfigPath(configPath) + + if e := config.ReadInConfig(); e != nil { + err = errors.Wrap(e, "can't read config") + } + + return err +} + +func SetupTest( + index, configPath, l10nPath string, + silent bool, +) (tfs lfs.TraverseFS, root string) { + var ( + err error + ) + + viper.Reset() + + tfs, root = ResetFS(index, silent) + + if err = MockConfigFile(tfs, configPath); err != nil { + ginkgo.Fail(err.Error()) + } + + if err = ReadGlobalConfig(configPath); err != nil { + ginkgo.Fail(err.Error()) + } + + if err = UseI18n(l10nPath); err != nil { + ginkgo.Fail(err.Error()) + } + + return tfs, root +} + +// MockConfigFile create a dummy config file in the file system specified +func MockConfigFile(tfs lfs.TraverseFS, configPath string) error { + var ( + err error + ) + + _ = tfs.MkDirAll(configPath, common.Permissions.Beezledub) + + if _, err = tfs.Create( + filepath.Join(configPath, common.Definitions.Pixa.ConfigTestFilename), + ); err != nil { + ginkgo.Fail(fmt.Sprintf("๐Ÿ”ฅ can't create dummy config (err: '%v')", err)) + } + + gomega.Expect(matchers.AsDirectory(configPath)).To(matchers.ExistInFS(tfs)) + + return err +} + +func ResetFS(index string, silent bool) (tfs lfs.TraverseFS, root string) { + tfs = &TestTraverseFS{ + fstest.MapFS{ + ".": &fstest.MapFile{ + Mode: os.ModeDir, + }, + }, + } + + root = Scientist(tfs, index, silent) + gomega.Expect(matchers.AsDirectory(root)).To(matchers.ExistInFS(tfs)) + + return tfs, root +} + +func Scientist(tfs lfs.TraverseFS, index string, silent bool) string { + research := filepath.Join("test", "data", "research") + scientist := filepath.Join(research, "scientist") + indexPath := filepath.Join(research, index) + utils.Must(ensure(scientist, indexPath, tfs, silent)) + + return scientist +} + +// ensure +// func ensure(root, index string, provider *IOProvider, verbose bool) error { +// parent, _ := lfs.SplitParent(root) +// builder := directoryTreeBuilder{ +// root: TrimRoot(root), +// stack: collections.NewStackWith([]string{parent}), +// index: index, +// doWrite: doWrite, +// provider: provider, +// verbose: verbose, +// show: func(path string, exists existsEntry) { +// if !verbose { +// return +// } + +// status := lo.Ternary(exists(path), "โœ…", "โŒ") + +// fmt.Printf("---> %v path: '%v'\n", status, path) +// }, +// } + +// return builder.walk() +// } + +func ensure(root, indexPath string, tfs lfs.TraverseFS, silent bool) error { + if tfs.DirectoryExists(root) { + return nil + } + + parent, _ := lfs.SplitParent(root) + builder := directoryTreeBuilder{ + // vfs: tfs, + root: root, + stack: collections.NewStackWith([]string{parent}), + // indexPath: indexPath, + // write: true, + // silent: silent, + } + + return builder.walk() +} diff --git a/src/internal/laboratory/traverse-fs.go b/src/internal/laboratory/traverse-fs.go new file mode 100644 index 0000000..7358db7 --- /dev/null +++ b/src/internal/laboratory/traverse-fs.go @@ -0,0 +1,94 @@ +package lab + +import ( + "io/fs" + "os" + "strings" + "testing/fstest" + + "github.com/samber/lo" + "github.com/snivilised/traverse/locale" +) + +var ( + perms = struct { + File fs.FileMode + Dir fs.FileMode + }{File: 0o666, Dir: 0o777} //nolint:mnd // ok +) + +type testMapFile struct { + f fstest.MapFile +} + +type TestTraverseFS struct { + fstest.MapFS +} + +func (f *TestTraverseFS) FileExists(name string) bool { + if mapFile, found := f.MapFS[name]; found && !mapFile.Mode.IsDir() { + return true + } + + return false +} + +func (f *TestTraverseFS) DirectoryExists(name string) bool { + if mapFile, found := f.MapFS[name]; found && mapFile.Mode.IsDir() { + return true + } + + return false +} + +func (f *TestTraverseFS) Create(name string) (*os.File, error) { + if _, err := f.Stat(name); err == nil { + return nil, fs.ErrExist + } + + file := &fstest.MapFile{ + Mode: perms.File, + } + + f.MapFS[name] = file + dummy := &os.File{} + return dummy, nil +} + +func (f *TestTraverseFS) MkDirAll(name string, perm os.FileMode) error { + if !fs.ValidPath(name) { + return locale.NewInvalidPathError(name) + } + + segments := strings.Split(name, "/") + + _ = lo.Reduce(segments, + func(acc []string, s string, _ int) []string { + acc = append(acc, s) + path := strings.Join(acc, "/") + + if _, found := f.MapFS[path]; !found { + f.MapFS[path] = &fstest.MapFile{ + Mode: perm | os.ModeDir, + } + } + + return acc + }, []string{}, + ) + + return nil +} + +func (f *TestTraverseFS) WriteFile(name string, data []byte, perm os.FileMode) error { + if _, err := f.Stat(name); err == nil { + return fs.ErrExist + } + + f.MapFS[name] = &fstest.MapFile{ + Data: data, + Mode: perm, + } + + return nil +}