From 2dfd3cc227fdaa68e376dd850c72f54ece93833f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20Kivim=C3=A4ki?= Date: Fri, 15 Nov 2024 15:31:18 +0200 Subject: [PATCH] fix: improve path handling on Windows (#155) --- .github/workflows/test.yml | 6 +++- internal/cli/execute.go | 4 +-- internal/cli/why.go | 12 ++++---- pkg/recipe/execute.go | 3 +- test/execute_test.go | 6 ++++ test/features/check-file-origin.feature | 6 ++-- test/features/check-recipes.feature | 11 ++++++- test/features/execute-manifest.feature | 1 + test/features/execute-recipes.feature | 1 + .../features/recipes-as-oci-artifacts.feature | 13 ++++++-- test/features/upgrade-recipe.feature | 1 + test/main_test.go | 30 ++++++++++++------- 12 files changed, 67 insertions(+), 27 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a3e7406b..45177792 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,11 @@ on: jobs: test: name: Run tests - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/internal/cli/execute.go b/internal/cli/execute.go index 0d903502..c7182eeb 100644 --- a/internal/cli/execute.go +++ b/internal/cli/execute.go @@ -212,7 +212,7 @@ func executeRecipe(cmd *cobra.Command, opts executeOptions, re *recipe.Recipe) e return fmt.Errorf("%w\n\n%s", err, retryMessage) } - sauce.SubPath = opts.Subpath + sauce.SubPath = filepath.ToSlash(opts.Subpath) // Automatically add recipe origin if the recipe was remote if recipe.DetermineRecipeURLType(opts.RecipeURL) == recipe.OCIType { @@ -240,7 +240,7 @@ func executeRecipe(cmd *cobra.Command, opts executeOptions, re *recipe.Recipe) e if opts.Subpath != "" { files = make(map[string]recipe.File, len(sauce.Files)) for path, file := range sauce.Files { - files[filepath.Join(opts.Subpath, path)] = file + files[filepath.ToSlash(filepath.Join(opts.Subpath, path))] = file } } diff --git a/internal/cli/why.go b/internal/cli/why.go index e970d919..da6c8a17 100644 --- a/internal/cli/why.go +++ b/internal/cli/why.go @@ -78,13 +78,13 @@ func runWhy(cmd *cobra.Command, opts whyOptions) error { for _, sauce := range sauces { for file := range sauce.Files { - if fileinfo.IsDir() { - if strings.HasPrefix(file, opts.Filepath) { - cmd.Printf("Directory '%s' is created by the recipe '%s' (sauce ID %s).\n", opts.Filepath, sauce.Recipe.Name, sauce.ID) - return nil - } + cleanedFilePath := filepath.Clean(file) + if fileinfo.IsDir() && strings.HasPrefix(cleanedFilePath, opts.Filepath) { + cmd.Printf("Directory '%s' is created by the recipe '%s' (sauce ID %s).\n", opts.Filepath, sauce.Recipe.Name, sauce.ID) + return nil } - if opts.Filepath == file { + + if opts.Filepath == cleanedFilePath { // TODO: Check if the file is modified by the user by comparing hashes cmd.Printf("File '%s' is created by the recipe '%s' (sauce ID %s).\n", opts.Filepath, sauce.Recipe.Name, sauce.ID) return nil diff --git a/pkg/recipe/execute.go b/pkg/recipe/execute.go index 207533b6..107bc7cd 100644 --- a/pkg/recipe/execute.go +++ b/pkg/recipe/execute.go @@ -3,6 +3,7 @@ package recipe import ( "errors" "maps" + "path/filepath" "strings" "github.com/gofrs/uuid" @@ -72,7 +73,7 @@ func (re *Recipe) Execute(engine RenderEngine, values VariableValues, id uuid.UU continue } - filename = strings.TrimSuffix(filename, re.TemplateExtension) + filename = filepath.ToSlash(strings.TrimSuffix(filename, re.TemplateExtension)) sauce.Files[filename] = NewFile(content) idx += 1 diff --git a/test/execute_test.go b/test/execute_test.go index f27dcb1b..05a206ef 100644 --- a/test/execute_test.go +++ b/test/execute_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "path/filepath" + "runtime" "strings" "github.com/cucumber/godog" @@ -63,6 +64,11 @@ func iExecuteRemoteRecipe(ctx context.Context, repository string) (context.Conte func recipesWillBeExecutedToTheSubPath(ctx context.Context, path string) (context.Context, error) { additionalFlags := ctx.Value(cmdAdditionalFlagsCtxKey{}).(map[string]string) + + if runtime.GOOS == "windows" && path[0] == '/' { + path = filepath.Clean(filepath.Join("C:/", path[1:])) + } + additionalFlags["subpath"] = path return ctx, nil diff --git a/test/features/check-file-origin.feature b/test/features/check-file-origin.feature index 32ff79e7..d1ad31d9 100644 --- a/test/features/check-file-origin.feature +++ b/test/features/check-file-origin.feature @@ -12,7 +12,7 @@ Feature: Check origin of a file in project directory using "why" command And recipe "foo" generates file "foo/bar.yml" with content "initial" And I execute recipe "foo" When I check why the file "foo/bar.yml" is created - Then CLI produced an output "File 'foo/bar.yml' is created by the recipe 'foo'" + Then CLI produced an output "File 'foo[/|\\]bar.yml' is created by the recipe 'foo'" Scenario: Directory generated by a recipe Given a recipe "foo" @@ -33,11 +33,11 @@ Feature: Check origin of a file in project directory using "why" command And recipe "foo" generates file "README.md" with content "initial" And I execute recipe "foo" When I check why the file ".jalapeno/sauces.yml" is created - Then CLI produced an output "File '.jalapeno/sauces.yml' is created by Jalapeno" + Then CLI produced an output "File '.jalapeno[/|\\]sauces.yml' is created by Jalapeno" Scenario: File not found Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" And I execute recipe "foo" When I check why the file "not-found" is created - Then CLI produced an error "file '.*/not-found' does not exist" + Then CLI produced an error "file '.*[/|\\]not-found' does not exist" diff --git a/test/features/check-recipes.feature b/test/features/check-recipes.feature index e5ad3444..49640a21 100644 --- a/test/features/check-recipes.feature +++ b/test/features/check-recipes.feature @@ -1,5 +1,5 @@ Feature: Check for new recipe versions - + @docker Scenario: Find newer version for a recipe Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" @@ -16,6 +16,7 @@ Feature: Check for new recipe versions Then CLI produced an output "new versions found: v0\.0\.2" Then CLI produced an output "To upgrade recipes to the latest version run:\n (.*) upgrade oci://localhost:\d+/foo:v0.0.2\n" + @docker Scenario: Find multiple newer version for a recipe Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" @@ -33,6 +34,7 @@ Feature: Check for new recipe versions Then CLI produced an output "new versions found: v0\.0\.2, v0\.0\.3" Then CLI produced an output "To upgrade recipes to the latest version run:\n (.*) upgrade oci://localhost:\d+/foo:v0\.0\.3\n" + @docker Scenario: Find newer version for multiple recipes Given a recipe "foo" And recipe "foo" generates file "foo.md" with content "initial" @@ -59,6 +61,7 @@ Feature: Check for new recipe versions Then CLI produced an output "foo: new versions found: v0\.0\.2" And CLI produced an output "bar: new versions found: v0\.0\.2" + @docker Scenario: Unable to find newer recipe versions Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" @@ -73,6 +76,7 @@ Feature: Check for new recipe versions And I check new versions for recipe "foo" Then CLI produced an output "no new versions found" + @docker Scenario: Unable to find newer recipe versions for all recipes Given a recipe "foo" And recipe "foo" generates file "foo.md" with content "initial" @@ -95,6 +99,7 @@ Feature: Check for new recipe versions Then CLI produced an output "foo: new versions found: v0\.0\.2" And CLI produced an output "bar: no new versions found" + @docker Scenario: Executing remote recipe automatically adds the repo as source for the sauce Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" @@ -108,6 +113,7 @@ Feature: Check for new recipe versions And I check new versions for recipe "foo" Then CLI produced an output "new versions found: v0\.0\.2" + @docker Scenario: Manually override the check from URL for locally executed recipe Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" @@ -121,6 +127,7 @@ Feature: Check for new recipe versions Then CLI produced an output "new versions found: v0\.0\.2" And the sauce in index 0 which should have property "CheckFrom" with value "^oci://localhost:\d+/foo$" + @docker Scenario: Find and upgrade newer version for recipes Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" @@ -136,6 +143,7 @@ Feature: Check for new recipe versions Then CLI produced an output "new versions found: v0\.0\.2" Then CLI produced an output "Upgrade completed" + @docker Scenario: Find and upgrade newer version for a specific recipe Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" @@ -157,6 +165,7 @@ Feature: Check for new recipe versions Then CLI produced an output "new versions found: v0\.0\.2" Then CLI produced an output "Upgrade completed" + @docker Scenario: Find and upgrade newer versions for multiple recipes Given a recipe "foo" And recipe "foo" generates file "foo.md" with content "initial" diff --git a/test/features/execute-manifest.feature b/test/features/execute-manifest.feature index 8de203fb..4c896a5e 100644 --- a/test/features/execute-manifest.feature +++ b/test/features/execute-manifest.feature @@ -17,6 +17,7 @@ Feature: Execute manifests And the project directory should contain file "foo.md" And the project directory should contain file "bar.md" + @docker Scenario: Execute a manifest with remote recipes Given a local OCI registry And a recipe "foo" diff --git a/test/features/execute-recipes.feature b/test/features/execute-recipes.feature index 2242b248..a2aaaea0 100644 --- a/test/features/execute-recipes.feature +++ b/test/features/execute-recipes.feature @@ -10,6 +10,7 @@ Feature: Execute recipes And the sauce in index 0 which should have property "Recipe.Name" with value "^foo$" And the sauce in index 0 which has a valid ID + @docker Scenario: Execute single recipe from remote registry Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" diff --git a/test/features/recipes-as-oci-artifacts.feature b/test/features/recipes-as-oci-artifacts.feature index b020854c..eb62001c 100644 --- a/test/features/recipes-as-oci-artifacts.feature +++ b/test/features/recipes-as-oci-artifacts.feature @@ -2,13 +2,14 @@ Feature: Recipes as OCI artifacts By pushing and pulling recipes as artifacts to OCI compatible repositories, we can improve recipe availability and discoverability + @docker Scenario: Push a recipe to OCI repository Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" And a local OCI registry When I push the recipe "foo" to the local OCI repository Then no errors were printed - + @docker Scenario: Pull a recipe from OCI repository Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" @@ -18,6 +19,7 @@ Feature: Recipes as OCI artifacts Then no errors were printed And the project directory should contain file "foo/recipe.yml" + @docker Scenario: Push a recipe to OCI repository using the 'latest' tag Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" @@ -28,6 +30,7 @@ Feature: Recipes as OCI artifacts Then no errors were printed And the project directory should contain file "foo/recipe.yml" with "version: v0.0.1" + @docker Scenario: Pushing a recipe to OCI repository using the 'latest' tag pushes the version tag also Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" @@ -38,6 +41,7 @@ Feature: Recipes as OCI artifacts Then no errors were printed And the project directory should contain file "foo/recipe.yml" with "version: v0.0.1" + @docker Scenario: Pushing a recipe to OCI repository using the 'latest' tag replaces the previous tag Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" @@ -51,6 +55,7 @@ Feature: Recipes as OCI artifacts Then no errors were printed And the project directory should contain file "foo/recipe.yml" with "version: v0.0.2" + @docker Scenario: Push a recipe to OCI repository with authentication Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" @@ -58,6 +63,7 @@ Feature: Recipes as OCI artifacts When I push the recipe "foo" to the local OCI repository Then no errors were printed + @docker Scenario: Pull a recipe from OCI repository with authentication Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" @@ -66,7 +72,8 @@ Feature: Recipes as OCI artifacts When I pull recipe from the local OCI repository "foo:v0.0.1" Then no errors were printed And the project directory should contain file "foo/recipe.yml" - + + @docker Scenario: Try to push a recipe to OCI repository without authentication Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" @@ -75,11 +82,13 @@ Feature: Recipes as OCI artifacts When I push the recipe "foo" to the local OCI repository Then CLI produced an error "basic credential not found" + @docker Scenario: Try to pull a recipe from OCI repository which not exist Given a local OCI registry with authentication When I pull recipe from the local OCI repository "foo:v0.0.1" Then CLI produced an error "recipe not found" + @docker Scenario: Push a recipe from OCI repository using credentials from config file Given a recipe "foo" And recipe "foo" generates file "README.md" with content "initial" diff --git a/test/features/upgrade-recipe.feature b/test/features/upgrade-recipe.feature index 526a558d..829a77eb 100644 --- a/test/features/upgrade-recipe.feature +++ b/test/features/upgrade-recipe.feature @@ -95,6 +95,7 @@ Feature: Upgrade sauce And the project directory should contain file "./foo/README.md" with "New version" And the project directory should contain file "./bar/README.md" with "initial" + @docker Scenario: Upgrade sauce from remote recipe Given a local OCI registry And a recipe "foo" diff --git a/test/main_test.go b/test/main_test.go index c9db8362..7390bf5d 100644 --- a/test/main_test.go +++ b/test/main_test.go @@ -67,14 +67,21 @@ const ( */ func TestFeatures(t *testing.T) { + opts := &godog.Options{ + Format: "pretty", + Strict: true, + Concurrency: runtime.NumCPU(), + Paths: []string{"features"}, + TestingT: t, + } + + // Skip tests needing OCI registry on Windows because there is no windows/amd64 image available + if runtime.GOOS == "windows" { + opts.Tags = "~@docker" + } + suite := godog.TestSuite{ - Options: &godog.Options{ - Format: "pretty", - Strict: true, - Concurrency: runtime.NumCPU(), - Paths: []string{"features"}, - TestingT: t, - }, + Options: opts, ScenarioInitializer: func(s *godog.ScenarioContext) { AddCommonSteps(s) @@ -335,7 +342,7 @@ func iRemoveFileFromTheRecipe(ctx context.Context, filename, recipeName string) } func aLocalOCIRegistry(ctx context.Context) (context.Context, error) { - resource, err := createLocalRegistry(&dockertest.RunOptions{Repository: "registry", Tag: "2"}) + resource, err := createLocalRegistry(&dockertest.RunOptions{Repository: "registry", Tag: "2", Platform: "linux/amd64"}) if err != nil { return ctx, err } @@ -360,6 +367,7 @@ func aLocalOCIRegistryWithAuth(ctx context.Context) (context.Context, error) { resource, err := createLocalRegistry(&dockertest.RunOptions{ Repository: "registry", Tag: "2", + Platform: "linux/amd64", Env: []string{ "REGISTRY_AUTH_HTPASSWD_REALM=jalapeno-test-realm", fmt.Sprintf("REGISTRY_AUTH_HTPASSWD_PATH=/auth/%s", HTPASSWD_FILENAME), @@ -429,7 +437,7 @@ func iClearTheOutput(ctx context.Context) (context.Context, error) { func theProjectDirectoryShouldContainFile(ctx context.Context, filename string) error { dir := ctx.Value(projectDirectoryPathCtxKey{}).(string) - info, err := os.Stat(filepath.Join(dir, filename)) + info, err := os.Stat(filepath.Join(dir, filepath.Clean(filename))) if err == nil && !info.Mode().IsRegular() { return fmt.Errorf("%s is not a regular file", filename) } @@ -438,11 +446,11 @@ func theProjectDirectoryShouldContainFile(ctx context.Context, filename string) func iCreateAFileWithContentsToTheProjectDir(ctx context.Context, filename, contents string) error { dir := ctx.Value(projectDirectoryPathCtxKey{}).(string) - return os.WriteFile(filepath.Join(dir, filename), []byte(contents), 0644) + return os.WriteFile(filepath.Join(dir, filepath.Clean(filename)), []byte(contents), 0644) } func theProjectDirectoryShouldContainFileWith(ctx context.Context, filename, searchTerm string) error { - content, err := readProjectDirectoryFile(ctx, filename) + content, err := readProjectDirectoryFile(ctx, filepath.Clean(filename)) if err != nil { return err }