From 477535a6a2ae06c87a5fc9fc84f712d1d3002790 Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Sat, 7 Sep 2024 19:55:06 -0400 Subject: [PATCH] feat: adds tools package and refactors mocks --- blueprint/Earthfile | 2 +- blueprint/go.mod | 6 +- blueprint/go.sum | 6 + blueprint/pkg/blueprint/blueprint.go | 2 +- blueprint/pkg/injector/injector.go | 2 +- blueprint/pkg/injector/injector_test.go | 5 +- .../{injector_mock.go => mocks/injector.go} | 12 +- blueprint/pkg/loader/loader.go | 69 +++-- blueprint/pkg/loader/loader_test.go | 27 +- blueprint/pkg/version/version_test.go | 2 +- blueprint/schema/util.go | 2 +- forge/cli/Earthfile | 2 +- forge/cli/cmd/cmds/scan.go | 4 +- forge/cli/go.mod | 4 +- forge/cli/pkg/earthfile/earthfile.go | 2 +- forge/cli/pkg/earthfile/scan.go | 2 +- forge/cli/pkg/earthfile/scan_test.go | 34 ++- forge/cli/pkg/earthly/earthly_test.go | 21 +- forge/cli/pkg/executor/executor.go | 2 +- .../{executor_mock.go => mocks/executor.go} | 13 +- forge/cli/pkg/secrets/interface.go | 2 +- .../pkg/secrets/{ => mocks}/interface_mock.go | 13 +- forge/cli/pkg/walker/walker.go | 33 --- tools/Earthfile | 35 +++ tools/README.md | 193 +++++++++++++ tools/go.mod | 20 ++ tools/go.sum | 57 ++++ tools/pkg/cue/example_test.go | 54 ++++ tools/pkg/cue/load.go | 40 +++ tools/pkg/cue/load_test.go | 75 +++++ tools/pkg/cue/mutate.go | 143 ++++++++++ tools/pkg/cue/mutate_test.go | 261 ++++++++++++++++++ tools/pkg/testutils/helpers.go | 47 ++++ {forge/cli => tools}/pkg/walker/fs.go | 23 +- {forge/cli => tools}/pkg/walker/fs_test.go | 51 +--- .../pkg/walker/fsreverse.go | 31 +-- .../pkg/walker/fsreverse_test.go | 61 +--- tools/pkg/walker/mocks/fileseeker.go | 200 ++++++++++++++ tools/pkg/walker/mocks/reverse.go | 87 ++++++ .../pkg/walker/mocks/walker.go | 27 +- tools/pkg/walker/walker.go | 50 ++++ .../pkg/walker}/walker_mock.go | 2 +- tools/pkg/walker/walker_test.go | 19 ++ 43 files changed, 1483 insertions(+), 260 deletions(-) rename blueprint/pkg/injector/{injector_mock.go => mocks/injector.go} (80%) rename forge/cli/pkg/executor/{executor_mock.go => mocks/executor.go} (82%) rename forge/cli/pkg/secrets/{ => mocks}/interface_mock.go (86%) delete mode 100644 forge/cli/pkg/walker/walker.go create mode 100644 tools/Earthfile create mode 100644 tools/README.md create mode 100644 tools/go.mod create mode 100644 tools/go.sum create mode 100644 tools/pkg/cue/example_test.go create mode 100644 tools/pkg/cue/load.go create mode 100644 tools/pkg/cue/load_test.go create mode 100644 tools/pkg/cue/mutate.go create mode 100644 tools/pkg/cue/mutate_test.go create mode 100644 tools/pkg/testutils/helpers.go rename {forge/cli => tools}/pkg/walker/fs.go (67%) rename {forge/cli => tools}/pkg/walker/fs_test.go (68%) rename blueprint/pkg/loader/walker.go => tools/pkg/walker/fsreverse.go (75%) rename blueprint/pkg/loader/walker_test.go => tools/pkg/walker/fsreverse_test.go (75%) create mode 100644 tools/pkg/walker/mocks/fileseeker.go create mode 100644 tools/pkg/walker/mocks/reverse.go rename forge/cli/pkg/walker/walker_mock.go => tools/pkg/walker/mocks/walker.go (65%) create mode 100644 tools/pkg/walker/walker.go rename {blueprint/pkg/loader => tools/pkg/walker}/walker_mock.go (99%) create mode 100644 tools/pkg/walker/walker_test.go diff --git a/blueprint/Earthfile b/blueprint/Earthfile index 4fc5067f..042d19be 100644 --- a/blueprint/Earthfile +++ b/blueprint/Earthfile @@ -10,7 +10,7 @@ deps: ENV GOMODCACHE=/go/modcache CACHE --persist --sharing shared /go - COPY ../cuetools+src/src /cuetools + COPY ../tools+src/src /tools COPY go.mod go.sum . RUN go mod download diff --git a/blueprint/go.mod b/blueprint/go.mod index 4d8b9c92..a4cec51d 100644 --- a/blueprint/go.mod +++ b/blueprint/go.mod @@ -3,19 +3,19 @@ module github.com/input-output-hk/catalyst-forge/blueprint require ( cuelang.org/go v0.10.0 github.com/Masterminds/semver/v3 v3.2.1 - github.com/input-output-hk/catalyst-forge/cuetools v0.0.0 - github.com/spf13/afero v1.11.0 + github.com/input-output-hk/catalyst-forge/tools v0.0.0 ) require ( github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/spf13/afero v1.11.0 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/text v0.17.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace github.com/input-output-hk/catalyst-forge/cuetools => ../cuetools +replace github.com/input-output-hk/catalyst-forge/tools => ../tools go 1.22.3 diff --git a/blueprint/go.sum b/blueprint/go.sum index 7baae16e..d02f2cf6 100644 --- a/blueprint/go.sum +++ b/blueprint/go.sum @@ -6,6 +6,8 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0 github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= @@ -28,12 +30,16 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 h1:sadMIsgmHpEOGbUs6VtHBXRR1OHevnj7hLx9ZcdNGW4= github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c= github.com/rogpeppe/go-internal v1.12.1-0.20240709150035-ccf4b4329d21 h1:igWZJluD8KtEtAgRyF4x6lqcxDry1ULztksMJh2mnQE= github.com/rogpeppe/go-internal v1.12.1-0.20240709150035-ccf4b4329d21/go.mod h1:RMRJLmBOqWacUkmJHRMiPKh1S1m3PA7Zh4W80/kWPpg= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= diff --git a/blueprint/pkg/blueprint/blueprint.go b/blueprint/pkg/blueprint/blueprint.go index 539b2188..5955fa59 100644 --- a/blueprint/pkg/blueprint/blueprint.go +++ b/blueprint/pkg/blueprint/blueprint.go @@ -8,7 +8,7 @@ import ( "github.com/Masterminds/semver/v3" "github.com/input-output-hk/catalyst-forge/blueprint/pkg/injector" "github.com/input-output-hk/catalyst-forge/blueprint/pkg/version" - cuetools "github.com/input-output-hk/catalyst-forge/cuetools/pkg" + cuetools "github.com/input-output-hk/catalyst-forge/tools/pkg/cue" ) // BlueprintFile represents a single blueprint file. diff --git a/blueprint/pkg/injector/injector.go b/blueprint/pkg/injector/injector.go index d217bbd1..5b07e573 100644 --- a/blueprint/pkg/injector/injector.go +++ b/blueprint/pkg/injector/injector.go @@ -1,6 +1,6 @@ package injector -//go:generate go run github.com/matryer/moq@latest --out ./injector_mock.go . EnvGetter +//go:generate go run github.com/matryer/moq@latest -skip-ensure --pkg mocks --out ./mocks/injector.go . EnvGetter import ( "fmt" diff --git a/blueprint/pkg/injector/injector_test.go b/blueprint/pkg/injector/injector_test.go index 9aae14cf..a4303b91 100644 --- a/blueprint/pkg/injector/injector_test.go +++ b/blueprint/pkg/injector/injector_test.go @@ -7,7 +7,8 @@ import ( "cuelang.org/go/cue" "cuelang.org/go/cue/cuecontext" - cuetools "github.com/input-output-hk/catalyst-forge/cuetools/pkg" + "github.com/input-output-hk/catalyst-forge/blueprint/pkg/injector/mocks" + cuetools "github.com/input-output-hk/catalyst-forge/tools/pkg/cue" ) func TestInjectEnv(t *testing.T) { @@ -96,7 +97,7 @@ func TestInjectEnv(t *testing.T) { i := Injector{ logger: slog.New(slog.NewTextHandler(io.Discard, nil)), - getter: &EnvGetterMock{ + getter: &mocks.EnvGetterMock{ GetFunc: func(key string) (string, bool) { return tt.env[key], true }, diff --git a/blueprint/pkg/injector/injector_mock.go b/blueprint/pkg/injector/mocks/injector.go similarity index 80% rename from blueprint/pkg/injector/injector_mock.go rename to blueprint/pkg/injector/mocks/injector.go index 4e05b4cc..1c737dff 100644 --- a/blueprint/pkg/injector/injector_mock.go +++ b/blueprint/pkg/injector/mocks/injector.go @@ -1,28 +1,24 @@ // Code generated by moq; DO NOT EDIT. // github.com/matryer/moq -package injector +package mocks import ( "sync" ) -// Ensure, that EnvGetterMock does implement EnvGetter. -// If this is not the case, regenerate this file with moq. -var _ EnvGetter = &EnvGetterMock{} - -// EnvGetterMock is a mock implementation of EnvGetter. +// EnvGetterMock is a mock implementation of injector.EnvGetter. // // func TestSomethingThatUsesEnvGetter(t *testing.T) { // -// // make and configure a mocked EnvGetter +// // make and configure a mocked injector.EnvGetter // mockedEnvGetter := &EnvGetterMock{ // GetFunc: func(key string) (string, bool) { // panic("mock out the Get method") // }, // } // -// // use mockedEnvGetter in code that requires EnvGetter +// // use mockedEnvGetter in code that requires injector.EnvGetter // // and then make assertions. // // } diff --git a/blueprint/pkg/loader/loader.go b/blueprint/pkg/loader/loader.go index 89e25df6..3f5f4602 100644 --- a/blueprint/pkg/loader/loader.go +++ b/blueprint/pkg/loader/loader.go @@ -14,7 +14,8 @@ import ( "github.com/input-output-hk/catalyst-forge/blueprint/pkg/injector" "github.com/input-output-hk/catalyst-forge/blueprint/pkg/version" "github.com/input-output-hk/catalyst-forge/blueprint/schema" - cuetools "github.com/input-output-hk/catalyst-forge/cuetools/pkg" + cuetools "github.com/input-output-hk/catalyst-forge/tools/pkg/cue" + "github.com/input-output-hk/catalyst-forge/tools/pkg/walker" ) const BlueprintFileName = "blueprint.cue" @@ -29,7 +30,7 @@ type BlueprintLoader struct { injector injector.Injector logger *slog.Logger rootPath string - walker ReverseWalker + walker walker.ReverseWalker } func (b *BlueprintLoader) Load() error { @@ -137,27 +138,31 @@ func (b *BlueprintLoader) Raw() blueprint.RawBlueprint { func (b *BlueprintLoader) findBlueprints(startPath, endPath string) (map[string][]byte, error) { bps := make(map[string][]byte) - err := b.walker.Walk(startPath, endPath, func(path string, fileType FileType, openFile func() (FileSeeker, error)) error { - if fileType == FileTypeFile { - if filepath.Base(path) == BlueprintFileName { - reader, err := openFile() - if err != nil { - return err + err := b.walker.Walk( + startPath, + endPath, + func(path string, fileType walker.FileType, openFile func() (walker.FileSeeker, error)) error { + if fileType == walker.FileTypeFile { + if filepath.Base(path) == BlueprintFileName { + reader, err := openFile() + if err != nil { + return err + } + + defer reader.Close() + + data, err := io.ReadAll(reader) + if err != nil { + return err + } + + bps[path] = data } - - defer reader.Close() - - data, err := io.ReadAll(reader) - if err != nil { - return err - } - - bps[path] = data } - } - return nil - }) + return nil + }, + ) if err != nil { return nil, err @@ -171,16 +176,20 @@ func (b *BlueprintLoader) findBlueprints(startPath, endPath string) (map[string] // the root is not found. func (b *BlueprintLoader) findGitRoot(startPath string) (string, error) { var gitRoot string - err := b.walker.Walk(startPath, "/", func(path string, fileType FileType, openFile func() (FileSeeker, error)) error { - if fileType == FileTypeDir { - if filepath.Base(path) == ".git" { - gitRoot = filepath.Dir(path) - return io.EOF + err := b.walker.Walk( + startPath, + "/", + func(path string, fileType walker.FileType, openFile func() (walker.FileSeeker, error)) error { + if fileType == walker.FileTypeDir { + if filepath.Base(path) == ".git" { + gitRoot = filepath.Dir(path) + return io.EOF + } } - } - return nil - }) + return nil + }, + ) if err != nil { return "", err @@ -202,7 +211,7 @@ func NewDefaultBlueprintLoader(rootPath string, logger = slog.New(slog.NewTextHandler(io.Discard, nil)) } - walker := NewDefaultFSReverseWalker(logger) + walker := walker.NewDefaultFSReverseWalker(logger) return BlueprintLoader{ injector: injector.NewDefaultInjector(logger), logger: logger, @@ -214,7 +223,7 @@ func NewDefaultBlueprintLoader(rootPath string, // NewBlueprintLoader creates a new blueprint loader func NewBlueprintLoader(rootPath string, logger *slog.Logger, - walker ReverseWalker, + walker walker.ReverseWalker, injector injector.Injector, ) BlueprintLoader { return BlueprintLoader{ diff --git a/blueprint/pkg/loader/loader_test.go b/blueprint/pkg/loader/loader_test.go index 8876169e..6ea8e729 100644 --- a/blueprint/pkg/loader/loader_test.go +++ b/blueprint/pkg/loader/loader_test.go @@ -12,6 +12,9 @@ import ( "cuelang.org/go/cue" "github.com/input-output-hk/catalyst-forge/blueprint/pkg/injector" + imocks "github.com/input-output-hk/catalyst-forge/blueprint/pkg/injector/mocks" + "github.com/input-output-hk/catalyst-forge/tools/pkg/walker" + wmocks "github.com/input-output-hk/catalyst-forge/tools/pkg/walker/mocks" ) type fieldTest struct { @@ -181,11 +184,11 @@ func TestBlueprintLoaderLoad(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - walker := &ReverseWalkerMock{ - WalkFunc: func(startPath string, endPath string, callback WalkerCallback) error { + walker := &wmocks.ReverseWalkerMock{ + WalkFunc: func(startPath string, endPath string, callback walker.WalkerCallback) error { // True when there is no git root, so we simulate only searching for blueprint files in the root path. if startPath == endPath && len(tt.files) > 0 { - err := callback(filepath.Join(tt.root, "blueprint.cue"), FileTypeFile, func() (FileSeeker, error) { + err := callback(filepath.Join(tt.root, "blueprint.cue"), walker.FileTypeFile, func() (walker.FileSeeker, error) { return NewMockFileSeeker(tt.files[filepath.Join(tt.root, "blueprint.cue")]), nil }) @@ -201,11 +204,11 @@ func TestBlueprintLoaderLoad(t *testing.T) { for path, content := range tt.files { var err error if content == "" { - err = callback(path, FileTypeDir, func() (FileSeeker, error) { + err = callback(path, walker.FileTypeDir, func() (walker.FileSeeker, error) { return nil, nil }) } else { - err = callback(path, FileTypeFile, func() (FileSeeker, error) { + err = callback(path, walker.FileTypeFile, func() (walker.FileSeeker, error) { return NewMockFileSeeker(content), nil }) } @@ -224,7 +227,7 @@ func TestBlueprintLoaderLoad(t *testing.T) { loader := BlueprintLoader{ injector: injector.NewInjector( slog.New(slog.NewTextHandler(io.Discard, nil)), - &injector.EnvGetterMock{ + &imocks.EnvGetterMock{ GetFunc: func(name string) (string, bool) { return "", false }, @@ -320,10 +323,10 @@ func TestBlueprintLoader_findBlueprints(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - walker := &ReverseWalkerMock{ - WalkFunc: func(startPath string, endPath string, callback WalkerCallback) error { + walker := &wmocks.ReverseWalkerMock{ + WalkFunc: func(startPath string, endPath string, callback walker.WalkerCallback) error { for path, content := range tt.files { - err := callback(path, FileTypeFile, func() (FileSeeker, error) { + err := callback(path, walker.FileTypeFile, func() (walker.FileSeeker, error) { return NewMockFileSeeker(content), nil }) @@ -393,10 +396,10 @@ func TestBlueprintLoader_findGitRoot(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var lastPath string - walker := &ReverseWalkerMock{ - WalkFunc: func(startPath string, endPath string, callback WalkerCallback) error { + walker := &wmocks.ReverseWalkerMock{ + WalkFunc: func(startPath string, endPath string, callback walker.WalkerCallback) error { for _, dir := range tt.dirs { - err := callback(dir, FileTypeDir, func() (FileSeeker, error) { + err := callback(dir, walker.FileTypeDir, func() (walker.FileSeeker, error) { return nil, nil }) diff --git a/blueprint/pkg/version/version_test.go b/blueprint/pkg/version/version_test.go index dcff8395..dc2e3b88 100644 --- a/blueprint/pkg/version/version_test.go +++ b/blueprint/pkg/version/version_test.go @@ -6,7 +6,7 @@ import ( "cuelang.org/go/cue/cuecontext" "github.com/Masterminds/semver/v3" "github.com/input-output-hk/catalyst-forge/blueprint/internal/testutils" - cuetools "github.com/input-output-hk/catalyst-forge/cuetools/pkg" + cuetools "github.com/input-output-hk/catalyst-forge/tools/pkg/cue" ) func TestGetVersion(t *testing.T) { diff --git a/blueprint/schema/util.go b/blueprint/schema/util.go index f5394d16..9b7b1927 100644 --- a/blueprint/schema/util.go +++ b/blueprint/schema/util.go @@ -6,7 +6,7 @@ import ( "cuelang.org/go/cue" "github.com/Masterminds/semver/v3" "github.com/input-output-hk/catalyst-forge/blueprint/pkg/version" - cuetools "github.com/input-output-hk/catalyst-forge/cuetools/pkg" + cuetools "github.com/input-output-hk/catalyst-forge/tools/pkg/cue" ) // SchemaFile contains the schema for blueprint files. diff --git a/forge/cli/Earthfile b/forge/cli/Earthfile index 8682fa7f..78d807cc 100644 --- a/forge/cli/Earthfile +++ b/forge/cli/Earthfile @@ -11,7 +11,7 @@ deps: CACHE --persist --sharing shared /go COPY ../../blueprint+src/src /blueprint - COPY ../../cuetools+src/src /cuetools + COPY ../../tools+src/src /tools COPY go.mod go.sum . RUN go mod download diff --git a/forge/cli/cmd/cmds/scan.go b/forge/cli/cmd/cmds/scan.go index 048ccd1c..e41f845c 100644 --- a/forge/cli/cmd/cmds/scan.go +++ b/forge/cli/cmd/cmds/scan.go @@ -8,7 +8,7 @@ import ( "sort" "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/earthfile" - "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/walker" + "github.com/input-output-hk/catalyst-forge/tools/pkg/walker" ) type ScanCmd struct { @@ -20,7 +20,7 @@ type ScanCmd struct { } func (c *ScanCmd) Run(logger *slog.Logger) error { - walker := walker.NewFilesystemWalker(logger) + walker := walker.NewDefaultFSWalker(logger) var rootPath string if c.Absolute { diff --git a/forge/cli/go.mod b/forge/cli/go.mod index 18c64915..b24060da 100644 --- a/forge/cli/go.mod +++ b/forge/cli/go.mod @@ -35,7 +35,7 @@ require ( github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/input-output-hk/catalyst-forge/cuetools v0.0.0 // indirect + github.com/input-output-hk/catalyst-forge/tools v0.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect @@ -55,4 +55,4 @@ require ( replace github.com/input-output-hk/catalyst-forge/blueprint => ../../blueprint -replace github.com/input-output-hk/catalyst-forge/cuetools => ../../cuetools +replace github.com/input-output-hk/catalyst-forge/tools => ../../tools diff --git a/forge/cli/pkg/earthfile/earthfile.go b/forge/cli/pkg/earthfile/earthfile.go index d0f0cb80..fa97e6bb 100644 --- a/forge/cli/pkg/earthfile/earthfile.go +++ b/forge/cli/pkg/earthfile/earthfile.go @@ -5,7 +5,7 @@ import ( "github.com/earthly/earthly/ast" "github.com/earthly/earthly/ast/spec" - "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/walker" + "github.com/input-output-hk/catalyst-forge/tools/pkg/walker" ) // Earthfile represents a parsed Earthfile. diff --git a/forge/cli/pkg/earthfile/scan.go b/forge/cli/pkg/earthfile/scan.go index 4208e907..1f40b4e8 100644 --- a/forge/cli/pkg/earthfile/scan.go +++ b/forge/cli/pkg/earthfile/scan.go @@ -7,7 +7,7 @@ import ( "path/filepath" "strings" - w "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/walker" + w "github.com/input-output-hk/catalyst-forge/tools/pkg/walker" ) // ScanEarthfiles scans the given root path for Earthfiles and returns a map diff --git a/forge/cli/pkg/earthfile/scan_test.go b/forge/cli/pkg/earthfile/scan_test.go index 0b1f376d..b16f77b6 100644 --- a/forge/cli/pkg/earthfile/scan_test.go +++ b/forge/cli/pkg/earthfile/scan_test.go @@ -3,14 +3,40 @@ package earthfile import ( "fmt" "io" + "io/fs" + "strings" "testing" "log/slog" - "github.com/input-output-hk/catalyst-forge/forge/cli/internal/testutils/mocks" - "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/walker" + "github.com/input-output-hk/catalyst-forge/tools/pkg/walker" + "github.com/input-output-hk/catalyst-forge/tools/pkg/walker/mocks" ) +type MockFileSeeker struct { + *strings.Reader +} + +func (MockFileSeeker) Stat() (fs.FileInfo, error) { + return MockFileInfo{}, nil +} + +func (MockFileSeeker) Close() error { + return nil +} + +type MockFileInfo struct { + fs.FileInfo +} + +func (MockFileInfo) Name() string { + return "Earthfile" +} + +func NewMockFileSeeker(s string) MockFileSeeker { + return MockFileSeeker{strings.NewReader(s)} +} + func TestScanEarthfiles(t *testing.T) { tests := []struct { callbackErr error @@ -93,11 +119,11 @@ foo1: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - walker := &walker.WalkerMock{ + walker := &mocks.WalkerMock{ WalkFunc: func(rootPath string, callback walker.WalkerCallback) error { for path, content := range tt.files { err := callback(path, walker.FileTypeFile, func() (walker.FileSeeker, error) { - return mocks.NewMockFileSeeker(content), tt.callbackErr + return NewMockFileSeeker(content), tt.callbackErr }) if err != nil { diff --git a/forge/cli/pkg/earthly/earthly_test.go b/forge/cli/pkg/earthly/earthly_test.go index 69d38ea3..07f3e245 100644 --- a/forge/cli/pkg/earthly/earthly_test.go +++ b/forge/cli/pkg/earthly/earthly_test.go @@ -11,8 +11,9 @@ import ( "github.com/input-output-hk/catalyst-forge/blueprint/pkg/utils" "github.com/input-output-hk/catalyst-forge/blueprint/schema" "github.com/input-output-hk/catalyst-forge/forge/cli/internal/testutils" - "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/executor" + emocks "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/executor/mocks" "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/secrets" + smocks "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/secrets/mocks" ) func TestEarthlyExecutorRun(t *testing.T) { @@ -20,7 +21,7 @@ func TestEarthlyExecutorRun(t *testing.T) { name string output string earthlyExec EarthlyExecutor - mockExec executor.ExecutorMock + mockExec emocks.ExecutorMock expect map[string]EarthlyExecutionResult expectCalls int expectErr bool @@ -30,7 +31,7 @@ func TestEarthlyExecutorRun(t *testing.T) { earthlyExec: NewEarthlyExecutor("/test/dir", "foo", nil, secrets.SecretStore{}, testutils.NewNoopLogger(), ), - mockExec: executor.ExecutorMock{ + mockExec: emocks.ExecutorMock{ ExecuteFunc: func(command string, args []string) ([]byte, error) { return []byte(`foobarbaz Image foo output as bar @@ -56,7 +57,7 @@ Artifact foo output as bar`), nil testutils.NewNoopLogger(), WithRetries(3), ), - mockExec: executor.ExecutorMock{ + mockExec: emocks.ExecutorMock{ ExecuteFunc: func(command string, args []string) ([]byte, error) { return []byte{}, fmt.Errorf("error") }, @@ -71,7 +72,7 @@ Artifact foo output as bar`), nil testutils.NewNoopLogger(), WithPlatforms("foo", "bar"), ), - mockExec: executor.ExecutorMock{ + mockExec: emocks.ExecutorMock{ ExecuteFunc: func(command string, args []string) ([]byte, error) { return []byte(`foobarbaz Image foo output as bar @@ -225,7 +226,7 @@ func TestEarthlyExecutor_buildSecrets(t *testing.T) { }{ { name: "simple", - provider: &secrets.SecretProviderMock{ + provider: &smocks.SecretProviderMock{ GetFunc: func(path string) (string, error) { return `{"key": "value"}`, nil }, @@ -250,7 +251,7 @@ func TestEarthlyExecutor_buildSecrets(t *testing.T) { }, { name: "key does not exist", - provider: &secrets.SecretProviderMock{ + provider: &smocks.SecretProviderMock{ GetFunc: func(path string) (string, error) { return `{"key": "value"}`, nil }, @@ -270,7 +271,7 @@ func TestEarthlyExecutor_buildSecrets(t *testing.T) { }, { name: "invalid JSON", - provider: &secrets.SecretProviderMock{ + provider: &smocks.SecretProviderMock{ GetFunc: func(path string) (string, error) { return `invalid`, nil }, @@ -288,7 +289,7 @@ func TestEarthlyExecutor_buildSecrets(t *testing.T) { }, { name: "secret provider does not exist", - provider: &secrets.SecretProviderMock{ + provider: &smocks.SecretProviderMock{ GetFunc: func(path string) (string, error) { return "", nil }, @@ -306,7 +307,7 @@ func TestEarthlyExecutor_buildSecrets(t *testing.T) { }, { name: "secret provider error", - provider: &secrets.SecretProviderMock{ + provider: &smocks.SecretProviderMock{ GetFunc: func(path string) (string, error) { return "", fmt.Errorf("error") }, diff --git a/forge/cli/pkg/executor/executor.go b/forge/cli/pkg/executor/executor.go index 750dfdf8..f5305c81 100644 --- a/forge/cli/pkg/executor/executor.go +++ b/forge/cli/pkg/executor/executor.go @@ -1,6 +1,6 @@ package executor -//go:generate go run github.com/matryer/moq@latest -out executor_mock.go . Executor +//go:generate go run github.com/matryer/moq@latest -pkg mocks -out mocks/executor.go . Executor // Executor is an interface for executing commands. type Executor interface { diff --git a/forge/cli/pkg/executor/executor_mock.go b/forge/cli/pkg/executor/mocks/executor.go similarity index 82% rename from forge/cli/pkg/executor/executor_mock.go rename to forge/cli/pkg/executor/mocks/executor.go index 4e24f5b5..99f1adb2 100644 --- a/forge/cli/pkg/executor/executor_mock.go +++ b/forge/cli/pkg/executor/mocks/executor.go @@ -1,28 +1,29 @@ // Code generated by moq; DO NOT EDIT. // github.com/matryer/moq -package executor +package mocks import ( + "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/executor" "sync" ) -// Ensure, that ExecutorMock does implement Executor. +// Ensure, that ExecutorMock does implement executor.Executor. // If this is not the case, regenerate this file with moq. -var _ Executor = &ExecutorMock{} +var _ executor.Executor = &ExecutorMock{} -// ExecutorMock is a mock implementation of Executor. +// ExecutorMock is a mock implementation of executor.Executor. // // func TestSomethingThatUsesExecutor(t *testing.T) { // -// // make and configure a mocked Executor +// // make and configure a mocked executor.Executor // mockedExecutor := &ExecutorMock{ // ExecuteFunc: func(command string, args []string) ([]byte, error) { // panic("mock out the Execute method") // }, // } // -// // use mockedExecutor in code that requires Executor +// // use mockedExecutor in code that requires executor.Executor // // and then make assertions. // // } diff --git a/forge/cli/pkg/secrets/interface.go b/forge/cli/pkg/secrets/interface.go index 9f80b968..ad27644f 100644 --- a/forge/cli/pkg/secrets/interface.go +++ b/forge/cli/pkg/secrets/interface.go @@ -1,6 +1,6 @@ package secrets -//go:generate go run github.com/matryer/moq@latest -out interface_mock.go . SecretProvider +//go:generate go run github.com/matryer/moq@latest --pkg mocks -out mocks/interface_mock.go . SecretProvider // SecretProvider is an interface for getting and setting secrets. type SecretProvider interface { diff --git a/forge/cli/pkg/secrets/interface_mock.go b/forge/cli/pkg/secrets/mocks/interface_mock.go similarity index 86% rename from forge/cli/pkg/secrets/interface_mock.go rename to forge/cli/pkg/secrets/mocks/interface_mock.go index 1b02028b..78ac6b22 100644 --- a/forge/cli/pkg/secrets/interface_mock.go +++ b/forge/cli/pkg/secrets/mocks/interface_mock.go @@ -1,21 +1,22 @@ // Code generated by moq; DO NOT EDIT. // github.com/matryer/moq -package secrets +package mocks import ( + "github.com/input-output-hk/catalyst-forge/forge/cli/pkg/secrets" "sync" ) -// Ensure, that SecretProviderMock does implement SecretProvider. +// Ensure, that SecretProviderMock does implement secrets.SecretProvider. // If this is not the case, regenerate this file with moq. -var _ SecretProvider = &SecretProviderMock{} +var _ secrets.SecretProvider = &SecretProviderMock{} -// SecretProviderMock is a mock implementation of SecretProvider. +// SecretProviderMock is a mock implementation of secrets.SecretProvider. // // func TestSomethingThatUsesSecretProvider(t *testing.T) { // -// // make and configure a mocked SecretProvider +// // make and configure a mocked secrets.SecretProvider // mockedSecretProvider := &SecretProviderMock{ // GetFunc: func(key string) (string, error) { // panic("mock out the Get method") @@ -25,7 +26,7 @@ var _ SecretProvider = &SecretProviderMock{} // }, // } // -// // use mockedSecretProvider in code that requires SecretProvider +// // use mockedSecretProvider in code that requires secrets.SecretProvider // // and then make assertions. // // } diff --git a/forge/cli/pkg/walker/walker.go b/forge/cli/pkg/walker/walker.go deleted file mode 100644 index 4a915178..00000000 --- a/forge/cli/pkg/walker/walker.go +++ /dev/null @@ -1,33 +0,0 @@ -package walker - -import ( - "io" - "io/fs" -) - -//go:generate go run github.com/matryer/moq@latest -out walker_mock.go . Walker - -// FileType is an enum that represents the type of a file. -type FileType int - -const ( - FileTypeFile FileType = iota - FileTypeDir -) - -// FileSeeker is an interface that combines the fs.File and io.Seeker interfaces. -type FileSeeker interface { - fs.File - io.Seeker -} - -// WalkerCallback is a callback function that is called for each file in the -// Walk function. -type WalkerCallback func(string, FileType, func() (FileSeeker, error)) error - -// Walker is an interface that allows walking over a set of files. -type Walker interface { - // Walk walks over the files in the given root path and calls the given - // function for each file. - Walk(rootPath string, callback WalkerCallback) error -} diff --git a/tools/Earthfile b/tools/Earthfile new file mode 100644 index 00000000..e3f3bdc8 --- /dev/null +++ b/tools/Earthfile @@ -0,0 +1,35 @@ +VERSION 0.8 + +deps: + FROM golang:1.23.0-alpine3.19 + + WORKDIR /work + + RUN mkdir -p /go/cache && mkdir -p /go/modcache + ENV GOCACHE=/go/cache + ENV GOMODCACHE=/go/modcache + CACHE --persist --sharing shared /go + + COPY go.mod go.sum . + RUN go mod download + +src: + FROM +deps + + CACHE --persist --sharing shared /go + + COPY --dir pkg . + RUN go generate ./... + + SAVE ARTIFACT . src + +check: + FROM +src + + RUN gofmt -l . | grep . && exit 1 || exit 0 + RUN go vet ./... + +test: + FROM +src + + RUN go test ./... \ No newline at end of file diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 00000000..aefc487c --- /dev/null +++ b/tools/README.md @@ -0,0 +1,193 @@ +# Tools + +The `tools` package provides common code that is used throughout Catalyst Forge. + +## Cue + +The `pkg/cue` package provides common utilities for interacting with the [CUE language](https://cuelang.org/). + +### Loading and Validation + +The contents of a CUE file can be loaded with: + +```go +package pkg + +import ( + "fmt" + "log" + "os" + + "cuelang.org/go/cue/cuecontext" + cuetools "github.com/input-output-hk/catalyst-forge/cuetools/pkg" +) + +func main() { + b, err := os.ReadFile("file.cue") + if err != nil { + log.Fatal(err) + } + + ctx := cuecontext.New() + v, err := cuetools.Compile(ctx, b) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Value: %v\n", v) +} +``` + +The `Compile` function will only return an error when a syntax error is present in the given file. +It does not validate whether the file is logically valid (i.e., non-concrete values are acceptable). +To further validate the file: + +```go +func main() { + // ... + err = cuetools.Validate(v, cue.Concrete(true)) + if err != nil { + log.Fatal(err) + } +} +``` + +By default, the validation method provided by the CUE API will ellide error messages when more than one error exists. +The `Validate` method handles this by building a proper error string that includes all errors encountered while validating. +Each error is placed on a new line in order to improve readability. + +### Mutating Values + +By default, CUE is immutable and it's not possible to arbitrarily delete and/or replace fields within a CUE value. +This constraint exists at the language level and cannot be easily broken via the Go API. +While respecting language boundaries is often the best solution, in some cases it may be overwhelmingly apparent that a field needs +to be mutated and that it can be done safely. +For those cases, this package provides functions for both deleting and replacing arbitrary fields. + +To delete a field: + +```go +package main + +import ( + "fmt" + "log" + + "cuelang.org/go/cue/cuecontext" + cuetools "github.com/input-output-hk/catalyst-forge/cuetools/pkg" +) + +func main() { + ctx := cuecontext.New() + v := ctx.CompileString(`{a: 1, b: 2}`) + v, err := cuetools.Delete(ctx, v, "a") + if err != nil { + log.Fatalf("failed to delete field: %v", err) + } + + fmt.Println(v) // { b: 2 } +} +``` + +To replace a field with a new value: + +```go +func main() { + // ... + v = ctx.CompileString(`{a: 1, b: 2}`) + v, err := cuetools.Replace(ctx, v, "a", ctx.CompileString("3")) + if err != nil { + log.Fatalf("failed to delete field: %v", err) + } + + fmt.Println(v) // { a: 3, b: 2} +} +``` + +The `path` argument for both functions can be nested: + +``` +a.b.c +``` + +And can also index into lists: + +``` +a.b[0].c +``` + +## File Walking + +The `pkg/walker` package provides interfaces/implementations for walking over files in a filesystem. + +### Walking + +The `FSWalker` can be used to walk all files starting from a given path. +The following example walks all files starting from `/my/path` and collects the contents of any file named `foo.txt` into the +`foos` map. + +```go +walker := NewDefaultFSWalker(nil) // An optional slogger can be passed +foos := make(map[string][]byte) +err := walker.Walk("/my/path", func(path string, fileType w.FileType, openFile func() (w.FileSeeker, error)) error { + if fileType == FileTypeFile { + if filepath.Base(path) == "foo.txt" { + reader, err := openFile() + if err != nil { + return err + } + + defer reader.Close() + + data, err := io.ReadAll(reader) + if err != nil { + return err + } + + foos[path] = data + } + } + + return nil +}) +``` + +### Reverse Walking + +The `FSReverseWalker` can be used to traverse the filesystem upwards from a start and end path. +The following example starts at `/my/start/path` and walks upwards over all files until it reaches `/my`. +It collects the contents of any file named `foo.txt` into the `foos` map. + +```go +walker := NewDefaultReverseFSWalker(nil) // An optional slogger can be passed +foos := make(map[string][]byte) +err := walker.Walk("/my/start/path", "/my", func(path string, fileType FileType, openFile func() (FileSeeker, error)) error { + if fileType == FileTypeFile { + if filepath.Base(path) == "foo.txt" { + reader, err := openFile() + if err != nil { + return err + } + + defer reader.Close() + + data, err := io.ReadAll(reader) + if err != nil { + return err + } + + foos[path] = data + } + } + + return nil +}) +``` + +## Testing + +Tests can be run with: + +``` +go test ./... +``` \ No newline at end of file diff --git a/tools/go.mod b/tools/go.mod new file mode 100644 index 00000000..cc33aceb --- /dev/null +++ b/tools/go.mod @@ -0,0 +1,20 @@ +module github.com/input-output-hk/catalyst-forge/tools + +go 1.22.3 + +require ( + cuelang.org/go v0.10.0 + github.com/spf13/afero v1.11.0 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/cockroachdb/apd/v3 v3.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/text v0.17.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/tools/go.sum b/tools/go.sum new file mode 100644 index 00000000..9a89abed --- /dev/null +++ b/tools/go.sum @@ -0,0 +1,57 @@ +cuelabs.dev/go/oci/ociregistry v0.0.0-20240807094312-a32ad29eed79 h1:EceZITBGET3qHneD5xowSTY/YHbNybvMWGh62K2fG/M= +cuelabs.dev/go/oci/ociregistry v0.0.0-20240807094312-a32ad29eed79/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg= +cuelang.org/go v0.10.0 h1:Y1Pu4wwga5HkXfLFK1sWAYaSWIBdcsr5Cb5AWj2pOuE= +cuelang.org/go v0.10.0/go.mod h1:HzlaqqqInHNiqE6slTP6+UtxT9hN6DAzgJgdbNxXvX8= +github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/proto v1.13.2 h1:z/etSFO3uyXeuEsVPzfl56WNgzcvIr42aQazXaQmFZY= +github.com/emicklei/proto v1.13.2/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 h1:sadMIsgmHpEOGbUs6VtHBXRR1OHevnj7hLx9ZcdNGW4= +github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c= +github.com/rogpeppe/go-internal v1.12.1-0.20240709150035-ccf4b4329d21 h1:igWZJluD8KtEtAgRyF4x6lqcxDry1ULztksMJh2mnQE= +github.com/rogpeppe/go-internal v1.12.1-0.20240709150035-ccf4b4329d21/go.mod h1:RMRJLmBOqWacUkmJHRMiPKh1S1m3PA7Zh4W80/kWPpg= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tools/pkg/cue/example_test.go b/tools/pkg/cue/example_test.go new file mode 100644 index 00000000..1930bb61 --- /dev/null +++ b/tools/pkg/cue/example_test.go @@ -0,0 +1,54 @@ +// Package cue provides a set of functions for loading and manipulating CUE. +package cue + +import ( + "fmt" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/cuecontext" +) + +var src = ` +{ + a: { + b: 1 + c: 2 + } + d: [ + { + e: 3 + } + ] +} +` + +func Example() { + ctx := cuecontext.New() + v, err := Compile(ctx, []byte(src)) + if err != nil { + panic(err) + } + + if err := Validate(v, cue.Concrete(true)); err != nil { + panic(err) + } + + // Replace a.b with 2 + v, err = Replace(ctx, v, "a.b", ctx.CompileString("2")) + fmt.Printf("a.b: %v\n", v.LookupPath(cue.ParsePath("a.b"))) + + // Replace d[0].e with 4 + v, err = Replace(ctx, v, "d[0].e", ctx.CompileString("4")) + fmt.Printf("d[0].e: %v\n", v.LookupPath(cue.ParsePath("d[0].e"))) + + // Delete a.c + v, err = Delete(ctx, v, "a.c") + fmt.Printf("a: %v\n", v.LookupPath(cue.ParsePath("a"))) + + // output: + // a.b: 2 + // d[0].e: 4 + // a: { + // b: 2 + // } +} diff --git a/tools/pkg/cue/load.go b/tools/pkg/cue/load.go new file mode 100644 index 00000000..2c1b12d9 --- /dev/null +++ b/tools/pkg/cue/load.go @@ -0,0 +1,40 @@ +package cue + +import ( + "fmt" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/errors" +) + +// Compile compiles the given CUE contents and returns the resulting value. +// If the contents are invalid, an error is returned. +func Compile(ctx *cue.Context, contents []byte) (cue.Value, error) { + v := ctx.CompileBytes(contents) + if v.Err() != nil { + return cue.Value{}, v.Err() + } + + return v, nil +} + +// Validate validates the given CUE value. If the value is invalid, an error +// is returned. +func Validate(c cue.Value, opts ...cue.Option) error { + if err := c.Validate(opts...); err != nil { + var errStr string + errs := errors.Errors(err) + + if len(errs) == 1 { + errStr = errs[0].Error() + } else { + errStr = "\n" + for _, e := range errs { + errStr += e.Error() + "\n" + } + } + return fmt.Errorf("failed to validate: %s", errStr) + } + + return nil +} diff --git a/tools/pkg/cue/load_test.go b/tools/pkg/cue/load_test.go new file mode 100644 index 00000000..2adc21c7 --- /dev/null +++ b/tools/pkg/cue/load_test.go @@ -0,0 +1,75 @@ +package cue + +import ( + "fmt" + "testing" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/cuecontext" + "github.com/input-output-hk/catalyst-forge/tools/pkg/testutils" + "github.com/stretchr/testify/assert" +) + +func TestCompile(t *testing.T) { + ctx := cuecontext.New() + tests := []struct { + name string + contents string + expectedVal cue.Value + expectedErr string + }{ + { + name: "valid contents", + contents: "{}", + expectedVal: ctx.CompileString("{}"), + expectedErr: "", + }, + { + name: "invalid contents", + contents: "{a: b}", + expectedVal: cue.Value{}, + expectedErr: "a: reference \"b\" not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v, err := Compile(ctx, []byte(tt.contents)) + if testutils.AssertError(t, err, tt.expectedErr != "", tt.expectedErr) { + return + } + + assert.True(t, v.Equals(tt.expectedVal)) + }) + } +} + +func TestValidate(t *testing.T) { + ctx := cuecontext.New() + tests := []struct { + name string + v cue.Value + expectErr bool + expectedErr string + }{ + { + name: "valid value", + v: ctx.CompileString("{}"), + expectErr: false, + expectedErr: "", + }, + { + name: "invalid value", + v: ctx.CompileString("{a: 1}").FillPath(cue.ParsePath("a"), fmt.Errorf("invalid value")), + expectErr: true, + expectedErr: "failed to validate: invalid value", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := Validate(tt.v) + testutils.AssertError(t, err, tt.expectErr, tt.expectedErr) + }) + } +} diff --git a/tools/pkg/cue/mutate.go b/tools/pkg/cue/mutate.go new file mode 100644 index 00000000..bfdd6dfb --- /dev/null +++ b/tools/pkg/cue/mutate.go @@ -0,0 +1,143 @@ +package cue + +import ( + "fmt" + + "cuelang.org/go/cue" +) + +// delete deletes the field at the given path from the given value. +// The path must point to either a struct field or a list index. +func Delete(ctx *cue.Context, v cue.Value, path string) (cue.Value, error) { + // final holds the final value after the delete operation + var final cue.Value + + refPath := cue.ParsePath(path) + refSels := refPath.Selectors() + + // Make sure the target path exists + if !v.LookupPath(refPath).Exists() { + return v, fmt.Errorf("path %q does not exist", path) + } + + // Isolate the last selector in the target path, which is the field to delete + deletedSel, parentSels := refSels[len(refSels)-1], refSels[:len(refSels)-1] + parentPath := cue.MakePath(parentSels...) // Path to the parent of the field to delete + + var err error + final, err = deleteFrom(ctx, v.LookupPath(parentPath), deletedSel) + if err != nil { + return v, fmt.Errorf("failed to delete field: %v", err) + } + + // Replace the parent struct in the given value with the new struct that has the target field removed + final, err = Replace(ctx, v, parentPath.String(), final) + if err != nil { + return v, fmt.Errorf("failed to rebuild struct: %v", err) + } + + return final, nil +} + +// replace replaces the value at the given path with the given value. +// The path must point to either a struct field or a list index. +func Replace(ctx *cue.Context, v cue.Value, path string, replace cue.Value) (cue.Value, error) { + cpath := cue.ParsePath(path) + if !v.LookupPath(cpath).Exists() { + return v, fmt.Errorf("path %q does not exist", path) + } + + final := replace + sels := cpath.Selectors() + for len(sels) > 0 { + var lastSel cue.Selector + curIndex := len(sels) - 1 + lastSel, sels = sels[curIndex], sels[:curIndex] + + switch lastSel.Type() { + case cue.IndexLabel: + new := ctx.CompileString("[...]") + curList, err := v.LookupPath(cue.MakePath(sels...)).List() + if err != nil { + return cue.Value{}, fmt.Errorf("expected list at path %s: %v", path, err) + } + + for i := 0; curList.Next(); i++ { + var val cue.Value + if curList.Selector() == lastSel { + val = final + } else { + val = curList.Value() + } + + new = new.FillPath(cue.MakePath(cue.Index(i)), val) + } + + final = new + case cue.StringLabel: + new := ctx.CompileString("{}") + curFields, err := v.LookupPath(cue.MakePath(sels...)).Fields() + if err != nil { + return cue.Value{}, fmt.Errorf("expected struct at path %s: %v", path, err) + } + + for curFields.Next() { + fieldPath := cue.MakePath(curFields.Selector()) + if curFields.Selector() == lastSel { + new = new.FillPath(fieldPath, final) + } else { + new = new.FillPath(fieldPath, curFields.Value()) + } + } + + final = new + default: + return cue.Value{}, fmt.Errorf("unknown selector type %s", lastSel.Type()) + } + } + + return final, nil +} + +// deleteFrom deletes the field at the given selector from the given value. +// The value must be a struct or a list. +func deleteFrom(ctx *cue.Context, v cue.Value, targetSel cue.Selector) (cue.Value, error) { + switch targetSel.Type() { + case cue.IndexLabel: + new := ctx.CompileString("[...]") + list, err := v.List() + if err != nil { + return v, fmt.Errorf("expected list: %v", err) + } + + var i int + for list.Next() { + if list.Selector() == targetSel { + continue + } + + new = new.FillPath(cue.MakePath(cue.Index(i)), list.Value()) + i++ + } + + return new, nil + case cue.StringLabel: + new := ctx.CompileString("{}") + fields, err := v.Fields() + if err != nil { + return v, fmt.Errorf("expected struct: %v", err) + } + + for fields.Next() { + if fields.Selector() == targetSel { + continue + } + + new = new.FillPath(cue.MakePath(fields.Selector()), fields.Value()) + } + + return new, nil + default: + return v, fmt.Errorf("unsupported selector type %s", targetSel.Type().String()) + } +} diff --git a/tools/pkg/cue/mutate_test.go b/tools/pkg/cue/mutate_test.go new file mode 100644 index 00000000..b7a70f89 --- /dev/null +++ b/tools/pkg/cue/mutate_test.go @@ -0,0 +1,261 @@ +package cue + +import ( + "fmt" + "testing" + + "cuelang.org/go/cue/cuecontext" + "cuelang.org/go/cue/format" + "github.com/input-output-hk/catalyst-forge/tools/pkg/testutils" + "github.com/stretchr/testify/assert" +) + +func TestDelete(t *testing.T) { + tests := []struct { + name string + value string + path string + expected string + expectErr bool + }{ + { + name: "delete field", + value: ` +{ + a: 1 + b: 2 +}`, + path: "a", + expected: ` +{ + b: 2 +}`, + }, + { + name: "delete nested field", + value: ` +{ + a: { + b: 1 + c: 2 + } +}`, + path: "a.b", + expected: ` +{ + a: { + c: 2 + } +}`, + expectErr: false, + }, + { + name: "delete list element", + value: ` +{ + a: [1, 2, 3] +}`, + path: "a[1]", + expected: ` +{ + a: [1, ...] & [_, 3, ...] & { + [...] + } +}`, + expectErr: false, + }, + { + name: "delete nested list element", + value: ` +{ + a: [ + { + b: [1, 2, 3] + } + ] +}`, + path: "a[0].b[1]", + expected: ` +{ + a: [{ + b: [1, ...] & [_, 3, ...] & { + [...] + } + }, ...] & { + [...] + } +}`, + expectErr: false, + }, + { + name: "delete non-existent field", + value: ` +{ + a: 1 +}`, + path: "b", + expected: "", + expectErr: true, + }, + { + name: "delete non-existent index", + value: ` +{ + a: [1, 2, 3] +}`, + path: "a[3]", + expected: "", + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := cuecontext.New() + v, err := Compile(ctx, []byte(tt.value)) + assert.NoError(t, err) + + final, err := Delete(ctx, v, tt.path) + if testutils.AssertError(t, err, tt.expectErr, "") { + return + } + + src, err := format.Node(final.Syntax()) + assert.NoError(t, err) + + assert.Equal(t, fmt.Sprintf("\n%s", src), tt.expected) + }) + } +} + +func TestReplace(t *testing.T) { + tests := []struct { + name string + value string + path string + replace string + expected string + expectErr bool + }{ + { + name: "replace field", + value: ` +{ + a: 1 + b: 2 +}`, + path: "a", + replace: "3", + expected: ` +{ + a: 3 + b: 2 +}`, + expectErr: false, + }, + { + name: "replace nested field", + value: ` +{ + a: { + b: 1 + c: 2 + } +}`, + path: "a.b", + replace: "3", + expected: ` +{ + a: { + b: 3 + c: 2 + } +}`, + expectErr: false, + }, + { + name: "replace list element", + value: ` +{ + a: [1, 2, 3] +}`, + path: "a[1]", + replace: "4", + expected: ` +{ + a: [1, ...] & [_, 4, ...] & [_, _, 3, ...] & { + [...] + } +}`, + expectErr: false, + }, + { + name: "replace nested list element", + value: ` +{ + a: [ + { + b: [1, 2, 3] + } + ] +}`, + path: "a[0].b[1]", + replace: "4", + expected: ` +{ + a: [{ + b: [1, ...] & [_, 4, ...] & [_, _, 3, ...] & { + [...] + } + }, ...] & { + [...] + } +}`, + expectErr: false, + }, + { + name: "replace non-existent field", + value: ` +{ + a: 1 +}`, + path: "b", + replace: "2", + expected: "", + expectErr: true, + }, + { + name: "replace non-existent index", + value: ` +{ + a: [1, 2, 3] +}`, + path: "a[3]", + replace: "4", + expected: "", + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := cuecontext.New() + v, err := Compile(ctx, []byte(tt.value)) + assert.NoError(t, err) + + replace, err := Compile(ctx, []byte(tt.replace)) + assert.NoError(t, err) + + final, err := Replace(ctx, v, tt.path, replace) + + if testutils.AssertError(t, err, tt.expectErr, "") { + return + } + + src, err := format.Node(final.Syntax()) + assert.NoError(t, err) + + assert.Equal(t, fmt.Sprintf("\n%s", src), tt.expected) + }) + } +} diff --git a/tools/pkg/testutils/helpers.go b/tools/pkg/testutils/helpers.go new file mode 100644 index 00000000..d8a061ea --- /dev/null +++ b/tools/pkg/testutils/helpers.go @@ -0,0 +1,47 @@ +// Package testutils provides a set of helper functions for testing. +package testutils + +import ( + "path/filepath" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/require" +) + +// AssertError asserts the state of an error. +// +// If isExpected is true, the function will assert that the error is not nil. +// If expectedErr is not empty, the function will assert that the error message +// matches the expected error message. +// If isExpected is false, the function will assert that the error is nil. +// The function returns true if an error was found, false otherwise. +func AssertError(t *testing.T, err error, isExpected bool, expectedErr string) bool { + if isExpected { + require.Error(t, err) + + if expectedErr != "" { + require.EqualError(t, err, expectedErr) + } + + return true + } else { + require.NoError(t, err) + } + + return false +} + +// SetupFS sets up the filesystem with the given files. +func SetupFS(t *testing.T, fs afero.Fs, files map[string]string) { + for path, content := range files { + dir := filepath.Dir(path) + if err := fs.MkdirAll(dir, 0755); err != nil { + t.Fatalf("failed to create directory %s: %v", dir, err) + } + + if err := afero.WriteFile(fs, path, []byte(content), 0644); err != nil { + t.Fatalf("failed to write file %s: %v", path, err) + } + } +} diff --git a/forge/cli/pkg/walker/fs.go b/tools/pkg/walker/fs.go similarity index 67% rename from forge/cli/pkg/walker/fs.go rename to tools/pkg/walker/fs.go index 40554bd1..c41e4252 100644 --- a/forge/cli/pkg/walker/fs.go +++ b/tools/pkg/walker/fs.go @@ -8,7 +8,7 @@ import ( ) // FilesystemWalker is a walker that walks over the local filesystem. -type FilesystemWalker struct { +type FSWalker struct { fs afero.Fs logger *slog.Logger } @@ -16,7 +16,7 @@ type FilesystemWalker struct { // Walk walks over the files and directories in the given root path and calls // the given function for each entry. // The reader passed to the function is closed after the function returns. -func (w *FilesystemWalker) Walk(rootPath string, callback WalkerCallback) error { +func (w *FSWalker) Walk(rootPath string, callback WalkerCallback) error { return afero.Walk(w.fs, rootPath, func(path string, info os.FileInfo, err error) error { w.logger.Debug("walking path", "path", path) if err != nil { @@ -45,9 +45,22 @@ func (w *FilesystemWalker) Walk(rootPath string, callback WalkerCallback) error }) } -// NewFilesystemWalker creates a new FilesystemWalker. -func NewFilesystemWalker(logger *slog.Logger) FilesystemWalker { - return FilesystemWalker{ +// NewFilesystemWalker creates a new FSWalker with the given filesystem. +func NewFSWalker(fs afero.Fs, logger *slog.Logger) FSWalker { + return FSWalker{ + fs: fs, + logger: logger, + } +} + +// NewDefaultFilesystemWalker creates a new FSWalker with the default filesystem +// and an optional logger. +func NewDefaultFSWalker(logger *slog.Logger) FSWalker { + if logger == nil { + logger = slog.New(slog.NewTextHandler(os.Stderr, nil)) + } + + return FSWalker{ fs: afero.NewOsFs(), logger: logger, } diff --git a/forge/cli/pkg/walker/fs_test.go b/tools/pkg/walker/fs_test.go similarity index 68% rename from forge/cli/pkg/walker/fs_test.go rename to tools/pkg/walker/fs_test.go index e28ee7c0..351f9132 100644 --- a/forge/cli/pkg/walker/fs_test.go +++ b/tools/pkg/walker/fs_test.go @@ -4,23 +4,13 @@ import ( "fmt" "io" "log/slog" - "maps" - "path/filepath" "testing" - "github.com/input-output-hk/catalyst-forge/forge/cli/internal/testutils" + "github.com/input-output-hk/catalyst-forge/tools/pkg/testutils" "github.com/spf13/afero" + "github.com/stretchr/testify/assert" ) -type wrapfs struct { - afero.Fs - trigger error -} - -func (w *wrapfs) Open(name string) (afero.File, error) { - return nil, w.trigger -} - func TestFileSystemWalkerWalk(t *testing.T) { tests := []struct { name string @@ -30,7 +20,7 @@ func TestFileSystemWalkerWalk(t *testing.T) { files map[string]string expectedFiles map[string]string expectErr bool - expectedErr error + expectedErr string }{ { name: "single directory", @@ -46,7 +36,7 @@ func TestFileSystemWalkerWalk(t *testing.T) { "/test1/file2": "file2", }, expectErr: false, - expectedErr: nil, + expectedErr: "", }, { name: "nested directories", @@ -64,13 +54,14 @@ func TestFileSystemWalkerWalk(t *testing.T) { "/test1/dir1/dir2/file3": "file3", }, expectErr: false, - expectedErr: nil, + expectedErr: "", }, { name: "error opening file", fs: &wrapfs{ - Fs: afero.NewMemMapFs(), - trigger: fmt.Errorf("fail"), + Fs: afero.NewMemMapFs(), + trigger: fmt.Errorf("fail"), + failAfter: 1, }, callbackErr: nil, path: "/test1", @@ -79,7 +70,7 @@ func TestFileSystemWalkerWalk(t *testing.T) { }, expectedFiles: map[string]string{}, expectErr: true, - expectedErr: fmt.Errorf("fail"), + expectedErr: "fail", }, { name: "callback error", @@ -91,27 +82,18 @@ func TestFileSystemWalkerWalk(t *testing.T) { }, expectedFiles: map[string]string{}, expectErr: true, - expectedErr: fmt.Errorf("callback error"), + expectedErr: "callback error", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - walker := FilesystemWalker{ + walker := FSWalker{ fs: tt.fs, logger: slog.New(slog.NewTextHandler(io.Discard, nil)), } - for path, content := range tt.files { - dir := filepath.Dir(path) - if err := tt.fs.MkdirAll(dir, 0755); err != nil { - t.Fatalf("failed to create directory %s: %v", dir, err) - } - - if err := afero.WriteFile(tt.fs, path, []byte(content), 0644); err != nil { - t.Fatalf("failed to write file %s: %v", path, err) - } - } + testutils.SetupFS(t, tt.fs, tt.files) callbackFiles := make(map[string]string) err := walker.Walk(tt.path, func(path string, fileType FileType, openFile func() (FileSeeker, error)) error { @@ -138,16 +120,11 @@ func TestFileSystemWalkerWalk(t *testing.T) { return nil }) - ret, err := testutils.CheckError(t, err, tt.expectErr, tt.expectedErr) - if err != nil { - t.Fatal(err) - } else if ret { + if testutils.AssertError(t, err, tt.expectErr, tt.expectedErr) { return } - if !maps.Equal(callbackFiles, tt.expectedFiles) { - t.Fatalf("expected: %v, got: %v", tt.expectedFiles, callbackFiles) - } + assert.Equal(t, tt.expectedFiles, callbackFiles) }) } } diff --git a/blueprint/pkg/loader/walker.go b/tools/pkg/walker/fsreverse.go similarity index 75% rename from blueprint/pkg/loader/walker.go rename to tools/pkg/walker/fsreverse.go index 557bdaab..79e12b96 100644 --- a/blueprint/pkg/loader/walker.go +++ b/tools/pkg/walker/fsreverse.go @@ -1,4 +1,4 @@ -package loader +package walker //go:generate go run github.com/matryer/moq@latest -out ./walker_mock.go . ReverseWalker @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/fs" "log/slog" "path/filepath" "strings" @@ -14,34 +13,6 @@ import ( "github.com/spf13/afero" ) -// FileType is an enum that represents the type of a file. -type FileType int - -const ( - FileTypeFile FileType = iota - FileTypeDir -) - -// FileSeeker is an interface that combines the fs.File and io.Seeker interfaces. -type FileSeeker interface { - fs.File - io.Seeker -} - -// WalkerCallback is a callback function that is called for each file in the -// Walk function. -type WalkerCallback func(string, FileType, func() (FileSeeker, error)) error - -// ReverseWalker is an interface that allows reverse walking over a set of -// files. -// The start path is the path where the walk starts and the end path is the path -// where the walk ends. -type ReverseWalker interface { - // Walk performs a reverse walk from the end path to the start path and - // calls the given function for each file. - Walk(startPath, endPath string, callback WalkerCallback) error -} - // FSReverseWalker is a ReverseWalker that walks over the local filesystem. type FSReverseWalker struct { fs afero.Fs diff --git a/blueprint/pkg/loader/walker_test.go b/tools/pkg/walker/fsreverse_test.go similarity index 75% rename from blueprint/pkg/loader/walker_test.go rename to tools/pkg/walker/fsreverse_test.go index e3d8c092..724aa5e0 100644 --- a/blueprint/pkg/loader/walker_test.go +++ b/tools/pkg/walker/fsreverse_test.go @@ -1,33 +1,16 @@ -package loader +package walker import ( "fmt" "io" "log/slog" - "maps" - "path/filepath" "testing" - "github.com/input-output-hk/catalyst-forge/blueprint/internal/testutils" + "github.com/input-output-hk/catalyst-forge/tools/pkg/testutils" "github.com/spf13/afero" + "github.com/stretchr/testify/assert" ) -type wrapfs struct { - afero.Fs - - attempts int - failAfter int - trigger error -} - -func (w *wrapfs) Open(name string) (afero.File, error) { - w.attempts++ - if w.attempts == w.failAfter { - return nil, w.trigger - } - return afero.Fs.Open(w.Fs, name) -} - func TestFSReverseWalkerWalk(t *testing.T) { tests := []struct { name string @@ -38,7 +21,7 @@ func TestFSReverseWalkerWalk(t *testing.T) { files map[string]string expectedFiles map[string]string expectErr bool - expectedErr error + expectedErr string }{ { name: "single directory", @@ -55,7 +38,7 @@ func TestFSReverseWalkerWalk(t *testing.T) { "/test1/file2": "file2", }, expectErr: false, - expectedErr: nil, + expectedErr: "", }, { name: "multiple directories", @@ -74,7 +57,7 @@ func TestFSReverseWalkerWalk(t *testing.T) { "/test1/test2/file3": "file3", }, expectErr: false, - expectedErr: nil, + expectedErr: "", }, { name: "multiple scoped directories", @@ -97,7 +80,7 @@ func TestFSReverseWalkerWalk(t *testing.T) { "/test1/test2/file3": "file3", }, expectErr: false, - expectedErr: nil, + expectedErr: "", }, { name: "error reading directory", @@ -110,7 +93,7 @@ func TestFSReverseWalkerWalk(t *testing.T) { }, expectedFiles: map[string]string{}, expectErr: true, - expectedErr: fmt.Errorf("failed to read directory: failed"), + expectedErr: "failed to read directory: failed", }, { name: "error reading file", @@ -123,7 +106,7 @@ func TestFSReverseWalkerWalk(t *testing.T) { }, expectedFiles: map[string]string{}, expectErr: true, - expectedErr: fmt.Errorf("failed to open file: failed"), + expectedErr: "failed to open file: failed", }, { name: "callback error", @@ -136,7 +119,7 @@ func TestFSReverseWalkerWalk(t *testing.T) { }, expectedFiles: map[string]string{}, expectErr: true, - expectedErr: fmt.Errorf("callback error"), + expectedErr: "callback error", }, { name: "callback EOF", @@ -149,7 +132,7 @@ func TestFSReverseWalkerWalk(t *testing.T) { }, expectedFiles: map[string]string{}, expectErr: false, - expectedErr: nil, + expectedErr: "", }, { name: "start path is not a subdirectory of end path", @@ -162,7 +145,7 @@ func TestFSReverseWalkerWalk(t *testing.T) { }, expectedFiles: map[string]string{}, expectErr: true, - expectedErr: fmt.Errorf("start path is not a subdirectory of end path"), + expectedErr: "start path is not a subdirectory of end path", }, } @@ -173,16 +156,7 @@ func TestFSReverseWalkerWalk(t *testing.T) { logger: slog.New(slog.NewTextHandler(io.Discard, nil)), } - for path, content := range tt.files { - dir := filepath.Dir(path) - if err := tt.fs.MkdirAll(dir, 0755); err != nil { - t.Fatalf("failed to create directory %s: %v", dir, err) - } - - if err := afero.WriteFile(tt.fs, path, []byte(content), 0644); err != nil { - t.Fatalf("failed to write file %s: %v", path, err) - } - } + testutils.SetupFS(t, tt.fs, tt.files) callbackFiles := make(map[string]string) err := walker.Walk(tt.startPath, tt.endPath, func(path string, fileType FileType, openFile func() (FileSeeker, error)) error { @@ -209,16 +183,11 @@ func TestFSReverseWalkerWalk(t *testing.T) { return nil }) - ret, err := testutils.CheckError(t, err, tt.expectErr, tt.expectedErr) - if err != nil { - t.Fatal(err) - } else if ret { + if testutils.AssertError(t, err, tt.expectErr, tt.expectedErr) { return } - if !maps.Equal(callbackFiles, tt.expectedFiles) { - t.Fatalf("expected: %v, got: %v", tt.expectedFiles, callbackFiles) - } + assert.Equal(t, tt.expectedFiles, callbackFiles) }) } } diff --git a/tools/pkg/walker/mocks/fileseeker.go b/tools/pkg/walker/mocks/fileseeker.go new file mode 100644 index 00000000..e50b4bce --- /dev/null +++ b/tools/pkg/walker/mocks/fileseeker.go @@ -0,0 +1,200 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package mocks + +import ( + "github.com/input-output-hk/catalyst-forge/tools/pkg/walker" + "io/fs" + "sync" +) + +// Ensure, that FileSeekerMock does implement walker.FileSeeker. +// If this is not the case, regenerate this file with moq. +var _ walker.FileSeeker = &FileSeekerMock{} + +// FileSeekerMock is a mock implementation of walker.FileSeeker. +// +// func TestSomethingThatUsesFileSeeker(t *testing.T) { +// +// // make and configure a mocked walker.FileSeeker +// mockedFileSeeker := &FileSeekerMock{ +// CloseFunc: func() error { +// panic("mock out the Close method") +// }, +// ReadFunc: func(bytes []byte) (int, error) { +// panic("mock out the Read method") +// }, +// SeekFunc: func(offset int64, whence int) (int64, error) { +// panic("mock out the Seek method") +// }, +// StatFunc: func() (fs.FileInfo, error) { +// panic("mock out the Stat method") +// }, +// } +// +// // use mockedFileSeeker in code that requires walker.FileSeeker +// // and then make assertions. +// +// } +type FileSeekerMock struct { + // CloseFunc mocks the Close method. + CloseFunc func() error + + // ReadFunc mocks the Read method. + ReadFunc func(bytes []byte) (int, error) + + // SeekFunc mocks the Seek method. + SeekFunc func(offset int64, whence int) (int64, error) + + // StatFunc mocks the Stat method. + StatFunc func() (fs.FileInfo, error) + + // calls tracks calls to the methods. + calls struct { + // Close holds details about calls to the Close method. + Close []struct { + } + // Read holds details about calls to the Read method. + Read []struct { + // Bytes is the bytes argument value. + Bytes []byte + } + // Seek holds details about calls to the Seek method. + Seek []struct { + // Offset is the offset argument value. + Offset int64 + // Whence is the whence argument value. + Whence int + } + // Stat holds details about calls to the Stat method. + Stat []struct { + } + } + lockClose sync.RWMutex + lockRead sync.RWMutex + lockSeek sync.RWMutex + lockStat sync.RWMutex +} + +// Close calls CloseFunc. +func (mock *FileSeekerMock) Close() error { + if mock.CloseFunc == nil { + panic("FileSeekerMock.CloseFunc: method is nil but FileSeeker.Close was just called") + } + callInfo := struct { + }{} + mock.lockClose.Lock() + mock.calls.Close = append(mock.calls.Close, callInfo) + mock.lockClose.Unlock() + return mock.CloseFunc() +} + +// CloseCalls gets all the calls that were made to Close. +// Check the length with: +// +// len(mockedFileSeeker.CloseCalls()) +func (mock *FileSeekerMock) CloseCalls() []struct { +} { + var calls []struct { + } + mock.lockClose.RLock() + calls = mock.calls.Close + mock.lockClose.RUnlock() + return calls +} + +// Read calls ReadFunc. +func (mock *FileSeekerMock) Read(bytes []byte) (int, error) { + if mock.ReadFunc == nil { + panic("FileSeekerMock.ReadFunc: method is nil but FileSeeker.Read was just called") + } + callInfo := struct { + Bytes []byte + }{ + Bytes: bytes, + } + mock.lockRead.Lock() + mock.calls.Read = append(mock.calls.Read, callInfo) + mock.lockRead.Unlock() + return mock.ReadFunc(bytes) +} + +// ReadCalls gets all the calls that were made to Read. +// Check the length with: +// +// len(mockedFileSeeker.ReadCalls()) +func (mock *FileSeekerMock) ReadCalls() []struct { + Bytes []byte +} { + var calls []struct { + Bytes []byte + } + mock.lockRead.RLock() + calls = mock.calls.Read + mock.lockRead.RUnlock() + return calls +} + +// Seek calls SeekFunc. +func (mock *FileSeekerMock) Seek(offset int64, whence int) (int64, error) { + if mock.SeekFunc == nil { + panic("FileSeekerMock.SeekFunc: method is nil but FileSeeker.Seek was just called") + } + callInfo := struct { + Offset int64 + Whence int + }{ + Offset: offset, + Whence: whence, + } + mock.lockSeek.Lock() + mock.calls.Seek = append(mock.calls.Seek, callInfo) + mock.lockSeek.Unlock() + return mock.SeekFunc(offset, whence) +} + +// SeekCalls gets all the calls that were made to Seek. +// Check the length with: +// +// len(mockedFileSeeker.SeekCalls()) +func (mock *FileSeekerMock) SeekCalls() []struct { + Offset int64 + Whence int +} { + var calls []struct { + Offset int64 + Whence int + } + mock.lockSeek.RLock() + calls = mock.calls.Seek + mock.lockSeek.RUnlock() + return calls +} + +// Stat calls StatFunc. +func (mock *FileSeekerMock) Stat() (fs.FileInfo, error) { + if mock.StatFunc == nil { + panic("FileSeekerMock.StatFunc: method is nil but FileSeeker.Stat was just called") + } + callInfo := struct { + }{} + mock.lockStat.Lock() + mock.calls.Stat = append(mock.calls.Stat, callInfo) + mock.lockStat.Unlock() + return mock.StatFunc() +} + +// StatCalls gets all the calls that were made to Stat. +// Check the length with: +// +// len(mockedFileSeeker.StatCalls()) +func (mock *FileSeekerMock) StatCalls() []struct { +} { + var calls []struct { + } + mock.lockStat.RLock() + calls = mock.calls.Stat + mock.lockStat.RUnlock() + return calls +} diff --git a/tools/pkg/walker/mocks/reverse.go b/tools/pkg/walker/mocks/reverse.go new file mode 100644 index 00000000..232a7dd0 --- /dev/null +++ b/tools/pkg/walker/mocks/reverse.go @@ -0,0 +1,87 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package mocks + +import ( + "github.com/input-output-hk/catalyst-forge/tools/pkg/walker" + "sync" +) + +// Ensure, that ReverseWalkerMock does implement walker.ReverseWalker. +// If this is not the case, regenerate this file with moq. +var _ walker.ReverseWalker = &ReverseWalkerMock{} + +// ReverseWalkerMock is a mock implementation of walker.ReverseWalker. +// +// func TestSomethingThatUsesReverseWalker(t *testing.T) { +// +// // make and configure a mocked walker.ReverseWalker +// mockedReverseWalker := &ReverseWalkerMock{ +// WalkFunc: func(startPath string, endPath string, callback walker.WalkerCallback) error { +// panic("mock out the Walk method") +// }, +// } +// +// // use mockedReverseWalker in code that requires walker.ReverseWalker +// // and then make assertions. +// +// } +type ReverseWalkerMock struct { + // WalkFunc mocks the Walk method. + WalkFunc func(startPath string, endPath string, callback walker.WalkerCallback) error + + // calls tracks calls to the methods. + calls struct { + // Walk holds details about calls to the Walk method. + Walk []struct { + // StartPath is the startPath argument value. + StartPath string + // EndPath is the endPath argument value. + EndPath string + // Callback is the callback argument value. + Callback walker.WalkerCallback + } + } + lockWalk sync.RWMutex +} + +// Walk calls WalkFunc. +func (mock *ReverseWalkerMock) Walk(startPath string, endPath string, callback walker.WalkerCallback) error { + if mock.WalkFunc == nil { + panic("ReverseWalkerMock.WalkFunc: method is nil but ReverseWalker.Walk was just called") + } + callInfo := struct { + StartPath string + EndPath string + Callback walker.WalkerCallback + }{ + StartPath: startPath, + EndPath: endPath, + Callback: callback, + } + mock.lockWalk.Lock() + mock.calls.Walk = append(mock.calls.Walk, callInfo) + mock.lockWalk.Unlock() + return mock.WalkFunc(startPath, endPath, callback) +} + +// WalkCalls gets all the calls that were made to Walk. +// Check the length with: +// +// len(mockedReverseWalker.WalkCalls()) +func (mock *ReverseWalkerMock) WalkCalls() []struct { + StartPath string + EndPath string + Callback walker.WalkerCallback +} { + var calls []struct { + StartPath string + EndPath string + Callback walker.WalkerCallback + } + mock.lockWalk.RLock() + calls = mock.calls.Walk + mock.lockWalk.RUnlock() + return calls +} diff --git a/forge/cli/pkg/walker/walker_mock.go b/tools/pkg/walker/mocks/walker.go similarity index 65% rename from forge/cli/pkg/walker/walker_mock.go rename to tools/pkg/walker/mocks/walker.go index 905a44b1..8a101d59 100644 --- a/forge/cli/pkg/walker/walker_mock.go +++ b/tools/pkg/walker/mocks/walker.go @@ -1,34 +1,35 @@ // Code generated by moq; DO NOT EDIT. // github.com/matryer/moq -package walker +package mocks import ( + "github.com/input-output-hk/catalyst-forge/tools/pkg/walker" "sync" ) -// Ensure, that WalkerMock does implement Walker. +// Ensure, that WalkerMock does implement walker.Walker. // If this is not the case, regenerate this file with moq. -var _ Walker = &WalkerMock{} +var _ walker.Walker = &WalkerMock{} -// WalkerMock is a mock implementation of Walker. +// WalkerMock is a mock implementation of walker.Walker. // // func TestSomethingThatUsesWalker(t *testing.T) { // -// // make and configure a mocked Walker +// // make and configure a mocked walker.Walker // mockedWalker := &WalkerMock{ -// WalkFunc: func(rootPath string, callback WalkerCallback) error { +// WalkFunc: func(rootPath string, callback walker.WalkerCallback) error { // panic("mock out the Walk method") // }, // } // -// // use mockedWalker in code that requires Walker +// // use mockedWalker in code that requires walker.Walker // // and then make assertions. // // } type WalkerMock struct { // WalkFunc mocks the Walk method. - WalkFunc func(rootPath string, callback WalkerCallback) error + WalkFunc func(rootPath string, callback walker.WalkerCallback) error // calls tracks calls to the methods. calls struct { @@ -37,20 +38,20 @@ type WalkerMock struct { // RootPath is the rootPath argument value. RootPath string // Callback is the callback argument value. - Callback WalkerCallback + Callback walker.WalkerCallback } } lockWalk sync.RWMutex } // Walk calls WalkFunc. -func (mock *WalkerMock) Walk(rootPath string, callback WalkerCallback) error { +func (mock *WalkerMock) Walk(rootPath string, callback walker.WalkerCallback) error { if mock.WalkFunc == nil { panic("WalkerMock.WalkFunc: method is nil but Walker.Walk was just called") } callInfo := struct { RootPath string - Callback WalkerCallback + Callback walker.WalkerCallback }{ RootPath: rootPath, Callback: callback, @@ -67,11 +68,11 @@ func (mock *WalkerMock) Walk(rootPath string, callback WalkerCallback) error { // len(mockedWalker.WalkCalls()) func (mock *WalkerMock) WalkCalls() []struct { RootPath string - Callback WalkerCallback + Callback walker.WalkerCallback } { var calls []struct { RootPath string - Callback WalkerCallback + Callback walker.WalkerCallback } mock.lockWalk.RLock() calls = mock.calls.Walk diff --git a/tools/pkg/walker/walker.go b/tools/pkg/walker/walker.go new file mode 100644 index 00000000..248ca004 --- /dev/null +++ b/tools/pkg/walker/walker.go @@ -0,0 +1,50 @@ +// Package walker provides implementations for walking over files in a +// filesystem. +package walker + +import ( + "io" + "io/fs" +) + +//go:generate go run github.com/matryer/moq@latest -pkg mocks -out mocks/fileseeker.go . FileSeeker +//go:generate go run github.com/matryer/moq@latest -pkg mocks -out mocks/reverse.go . ReverseWalker +//go:generate go run github.com/matryer/moq@latest -pkg mocks -out mocks/walker.go . Walker + +// FileType is an enum that represents the type of a file. +type FileType int + +const ( + // FileTypeFile represents a file. + FileTypeFile FileType = iota + + // FileTypeDir represents a directory. + FileTypeDir +) + +// FileSeeker is an interface that combines the fs.File and io.Seeker interfaces. +type FileSeeker interface { + fs.File + io.Seeker +} + +// WalkerCallback is a callback function that is called for each file in the +// Walk function. +type WalkerCallback func(string, FileType, func() (FileSeeker, error)) error + +// Walker is an interface that allows walking over a set of files. +type Walker interface { + // Walk walks over the files in the given root path and calls the given + // function for each file. + Walk(rootPath string, callback WalkerCallback) error +} + +// ReverseWalker is an interface that allows reverse walking over a set of +// files. +// The start path is the path where the walk starts and the end path is the path +// where the walk ends. +type ReverseWalker interface { + // Walk performs a reverse walk from the end path to the start path and + // calls the given function for each file. + Walk(startPath, endPath string, callback WalkerCallback) error +} diff --git a/blueprint/pkg/loader/walker_mock.go b/tools/pkg/walker/walker_mock.go similarity index 99% rename from blueprint/pkg/loader/walker_mock.go rename to tools/pkg/walker/walker_mock.go index 0f49282b..0cf3c8fe 100644 --- a/blueprint/pkg/loader/walker_mock.go +++ b/tools/pkg/walker/walker_mock.go @@ -1,7 +1,7 @@ // Code generated by moq; DO NOT EDIT. // github.com/matryer/moq -package loader +package walker import ( "sync" diff --git a/tools/pkg/walker/walker_test.go b/tools/pkg/walker/walker_test.go new file mode 100644 index 00000000..f1a4a26d --- /dev/null +++ b/tools/pkg/walker/walker_test.go @@ -0,0 +1,19 @@ +package walker + +import "github.com/spf13/afero" + +type wrapfs struct { + afero.Fs + + attempts int + failAfter int + trigger error +} + +func (w *wrapfs) Open(name string) (afero.File, error) { + w.attempts++ + if w.attempts == w.failAfter { + return nil, w.trigger + } + return afero.Fs.Open(w.Fs, name) +}