Skip to content

Commit

Permalink
fix: improve path handling on Windows (#155)
Browse files Browse the repository at this point in the history
  • Loading branch information
majori committed Nov 15, 2024
1 parent 83e1963 commit 2dfd3cc
Show file tree
Hide file tree
Showing 12 changed files with 67 additions and 27 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions internal/cli/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}

Expand Down
12 changes: 6 additions & 6 deletions internal/cli/why.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion pkg/recipe/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package recipe
import (
"errors"
"maps"
"path/filepath"
"strings"

"github.com/gofrs/uuid"
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions test/execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"path/filepath"
"runtime"
"strings"

"github.com/cucumber/godog"
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions test/features/check-file-origin.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
11 changes: 10 additions & 1 deletion test/features/check-recipes.feature
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand Down
1 change: 1 addition & 0 deletions test/features/execute-manifest.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions test/features/execute-recipes.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 11 additions & 2 deletions test/features/recipes-as-oci-artifacts.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -51,13 +55,15 @@ 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"
And a local OCI registry with authentication
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"
Expand All @@ -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"
Expand All @@ -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"
Expand Down
1 change: 1 addition & 0 deletions test/features/upgrade-recipe.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
30 changes: 19 additions & 11 deletions test/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
}
Expand All @@ -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),
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
}
Expand Down

0 comments on commit 2dfd3cc

Please sign in to comment.