diff --git a/commands/platformify.go b/commands/platformify.go index 88a7711..2088631 100644 --- a/commands/platformify.go +++ b/commands/platformify.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" "io" + "os" + "path" "github.com/spf13/cobra" @@ -39,11 +41,7 @@ func Platformify(ctx context.Context, stdout, stderr io.Writer, assets *vendoriz answers := models.NewAnswers() answers.Flavor, _ = ctx.Value(FlavorKey).(string) ctx = models.ToContext(ctx, answers) - ctx = colors.ToContext( - ctx, - stdout, - stderr, - ) + ctx = colors.ToContext(ctx, stdout, stderr) q := questionnaire.New( &question.WorkingDirectory{}, &question.FilesOverwrite{}, @@ -76,12 +74,24 @@ func Platformify(ctx context.Context, stdout, stderr io.Writer, assets *vendoriz input := answers.ToUserInput() pfier := platformifier.New(input, assets.ConfigFlavor) - err = pfier.Platformify(ctx) + configFiles, err := pfier.Platformify(ctx) if err != nil { fmt.Fprintln(stderr, colors.Colorize(colors.ErrorCode, err.Error())) return fmt.Errorf("could not configure project: %w", err) } + for file, contents := range configFiles { + filePath := path.Join(answers.Cwd, file) + if err := os.MkdirAll(path.Dir(filePath), os.ModeDir|os.ModePerm); err != nil { + fmt.Fprintf(stderr, "Could not create parent directories of file %s: %s\n", file, err) + continue + } + if err := os.WriteFile(filePath, contents, 0o664); err != nil { + fmt.Fprintf(stderr, "Could not write file %s: %s\n", file, err) + continue + } + } + done := question.Done{} return done.Ask(ctx) } diff --git a/internal/question/application_root.go b/internal/question/application_root.go index 7a13c08..f1c8651 100644 --- a/internal/question/application_root.go +++ b/internal/question/application_root.go @@ -3,7 +3,6 @@ package question import ( "context" "path" - "path/filepath" "github.com/platformsh/platformify/internal/question/models" "github.com/platformsh/platformify/internal/utils" @@ -20,28 +19,28 @@ func (q *ApplicationRoot) Ask(ctx context.Context) error { for _, dm := range answers.DependencyManagers { switch dm { case models.Composer: - if composerPath := utils.FindFile(answers.WorkingDirectory, "composer.json"); composerPath != "" { - answers.ApplicationRoot, _ = filepath.Rel(answers.WorkingDirectory, path.Dir(composerPath)) + if composerPath := utils.FindFile(answers.WorkingDirectory, "", "composer.json"); composerPath != "" { + answers.ApplicationRoot = path.Dir(composerPath) return nil } case models.Npm, models.Yarn: - if packagePath := utils.FindFile(answers.WorkingDirectory, "package.json"); packagePath != "" { - answers.ApplicationRoot, _ = filepath.Rel(answers.WorkingDirectory, path.Dir(packagePath)) + if packagePath := utils.FindFile(answers.WorkingDirectory, "", "package.json"); packagePath != "" { + answers.ApplicationRoot = path.Dir(packagePath) return nil } case models.Poetry: - if pyProjectPath := utils.FindFile(answers.WorkingDirectory, "pyproject.toml"); pyProjectPath != "" { - answers.ApplicationRoot, _ = filepath.Rel(answers.WorkingDirectory, path.Dir(pyProjectPath)) + if pyProjectPath := utils.FindFile(answers.WorkingDirectory, "", "pyproject.toml"); pyProjectPath != "" { + answers.ApplicationRoot = path.Dir(pyProjectPath) return nil } case models.Pipenv: - if pipfilePath := utils.FindFile(answers.WorkingDirectory, "Pipfile"); pipfilePath != "" { - answers.ApplicationRoot, _ = filepath.Rel(answers.WorkingDirectory, path.Dir(pipfilePath)) + if pipfilePath := utils.FindFile(answers.WorkingDirectory, "", "Pipfile"); pipfilePath != "" { + answers.ApplicationRoot = path.Dir(pipfilePath) return nil } case models.Pip: - if requirementsPath := utils.FindFile(answers.WorkingDirectory, "requirements.txt"); requirementsPath != "" { - answers.ApplicationRoot, _ = filepath.Rel(answers.WorkingDirectory, path.Dir(requirementsPath)) + if requirementsPath := utils.FindFile(answers.WorkingDirectory, "", "requirements.txt"); requirementsPath != "" { + answers.ApplicationRoot = path.Dir(requirementsPath) return nil } } diff --git a/internal/question/build_steps.go b/internal/question/build_steps.go index f846af1..d13b7fc 100644 --- a/internal/question/build_steps.go +++ b/internal/question/build_steps.go @@ -3,7 +3,6 @@ package question import ( "context" "fmt" - "path" "path/filepath" "slices" @@ -74,8 +73,9 @@ func (q *BuildSteps) Ask(ctx context.Context) error { ) } if _, ok := utils.GetJSONValue( + answers.WorkingDirectory, []string{"scripts", "build"}, - path.Join(answers.WorkingDirectory, "package.json"), + "package.json", true, ); ok { if dm == models.Yarn { @@ -100,7 +100,8 @@ func (q *BuildSteps) Ask(ctx context.Context) error { switch answers.Stack { case models.Django: if managePyPath := utils.FindFile( - path.Join(answers.WorkingDirectory, answers.ApplicationRoot), + answers.WorkingDirectory, + answers.ApplicationRoot, managePyFile, ); managePyPath != "" { prefix := "" @@ -110,7 +111,7 @@ func (q *BuildSteps) Ask(ctx context.Context) error { prefix = "poetry run " } - managePyPath, _ = filepath.Rel(path.Join(answers.WorkingDirectory, answers.ApplicationRoot), managePyPath) + managePyPath, _ = filepath.Rel(answers.ApplicationRoot, managePyPath) assets, _ := vendorization.FromContext(ctx) answers.BuildSteps = append( answers.BuildSteps, diff --git a/internal/question/build_steps_test.go b/internal/question/build_steps_test.go index 6fd6979..4fcb575 100644 --- a/internal/question/build_steps_test.go +++ b/internal/question/build_steps_test.go @@ -4,6 +4,7 @@ import ( "context" "reflect" "testing" + "testing/fstest" "github.com/platformsh/platformify/internal/question/models" ) @@ -37,6 +38,7 @@ func TestBuildSteps_Ask(t *testing.T) { Dependencies: map[string]map[string]string{}, DependencyManagers: []models.DepManager{models.Yarn}, Environment: map[string]string{}, + WorkingDirectory: fstest.MapFS{}, }}, buildSteps: []string{"yarn", "yarn exec next build"}, wantErr: false, @@ -50,6 +52,7 @@ func TestBuildSteps_Ask(t *testing.T) { Dependencies: map[string]map[string]string{}, DependencyManagers: []models.DepManager{models.Npm}, Environment: map[string]string{}, + WorkingDirectory: fstest.MapFS{}, }}, buildSteps: []string{"npm i", "npm exec next build"}, wantErr: false, @@ -63,6 +66,7 @@ func TestBuildSteps_Ask(t *testing.T) { Dependencies: map[string]map[string]string{}, DependencyManagers: []models.DepManager{models.Bundler}, Environment: map[string]string{}, + WorkingDirectory: fstest.MapFS{}, }}, buildSteps: []string{"bundle install"}, wantErr: false, @@ -76,6 +80,7 @@ func TestBuildSteps_Ask(t *testing.T) { Dependencies: map[string]map[string]string{}, DependencyManagers: []models.DepManager{models.Bundler}, Environment: map[string]string{}, + WorkingDirectory: fstest.MapFS{}, }}, buildSteps: []string{"bundle install", "bundle exec rails assets:precompile"}, wantErr: false, diff --git a/internal/question/dependency_manager.go b/internal/question/dependency_manager.go index 22a4526..8c4bf57 100644 --- a/internal/question/dependency_manager.go +++ b/internal/question/dependency_manager.go @@ -58,27 +58,27 @@ func (q *DependencyManager) Ask(ctx context.Context) error { } }() - if exists := utils.FileExists(answers.WorkingDirectory, poetryLockFile); exists { + if exists := utils.FileExists(answers.WorkingDirectory, "", poetryLockFile); exists { answers.DependencyManagers = append(answers.DependencyManagers, models.Poetry) - } else if exists := utils.FileExists(answers.WorkingDirectory, pipenvLockFile); exists { + } else if exists := utils.FileExists(answers.WorkingDirectory, "", pipenvLockFile); exists { answers.DependencyManagers = append(answers.DependencyManagers, models.Pipenv) - } else if exists := utils.FileExists(answers.WorkingDirectory, pipLockFile); exists { + } else if exists := utils.FileExists(answers.WorkingDirectory, "", pipLockFile); exists { answers.DependencyManagers = append(answers.DependencyManagers, models.Pip) } - if exists := utils.FileExists(answers.WorkingDirectory, composerLockFile); exists { + if exists := utils.FileExists(answers.WorkingDirectory, "", composerLockFile); exists { answers.DependencyManagers = append(answers.DependencyManagers, models.Composer) answers.Dependencies["php"] = map[string]string{"composer/composer": "^2"} } - if exists := utils.FileExists(answers.WorkingDirectory, yarnLockFileName); exists { + if exists := utils.FileExists(answers.WorkingDirectory, "", yarnLockFileName); exists { answers.DependencyManagers = append(answers.DependencyManagers, models.Yarn) answers.Dependencies["nodejs"] = map[string]string{"yarn": "^1.22.0"} - } else if exists := utils.FileExists(answers.WorkingDirectory, npmLockFileName); exists { + } else if exists := utils.FileExists(answers.WorkingDirectory, "", npmLockFileName); exists { answers.DependencyManagers = append(answers.DependencyManagers, models.Npm) } - if exists := utils.FileExists(answers.WorkingDirectory, bundlerLockFile); exists { + if exists := utils.FileExists(answers.WorkingDirectory, "", bundlerLockFile); exists { answers.DependencyManagers = append(answers.DependencyManagers, models.Bundler) } diff --git a/internal/question/deploy_command.go b/internal/question/deploy_command.go index b7c6fe3..fcb4c00 100644 --- a/internal/question/deploy_command.go +++ b/internal/question/deploy_command.go @@ -3,7 +3,6 @@ package question import ( "context" "fmt" - "path" "path/filepath" "slices" @@ -21,9 +20,9 @@ func (q *DeployCommand) Ask(ctx context.Context) error { switch answers.Stack { case models.Django: - managePyPath := utils.FindFile(path.Join(answers.WorkingDirectory, answers.ApplicationRoot), managePyFile) + managePyPath := utils.FindFile(answers.WorkingDirectory, answers.ApplicationRoot, managePyFile) if managePyPath != "" { - managePyPath, _ = filepath.Rel(path.Join(answers.WorkingDirectory, answers.ApplicationRoot), managePyPath) + managePyPath, _ = filepath.Rel(answers.ApplicationRoot, managePyPath) prefix := "" if slices.Contains(answers.DependencyManagers, models.Pipenv) { prefix = "pipenv run " diff --git a/internal/question/done.go b/internal/question/done.go index a2b6007..a330a94 100644 --- a/internal/question/done.go +++ b/internal/question/done.go @@ -78,7 +78,7 @@ func (q *Done) Ask(ctx context.Context) error { ), ), ) - fmt.Fprintf(out, " $ git init %s\n", answers.WorkingDirectory) + fmt.Fprintf(out, " $ git init %s\n", answers.Cwd) fmt.Fprintln(out, " $ git add .") fmt.Fprintf(out, " $ git commit -m 'Add %s configuration files'\n", assets.ServiceName) fmt.Fprintf(out, " $ %s project:set-remote\n", assets.Binary) diff --git a/internal/question/files_overwrite.go b/internal/question/files_overwrite.go index 0de6f16..2806464 100644 --- a/internal/question/files_overwrite.go +++ b/internal/question/files_overwrite.go @@ -3,8 +3,7 @@ package question import ( "context" "fmt" - "os" - "path/filepath" + "io/fs" "github.com/AlecAivazis/survey/v2" @@ -30,7 +29,7 @@ func (q *FilesOverwrite) Ask(ctx context.Context) error { assets, _ := vendorization.FromContext(ctx) existingFiles := make([]string, 0, len(assets.ProprietaryFiles())) for _, p := range assets.ProprietaryFiles() { - if st, err := os.Stat(filepath.Join(answers.WorkingDirectory, p)); err == nil && !st.IsDir() { + if st, err := fs.Stat(answers.WorkingDirectory, p); err == nil && !st.IsDir() { existingFiles = append(existingFiles, p) } } @@ -40,7 +39,7 @@ func (q *FilesOverwrite) Ask(ctx context.Context) error { stderr, colors.Colorize( colors.WarningCode, - fmt.Sprintf("You are reconfiguring the project at %s.", answers.WorkingDirectory), + fmt.Sprintf("You are reconfiguring the project at %s.", answers.Cwd), ), ) fmt.Fprintln( diff --git a/internal/question/locations.go b/internal/question/locations.go index 8993e8f..ca66d54 100644 --- a/internal/question/locations.go +++ b/internal/question/locations.go @@ -2,7 +2,7 @@ package question import ( "context" - "path/filepath" + "path" "github.com/platformsh/platformify/internal/question/models" "github.com/platformsh/platformify/internal/utils" @@ -29,10 +29,9 @@ func (q *Locations) Ask(ctx context.Context) error { "passthru": "/index.php", "root": "", } - if indexPath := utils.FindFile(answers.WorkingDirectory, "index.php"); indexPath != "" { - indexRelPath, _ := filepath.Rel(answers.WorkingDirectory, indexPath) - if filepath.Dir(indexRelPath) != "." { - locations["root"] = filepath.Dir(indexRelPath) + if indexPath := utils.FindFile(answers.WorkingDirectory, "", "index.php"); indexPath != "" { + if path.Dir(indexPath) != "." { + locations["root"] = path.Dir(indexPath) } } answers.Locations["/"] = locations diff --git a/internal/question/models/answer.go b/internal/question/models/answer.go index 5d77a95..9bb9e26 100644 --- a/internal/question/models/answer.go +++ b/internal/question/models/answer.go @@ -2,6 +2,7 @@ package models import ( "encoding/json" + "io/fs" "os" "path/filepath" "strings" @@ -10,23 +11,24 @@ import ( ) type Answers struct { - Stack Stack `json:"stack"` - Flavor string `json:"flavor"` - Type RuntimeType `json:"type"` - Name string `json:"name"` - ApplicationRoot string `json:"application_root"` - Environment map[string]string `json:"environment"` - BuildSteps []string `json:"build_steps"` - WebCommand string `json:"web_command"` - SocketFamily SocketFamily `json:"socket_family"` - DeployCommand []string `json:"deploy_command"` - DependencyManagers []DepManager `json:"dependency_managers"` - Dependencies map[string]map[string]string `json:"dependencies"` - BuildFlavor string `json:"build_flavor"` - Disk string `json:"disk"` - Mounts map[string]map[string]string `json:"mounts"` - Services []Service `json:"services"` - WorkingDirectory string `json:"working_directory"` + Stack Stack `json:"stack"` + Flavor string `json:"flavor"` + Type RuntimeType `json:"type"` + Name string `json:"name"` + ApplicationRoot string `json:"application_root"` + Environment map[string]string `json:"environment"` + BuildSteps []string `json:"build_steps"` + WebCommand string `json:"web_command"` + SocketFamily SocketFamily `json:"socket_family"` + DeployCommand []string `json:"deploy_command"` + DependencyManagers []DepManager `json:"dependency_managers"` + Dependencies map[string]map[string]string `json:"dependencies"` + BuildFlavor string `json:"build_flavor"` + Disk string `json:"disk"` + Mounts map[string]map[string]string `json:"mounts"` + Services []Service `json:"services"` + WorkingDirectory fs.FS + Cwd string HasGit bool `json:"has_git"` FilesCreated []string `json:"files_created"` Locations map[string]map[string]interface{} `json:"locations"` @@ -115,7 +117,6 @@ func (a *Answers) ToUserInput() *platformifier.UserInput { return &platformifier.UserInput{ Stack: getStack(a.Stack), - Root: "", ApplicationRoot: filepath.Join(string(os.PathSeparator), a.ApplicationRoot), Name: a.Name, Type: a.Type.String(), diff --git a/internal/question/name.go b/internal/question/name.go index 78f0131..f8409f3 100644 --- a/internal/question/name.go +++ b/internal/question/name.go @@ -30,7 +30,7 @@ func (q *Name) Ask(ctx context.Context) error { } question := &survey.Input{ - Message: "Tell us your project's application name:", Default: slugify(path.Base(answers.WorkingDirectory)), + Message: "Tell us your project's application name:", Default: slugify(path.Base(answers.Cwd)), } var name string diff --git a/internal/question/stack.go b/internal/question/stack.go index 6d94a10..e21df01 100644 --- a/internal/question/stack.go +++ b/internal/question/stack.go @@ -3,8 +3,6 @@ package question import ( "context" "fmt" - "os" - "slices" "strings" "github.com/AlecAivazis/survey/v2" @@ -53,16 +51,16 @@ func (q *Stack) Ask(ctx context.Context) error { answers.Stack = models.GenericStack - hasSettingsPy := utils.FileExists(answers.WorkingDirectory, settingsPyFile) - hasManagePy := utils.FileExists(answers.WorkingDirectory, managePyFile) + hasSettingsPy := utils.FileExists(answers.WorkingDirectory, "", settingsPyFile) + hasManagePy := utils.FileExists(answers.WorkingDirectory, "", managePyFile) if hasSettingsPy && hasManagePy { answers.Stack = models.Django return nil } - rackPath := utils.FindFile(answers.WorkingDirectory, rackFile) + rackPath := utils.FindFile(answers.WorkingDirectory, "", rackFile) if rackPath != "" { - f, err := os.Open(rackPath) + f, err := answers.WorkingDirectory.Open(rackPath) if err == nil { defer f.Close() if ok, _ := utils.ContainsStringInFile(f, "Rails.application.load_server", true); ok { @@ -72,9 +70,9 @@ func (q *Stack) Ask(ctx context.Context) error { } } - requirementsPath := utils.FindFile(answers.WorkingDirectory, "requirements.txt") + requirementsPath := utils.FindFile(answers.WorkingDirectory, "", "requirements.txt") if requirementsPath != "" { - f, err := os.Open(requirementsPath) + f, err := answers.WorkingDirectory.Open(requirementsPath) if err == nil { defer f.Close() if ok, _ := utils.ContainsStringInFile(f, "flask", true); ok { @@ -84,78 +82,82 @@ func (q *Stack) Ask(ctx context.Context) error { } } - pyProjectPath := utils.FindFile(answers.WorkingDirectory, "pyproject.toml") + pyProjectPath := utils.FindFile(answers.WorkingDirectory, "", "pyproject.toml") if pyProjectPath != "" { - if _, ok := utils.GetTOMLValue([]string{"tool", "poetry", "dependencies", "flask"}, pyProjectPath, true); ok { + if _, ok := utils.GetTOMLValue(answers.WorkingDirectory, []string{"tool", "poetry", "dependencies", "flask"}, pyProjectPath, true); ok { answers.Stack = models.Flask return nil } } - pipfilePath := utils.FindFile(answers.WorkingDirectory, "Pipfile") + pipfilePath := utils.FindFile(answers.WorkingDirectory, "", "Pipfile") if pipfilePath != "" { - if _, ok := utils.GetTOMLValue([]string{"packages", "flask"}, pipfilePath, true); ok { + if _, ok := utils.GetTOMLValue(answers.WorkingDirectory, []string{"packages", "flask"}, pipfilePath, true); ok { answers.Stack = models.Flask return nil } } - composerJSONPaths := utils.FindAllFiles(answers.WorkingDirectory, composerJSONFile) + composerJSONPaths := utils.FindAllFiles(answers.WorkingDirectory, "", composerJSONFile) for _, composerJSONPath := range composerJSONPaths { - if _, ok := utils.GetJSONValue([]string{"require", "laravel/framework"}, composerJSONPath, true); ok { + if _, ok := utils.GetJSONValue(answers.WorkingDirectory, []string{"require", "laravel/framework"}, composerJSONPath, true); ok { answers.Stack = models.Laravel return nil } } - packageJSONPaths := utils.FindAllFiles(answers.WorkingDirectory, packageJSONFile) + packageJSONPaths := utils.FindAllFiles(answers.WorkingDirectory, "", packageJSONFile) for _, packageJSONPath := range packageJSONPaths { - if _, ok := utils.GetJSONValue([]string{"dependencies", "next"}, packageJSONPath, true); ok { + if _, ok := utils.GetJSONValue(answers.WorkingDirectory, []string{"dependencies", "next"}, packageJSONPath, true); ok { answers.Stack = models.NextJS return nil } - if _, ok := utils.GetJSONValue([]string{"dependencies", "@strapi/strapi"}, packageJSONPath, true); ok { + if _, ok := utils.GetJSONValue(answers.WorkingDirectory, []string{"dependencies", "@strapi/strapi"}, packageJSONPath, true); ok { answers.Stack = models.Strapi return nil } - if _, ok := utils.GetJSONValue([]string{"dependencies", "strapi"}, packageJSONPath, true); ok { + if _, ok := utils.GetJSONValue(answers.WorkingDirectory, []string{"dependencies", "strapi"}, packageJSONPath, true); ok { answers.Stack = models.Strapi return nil } - if _, ok := utils.GetJSONValue([]string{"dependencies", "express"}, packageJSONPath, true); ok { + if _, ok := utils.GetJSONValue(answers.WorkingDirectory, []string{"dependencies", "express"}, packageJSONPath, true); ok { answers.Stack = models.Express return nil } } - hasSymfonyLock := utils.FileExists(answers.WorkingDirectory, symfonyLockFile) + hasSymfonyLock := utils.FileExists(answers.WorkingDirectory, "", symfonyLockFile) hasSymfonyBundle := false hasIbexaDependencies := false hasShopwareDependencies := false for _, composerJSONPath := range composerJSONPaths { - if _, ok := utils.GetJSONValue([]string{"autoload", "psr-0", "shopware"}, composerJSONPath, true); ok { + if _, ok := utils.GetJSONValue(answers.WorkingDirectory, []string{"autoload", "psr-0", "shopware"}, composerJSONPath, true); ok { hasShopwareDependencies = true break } - if _, ok := utils.GetJSONValue([]string{"autoload", "psr-4", "shopware\\core\\"}, composerJSONPath, true); ok { + if _, ok := utils.GetJSONValue(answers.WorkingDirectory, []string{"autoload", "psr-4", "shopware\\core\\"}, composerJSONPath, true); ok { hasShopwareDependencies = true break } - if _, ok := utils.GetJSONValue([]string{"autoload", "psr-4", "shopware\\appbundle\\"}, composerJSONPath, true); ok { + if _, ok := utils.GetJSONValue(answers.WorkingDirectory, []string{"autoload", "psr-4", "shopware\\appbundle\\"}, composerJSONPath, true); ok { hasShopwareDependencies = true break } - if keywords, ok := utils.GetJSONValue([]string{"keywords"}, composerJSONPath, true); ok { - if keywordsVal, ok := keywords.([]string); ok && slices.Contains(keywordsVal, "shopware") { - hasShopwareDependencies = true - break + if keywords, ok := utils.GetJSONValue(answers.WorkingDirectory, []string{"keywords"}, composerJSONPath, true); ok { + if keywordsVal, ok := keywords.([]any); ok { + for _, kw := range keywordsVal { + if kwStr, ok := kw.(string); ok && kwStr == "shopware" { + hasShopwareDependencies = true + break + } + } } } - if requirements, ok := utils.GetJSONValue([]string{"require"}, composerJSONPath, true); ok { + if requirements, ok := utils.GetJSONValue(answers.WorkingDirectory, []string{"require"}, composerJSONPath, true); ok { if requirementsVal, requirementsOK := requirements.(map[string]interface{}); requirementsOK { if _, hasSymfonyFrameworkBundle := requirementsVal["symfony/framework-bundle"]; hasSymfonyFrameworkBundle { hasSymfonyBundle = true diff --git a/internal/question/web_command.go b/internal/question/web_command.go index 28fafdb..9036aec 100644 --- a/internal/question/web_command.go +++ b/internal/question/web_command.go @@ -3,7 +3,7 @@ package question import ( "context" "fmt" - "os" + "io/fs" "path" "path/filepath" "slices" @@ -54,14 +54,14 @@ func (q *WebCommand) Ask(ctx context.Context) error { pythonPath := "" wsgi := "app.wsgi" // try to find the wsgi.py file to change the default command - wsgiPath := utils.FindFile(path.Join(answers.WorkingDirectory, answers.ApplicationRoot), "wsgi.py") + wsgiPath := utils.FindFile(answers.WorkingDirectory, answers.ApplicationRoot, "wsgi.py") if wsgiPath != "" { wsgiParentDir := path.Base(path.Dir(wsgiPath)) wsgi = fmt.Sprintf("%s.wsgi", wsgiParentDir) // add the pythonpath if the wsgi.py file is not in the root of the app wsgiRel, _ := filepath.Rel( - path.Join(answers.WorkingDirectory, answers.ApplicationRoot), + answers.ApplicationRoot, path.Dir(path.Dir(wsgiPath)), ) if wsgiRel != "." { @@ -94,8 +94,9 @@ func (q *WebCommand) Ask(ctx context.Context) error { return nil case models.Strapi: if _, ok := utils.GetJSONValue( + answers.WorkingDirectory, []string{"scripts", "start"}, - path.Join(answers.WorkingDirectory, "package.json"), + "package.json", true, ); ok { if slices.Contains(answers.DependencyManagers, models.Yarn) { @@ -106,8 +107,9 @@ func (q *WebCommand) Ask(ctx context.Context) error { } case models.Express: if _, ok := utils.GetJSONValue( + answers.WorkingDirectory, []string{"scripts", "start"}, - path.Join(answers.WorkingDirectory, "package.json"), + "package.json", true, ); ok { if slices.Contains(answers.DependencyManagers, models.Yarn) { @@ -119,16 +121,16 @@ func (q *WebCommand) Ask(ctx context.Context) error { } if mainPath, ok := utils.GetJSONValue( + answers.WorkingDirectory, []string{"main"}, - path.Join(answers.WorkingDirectory, "package.json"), + "package.json", true, ); ok { answers.WebCommand = fmt.Sprintf("node %s", mainPath.(string)) return nil } - if indexFile := utils.FindFile(answers.WorkingDirectory, "index.js"); indexFile != "" { - indexFile, _ = filepath.Rel(answers.WorkingDirectory, indexFile) + if indexFile := utils.FindFile(answers.WorkingDirectory, "", "index.js"); indexFile != "" { answers.WebCommand = fmt.Sprintf("node %s", indexFile) return nil } @@ -136,7 +138,7 @@ func (q *WebCommand) Ask(ctx context.Context) error { appPath := "" // try to find the app.py, api.py or server.py files for _, name := range []string{"app.py", "server.py", "api.py"} { - if _, err := os.Stat(path.Join(answers.WorkingDirectory, name)); err == nil { + if _, err := fs.Stat(answers.WorkingDirectory, name); err == nil { appPath = fmt.Sprintf("'%s:app'", strings.TrimSuffix(name, ".py")) break } diff --git a/internal/question/working_directory.go b/internal/question/working_directory.go index db12e87..dfd89af 100644 --- a/internal/question/working_directory.go +++ b/internal/question/working_directory.go @@ -31,7 +31,8 @@ func (q *WorkingDirectory) Ask(ctx context.Context) error { if !ok { return nil } - answers.WorkingDirectory = cwd + answers.WorkingDirectory = os.DirFS(cwd) + answers.Cwd = cwd answers.HasGit = false var outBuf, errBuf bytes.Buffer @@ -84,7 +85,8 @@ func (q *WorkingDirectory) Ask(ctx context.Context) error { } if proceed { - answers.WorkingDirectory = gitRepoAbsPath + answers.WorkingDirectory = os.DirFS(gitRepoAbsPath) + answers.Cwd = gitRepoAbsPath answers.HasGit = true } } diff --git a/internal/utils/utils.go b/internal/utils/utils.go index d83b378..381c3bb 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -6,6 +6,7 @@ import ( "cmp" "encoding/json" "io" + "io/fs" "os" "path/filepath" "slices" @@ -21,36 +22,39 @@ var skipDirs = []string{ ".git", } -// FileExists checks if the file exists -func FileExists(searchPath, name string) bool { - return FindFile(searchPath, name) != "" +// FileExists checks if the file exists. +func FileExists(fileSystem fs.FS, searchPath, name string) bool { + return FindFile(fileSystem, searchPath, name) != "" } // FindFile searches for the file inside the path recursively -// and returns the full path of the file if found -// If multiple files exist, tries to return the one closest to root -func FindFile(searchPath, name string) string { - files := FindAllFiles(searchPath, name) +// and returns the full path of the file if found. +// If multiple files exist, tries to return the one closest to root. +func FindFile(fileSystem fs.FS, searchPath, name string) string { + files := FindAllFiles(fileSystem, searchPath, name) if len(files) == 0 { return "" } slices.SortFunc(files, func(a, b string) int { - return cmp.Compare(strings.Count(a, string(os.PathSeparator)), strings.Count(b, string(os.PathSeparator))) + return cmp.Compare(strings.Count(a, "/"), strings.Count(b, "/")) }) return files[0] } -// FindAllFiles searches for the file inside the path recursively and returns all matches -func FindAllFiles(searchPath, name string) []string { +// FindAllFiles searches for the file inside the path recursively and returns all matches. +func FindAllFiles(fileSystem fs.FS, searchPath, name string) []string { found := make([]string, 0) - _ = filepath.WalkDir(searchPath, func(p string, d os.DirEntry, err error) error { + if searchPath == "" { + searchPath = "." + } + _ = fs.WalkDir(fileSystem, searchPath, func(p string, d os.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { - // Skip vendor directories + // Skip vendor directories. if slices.Contains(skipDirs, d.Name()) { return filepath.SkipDir } @@ -67,7 +71,7 @@ func FindAllFiles(searchPath, name string) []string { return found } -func GetMapValue(keyPath []string, data map[string]interface{}) (value interface{}, ok bool) { +func GetMapValue(keyPath []string, data map[string]any) (value any, ok bool) { if len(keyPath) == 0 { return data, true } @@ -77,7 +81,7 @@ func GetMapValue(keyPath []string, data map[string]interface{}) (value interface return nil, false } - if data, ok = value.(map[string]interface{}); !ok { + if data, ok = value.(map[string]any); !ok { return nil, false } } @@ -89,15 +93,9 @@ func GetMapValue(keyPath []string, data map[string]interface{}) (value interface return value, true } -// GetJSONValue gets a value from a JSON file, by traversing the path given -func GetJSONValue(keyPath []string, filePath string, caseInsensitive bool) (value interface{}, ok bool) { - fin, err := os.Open(filePath) - if err != nil { - return nil, false - } - defer fin.Close() - - rawData, err := io.ReadAll(fin) +// GetJSONValue gets a value from a JSON file, by traversing the path given. +func GetJSONValue(fileSystem fs.FS, keyPath []string, filePath string, caseInsensitive bool) (value any, ok bool) { + rawData, err := fs.ReadFile(fileSystem, filePath) if err != nil { return nil, false } @@ -109,7 +107,7 @@ func GetJSONValue(keyPath []string, filePath string, caseInsensitive bool) (valu } } - var data map[string]interface{} + var data map[string]any err = json.Unmarshal(rawData, &data) if err != nil { return nil, false @@ -118,7 +116,7 @@ func GetJSONValue(keyPath []string, filePath string, caseInsensitive bool) (valu return GetMapValue(keyPath, data) } -// ContainsStringInFile checks if the given file contains the given string +// ContainsStringInFile checks if the given file contains the given string. func ContainsStringInFile(file io.Reader, target string, caseInsensitive bool) (bool, error) { if caseInsensitive { target = strings.ToLower(target) @@ -143,15 +141,9 @@ func ContainsStringInFile(file io.Reader, target string, caseInsensitive bool) ( return false, nil } -// GetTOMLValue gets a value from a TOML file, by traversing the path given -func GetTOMLValue(keyPath []string, filePath string, caseInsensitive bool) (value interface{}, ok bool) { - fin, err := os.Open(filePath) - if err != nil { - return nil, false - } - defer fin.Close() - - rawData, err := io.ReadAll(fin) +// GetTOMLValue gets a value from a TOML file, by traversing the path given. +func GetTOMLValue(fileSystem fs.FS, keyPath []string, filePath string, caseInsensitive bool) (value any, ok bool) { + rawData, err := fs.ReadFile(fileSystem, filePath) if err != nil { return nil, false } @@ -163,7 +155,7 @@ func GetTOMLValue(keyPath []string, filePath string, caseInsensitive bool) (valu } } - var data map[string]interface{} + var data map[string]any err = toml.Unmarshal(rawData, &data) if err != nil { return nil, false diff --git a/platformifier/django.go b/platformifier/django.go index 08d5cae..df60b6d 100644 --- a/platformifier/django.go +++ b/platformifier/django.go @@ -1,16 +1,15 @@ package platformifier import ( + "bytes" "context" "fmt" "io/fs" - "os" "path/filepath" "text/template" "github.com/Masterminds/sprig/v3" - "github.com/platformsh/platformify/internal/colors" "github.com/platformsh/platformify/internal/utils" "github.com/platformsh/platformify/vendorization" ) @@ -21,7 +20,7 @@ const ( importSettingsPshLine = "from .settings_psh import *" ) -func newDjangoPlatformifier(templates fs.FS, fileSystem FS) *djangoPlatformifier { +func newDjangoPlatformifier(templates, fileSystem fs.FS) *djangoPlatformifier { return &djangoPlatformifier{ templates: templates, fileSystem: fileSystem, @@ -30,64 +29,42 @@ func newDjangoPlatformifier(templates fs.FS, fileSystem FS) *djangoPlatformifier type djangoPlatformifier struct { templates fs.FS - fileSystem FS + fileSystem fs.FS } -func (p *djangoPlatformifier) Platformify(ctx context.Context, input *UserInput) error { - appRoot := filepath.Join(input.Root, input.ApplicationRoot) - if settingsPath := p.fileSystem.Find(appRoot, settingsPyFile, true); len(settingsPath) > 0 { - pshSettingsPath := filepath.Join(filepath.Dir(settingsPath[0]), settingsPshPyFile) - tpl, parseErr := template.New(settingsPshPyFile).Funcs(sprig.FuncMap()). - ParseFS(p.templates, settingsPshPyFile) - if parseErr != nil { - return fmt.Errorf("could not parse template: %w", parseErr) - } - pshSettingsFile, err := p.fileSystem.Create(pshSettingsPath) - if err != nil { - return err - } - defer pshSettingsFile.Close() - - assets, _ := vendorization.FromContext(ctx) - err = tpl.Execute(pshSettingsFile, templateData{input, assets}) - if err != nil { - return err - } +func (p *djangoPlatformifier) Platformify(ctx context.Context, input *UserInput) (map[string][]byte, error) { + files := make(map[string][]byte) + settingsPath := utils.FindFile(p.fileSystem, input.ApplicationRoot, settingsPyFile) + if settingsPath == "" { + return files, nil + } - // append from .settings_psh import * to the bottom of settings.py - settingsFile, err := p.fileSystem.Open(settingsPath[0], os.O_APPEND|os.O_RDWR, 0o644) - if err != nil { - return nil - } - defer settingsFile.Close() + pshSettingsPath := filepath.Join(filepath.Dir(settingsPath), settingsPshPyFile) + tpl, parseErr := template.New(settingsPshPyFile).Funcs(sprig.FuncMap()). + ParseFS(p.templates, settingsPshPyFile) + if parseErr != nil { + return nil, fmt.Errorf("could not parse template: %w", parseErr) + } - // Check if there is an import line in the file - found, err := utils.ContainsStringInFile(settingsFile, importSettingsPshLine, false) - if err != nil { - return err - } + pshSettingsBuffer := &bytes.Buffer{} + assets, _ := vendorization.FromContext(ctx) + if err := tpl.Execute(pshSettingsBuffer, templateData{input, assets}); err != nil { + return nil, fmt.Errorf("could not execute template: %w", err) + } + files[pshSettingsPath] = pshSettingsBuffer.Bytes() - if !found { - if _, err = settingsFile.Write([]byte("\n\n" + importSettingsPshLine + "\n")); err != nil { - out, _, ok := colors.FromContext(ctx) - if !ok { - return nil - } + // Append the import line to settings.py if not already present. + settingsFile, err := fs.ReadFile(p.fileSystem, settingsPath) + if err != nil { + return files, nil + } - fmt.Fprintf( - out, - colors.Colorize( - colors.WarningCode, - "We have created a %s file for you. Please add the following line to your %s file:\n", - ), - settingsPshPyFile, - settingsPyFile, - ) - fmt.Fprint(out, colors.Colorize(colors.WarningCode, " "+importSettingsPshLine+"\n")) - return nil - } + if !bytes.Contains(settingsFile, []byte(importSettingsPshLine)) { + b := bytes.NewBuffer(settingsFile) + if _, err := b.WriteString("\n\n" + importSettingsPshLine + "\n"); err == nil { + files[settingsPath] = b.Bytes() } } - return nil + return files, nil } diff --git a/platformifier/django_test.go b/platformifier/django_test.go index 2962b6d..78a4640 100644 --- a/platformifier/django_test.go +++ b/platformifier/django_test.go @@ -3,17 +3,12 @@ package platformifier import ( "context" "embed" - "errors" "io/fs" - "os" "testing" + "testing/fstest" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - - "github.com/platformsh/platformify/internal/utils" ) const ( @@ -25,99 +20,65 @@ var ( testDjangoTemplatesFS embed.FS ) -type PlatformifyDjangoSuiteTester struct { - suite.Suite - - cwd string - templates fs.FS - fileSystem *MockFS -} - -func (s *PlatformifyDjangoSuiteTester) SetupTest() { - ctrl := gomock.NewController(s.T()) - - s.fileSystem = NewMockFS(ctrl) - - cwd, err := os.Getwd() - require.NoError(s.T(), err) - s.cwd = cwd - +func TestDjangoPlatformifier_SuccessfulFileCreation(t *testing.T) { templates, err := fs.Sub(testDjangoTemplatesFS, djangoTemplatesDir) - require.NoError(s.T(), err) - s.templates = templates + require.NoError(t, err) + + // Provide a filesystem with a settings.py file. + fileSystem := fstest.MapFS{ + "settings.py": &fstest.MapFile{ + Data: []byte("# Django settings\nDEBUG = True\n"), + }, + } + input := &UserInput{WorkingDirectory: fileSystem} + + p := newDjangoPlatformifier(templates, fileSystem) + files, err := p.Platformify(context.Background(), input) + assert.NoError(t, err) + + // The PSH settings file should have been generated. + assert.Contains(t, files, settingsPshPyFile) + assert.NotEmpty(t, files[settingsPshPyFile]) + + // The settings.py file should contain the import line. + assert.Contains(t, files, settingsPyFile) + assert.Contains(t, string(files[settingsPyFile]), importSettingsPshLine) } -func (s *PlatformifyDjangoSuiteTester) TestSuccessfulFileCreation() { - // GIVEN mock buffers to store settings and PSH settings files - settingsBuff, settingsPSHBuff := &MockBuffer{}, &MockBuffer{} - // AND working directory is a current directory - input := &UserInput{WorkingDirectory: s.cwd} - // AND the settings.py file exists - s.fileSystem.EXPECT(). - Find("", settingsPyFile, true). - Return([]string{settingsPyFile}).Times(1) - s.fileSystem.EXPECT(). - Open(gomock.Eq(settingsPyFile), gomock.Any(), gomock.Any()). - Return(settingsBuff, nil).Times(1) - // AND creation of the PSH settings file returns no errors - s.fileSystem.EXPECT(). - Create(gomock.Eq(settingsPshPyFile)). - Return(settingsPSHBuff, nil).Times(1) - - // WHEN run config files creation - p := newDjangoPlatformifier(s.templates, s.fileSystem) - err := p.Platformify(context.Background(), input) - // THEN it doesn't return any errors - assert.NoError(s.T(), err) - // AND the buffer contains settings file - assert.NotEmpty(s.T(), settingsPSHBuff) - - // WHEN check if settings file contains the line that imported psh settings file - found, err := utils.ContainsStringInFile(settingsBuff, importSettingsPshLine, false) - // THEN it doesn't return any errors - assert.NoError(s.T(), err) - // AND the line is found - assert.True(s.T(), found) -} - -func (s *PlatformifyDjangoSuiteTester) TestSettingsFileNotFound() { - // GIVEN mock buffer to store PSH settings file - buff := &MockBuffer{} - // AND working directory is a current directory - input := &UserInput{WorkingDirectory: s.cwd} - // AND the settings.py file doesn't exist - s.fileSystem.EXPECT(). - Find("", settingsPyFile, true). - Return([]string{}).Times(1) - - // WHEN run config files creation - p := newDjangoPlatformifier(s.templates, s.fileSystem) - err := p.Platformify(context.Background(), input) - // THEN it doesn't return any errors - assert.NoError(s.T(), err) - // AND the buffer is empty - assert.Empty(s.T(), buff) -} - -func (s *PlatformifyDjangoSuiteTester) TestPSHSettingsFileCreationError() { - // GIVEN working directory is a current directory - input := &UserInput{WorkingDirectory: s.cwd} - // AND the settings.py file exists - s.fileSystem.EXPECT(). - Find("", settingsPyFile, true). - Return([]string{settingsPyFile}).Times(1) - // AND creating PSH settings file fails - s.fileSystem.EXPECT(). - Create(gomock.Eq(settingsPshPyFile)). - Return(nil, errors.New("")).Times(1) - - // WHEN run config files creation - p := newDjangoPlatformifier(s.templates, s.fileSystem) - err := p.Platformify(context.Background(), input) - // THEN it fails - assert.Error(s.T(), err) +func TestDjangoPlatformifier_SettingsFileNotFound(t *testing.T) { + templates, err := fs.Sub(testDjangoTemplatesFS, djangoTemplatesDir) + require.NoError(t, err) + + // Provide an empty filesystem (no settings.py). + fileSystem := fstest.MapFS{} + input := &UserInput{WorkingDirectory: fileSystem} + + p := newDjangoPlatformifier(templates, fileSystem) + files, err := p.Platformify(context.Background(), input) + // No error when settings.py is missing. + assert.NoError(t, err) + // No files should be generated. + assert.Empty(t, files) } -func TestPlatformifyDjangoSuite(t *testing.T) { - suite.Run(t, new(PlatformifyDjangoSuiteTester)) +func TestDjangoPlatformifier_SettingsAlreadyImported(t *testing.T) { + templates, err := fs.Sub(testDjangoTemplatesFS, djangoTemplatesDir) + require.NoError(t, err) + + // Provide a filesystem where settings.py already contains the import line. + fileSystem := fstest.MapFS{ + "settings.py": &fstest.MapFile{ + Data: []byte("# Django settings\n" + importSettingsPshLine + "\n"), + }, + } + input := &UserInput{WorkingDirectory: fileSystem} + + p := newDjangoPlatformifier(templates, fileSystem) + files, err := p.Platformify(context.Background(), input) + assert.NoError(t, err) + + // The PSH settings file should still be generated. + assert.Contains(t, files, settingsPshPyFile) + // The settings.py should NOT be in the output (no modification needed). + assert.NotContains(t, files, settingsPyFile) } diff --git a/platformifier/fs.go b/platformifier/fs.go deleted file mode 100644 index 9d60627..0000000 --- a/platformifier/fs.go +++ /dev/null @@ -1,90 +0,0 @@ -package platformifier - -import ( - "errors" - "io" - "io/fs" - "os" - "path" - "path/filepath" - "slices" - "strings" -) - -var skipDirs = []string{ - "vendor", - "node_modules", - ".next", - ".git", -} - -//go:generate mockgen -destination=fs_mock_test.go -package=platformifier -source=fs.go -type FS interface { - Create(name string) (io.WriteCloser, error) - Find(root, name string, firstMatch bool) []string - Open(name string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) -} - -func NewOSFileSystem(root string) *OSFileSystem { - return &OSFileSystem{ - root: root, - } -} - -type OSFileSystem struct { - root string -} - -func (f *OSFileSystem) Open(name string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { - return os.OpenFile(f.fullPath(name), flag, perm) -} - -func (f *OSFileSystem) Create(name string) (io.WriteCloser, error) { - filePath := f.fullPath(name) - if err := os.MkdirAll(path.Dir(filePath), os.ModeDir|os.ModePerm); err != nil { - return nil, err - } - - return os.Create(filePath) -} - -// Find searches for the file inside the path recursively and returns all matches -func (f *OSFileSystem) Find(root, name string, firstMatch bool) []string { - root = strings.TrimPrefix(root, "/") - if root == "" { - root = "." - } - found := make([]string, 0) - _ = fs.WalkDir(f.readonly(), root, func(p string, d os.DirEntry, err error) error { - if err != nil { - return err - } - - if d.IsDir() { - // Skip vendor directories - if slices.Contains(skipDirs, d.Name()) { - return filepath.SkipDir - } - return nil - } - - if d.Name() == name { - found = append(found, p) - if firstMatch { - return errors.New("found") - } - } - - return nil - }) - - return found -} - -func (f *OSFileSystem) readonly() fs.FS { - return os.DirFS(f.root) -} - -func (f *OSFileSystem) fullPath(name string) string { - return filepath.Join(f.root, name) -} diff --git a/platformifier/fs_mock_test.go b/platformifier/fs_mock_test.go deleted file mode 100644 index af7f785..0000000 --- a/platformifier/fs_mock_test.go +++ /dev/null @@ -1,80 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: fs.go - -// Package platformifier is a generated GoMock package. -package platformifier - -import ( - io "io" - os "os" - reflect "reflect" - - gomock "github.com/golang/mock/gomock" -) - -// MockFS is a mock of FS interface. -type MockFS struct { - ctrl *gomock.Controller - recorder *MockFSMockRecorder -} - -// MockFSMockRecorder is the mock recorder for MockFS. -type MockFSMockRecorder struct { - mock *MockFS -} - -// NewMockFS creates a new mock instance. -func NewMockFS(ctrl *gomock.Controller) *MockFS { - mock := &MockFS{ctrl: ctrl} - mock.recorder = &MockFSMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockFS) EXPECT() *MockFSMockRecorder { - return m.recorder -} - -// Create mocks base method. -func (m *MockFS) Create(name string) (io.WriteCloser, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", name) - ret0, _ := ret[0].(io.WriteCloser) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Create indicates an expected call of Create. -func (mr *MockFSMockRecorder) Create(name interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockFS)(nil).Create), name) -} - -// Find mocks base method. -func (m *MockFS) Find(root, name string, firstMatch bool) []string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Find", root, name, firstMatch) - ret0, _ := ret[0].([]string) - return ret0 -} - -// Find indicates an expected call of Find. -func (mr *MockFSMockRecorder) Find(root, name, firstMatch interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockFS)(nil).Find), root, name, firstMatch) -} - -// Open mocks base method. -func (m *MockFS) Open(name string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Open", name, flag, perm) - ret0, _ := ret[0].(io.ReadWriteCloser) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Open indicates an expected call of Open. -func (mr *MockFSMockRecorder) Open(name, flag, perm interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Open", reflect.TypeOf((*MockFS)(nil).Open), name, flag, perm) -} diff --git a/platformifier/generic.go b/platformifier/generic.go index f38fbc2..66a906a 100644 --- a/platformifier/generic.go +++ b/platformifier/generic.go @@ -3,33 +3,32 @@ package platformifier import ( "bytes" "context" - "fmt" - "io" "io/fs" "strings" "text/template" - "github.com/platformsh/platformify/vendorization" - "github.com/Masterminds/sprig/v3" + + "github.com/platformsh/platformify/vendorization" ) -func newGenericPlatformifier(templates fs.FS, fileSystem FS) *genericPlatformifier { +func newGenericPlatformifier(templates, fileSystem fs.FS) *genericPlatformifier { return &genericPlatformifier{ templates: templates, fileSystem: fileSystem, } } -// genericPlatformifier contains the configuration for the application to Platformify +// genericPlatformifier contains the configuration for the application to Platformify. type genericPlatformifier struct { templates fs.FS - fileSystem FS + fileSystem fs.FS } -// Platformify will generate the needed configuration files in the current directory. -func (p *genericPlatformifier) Platformify(ctx context.Context, input *UserInput) error { +// Platformify renders templates and returns the result as a file map. +func (p *genericPlatformifier) Platformify(ctx context.Context, input *UserInput) (map[string][]byte, error) { assets, _ := vendorization.FromContext(ctx) + files := make(map[string][]byte) err := fs.WalkDir(p.templates, ".", func(name string, d fs.DirEntry, _ error) error { if d.IsDir() { return nil @@ -40,26 +39,17 @@ func (p *genericPlatformifier) Platformify(ctx context.Context, input *UserInput return err } - // Skip empty files + // Skip empty files. if strings.TrimSpace(contents.String()) == "" { return nil } - f, writeErr := p.fileSystem.Create(name) - if writeErr != nil { - return fmt.Errorf("could not write template: %w", writeErr) - } - defer f.Close() - - if _, err := io.Copy(f, contents); err != nil { - return err - } - + files[name] = contents.Bytes() return nil }) if err != nil { - return err + return nil, err } - return nil + return files, nil } diff --git a/platformifier/generic_test.go b/platformifier/generic_test.go index 86db6f5..971173b 100644 --- a/platformifier/generic_test.go +++ b/platformifier/generic_test.go @@ -1,18 +1,14 @@ package platformifier import ( - "bytes" "context" "embed" - "errors" "io/fs" - "os" "testing" + "testing/fstest" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" ) const ( @@ -28,146 +24,36 @@ var ( testGenericTemplatesFS embed.FS ) -type MockBuffer struct { - bytes.Buffer -} - -func (b *MockBuffer) Close() error { - return nil -} - -type PlatformifyGenericSuiteTester struct { - suite.Suite - - cwd string - templates fs.FS - fileSystem *MockFS -} - -func (s *PlatformifyGenericSuiteTester) SetupTest() { - ctrl := gomock.NewController(s.T()) - - cwd, err := os.Getwd() - require.NoError(s.T(), err) - s.cwd = cwd - +func TestGenericPlatformifier_SuccessfulConfigsCreation(t *testing.T) { templates, err := fs.Sub(testGenericTemplatesFS, genericTemplatesDir) - require.NoError(s.T(), err) - s.templates = templates + require.NoError(t, err) - s.fileSystem = NewMockFS(ctrl) -} + input := &UserInput{WorkingDirectory: fstest.MapFS{}} + p := newGenericPlatformifier(templates, fstest.MapFS{}) + files, err := p.Platformify(context.Background(), input) + assert.NoError(t, err) -func (s *PlatformifyGenericSuiteTester) TestSuccessfulConfigsCreation() { - // GIVEN mock buffers to store config files - envBuff, appBuff, routesBuff, servicesBuff := &MockBuffer{}, &MockBuffer{}, &MockBuffer{}, &MockBuffer{} - // AND working directory is a current directory - input := &UserInput{WorkingDirectory: s.cwd} - // AND creation of the environment file returns no errors - s.fileSystem.EXPECT(). - Create(gomock.Eq(environmentFile)). - Return(envBuff, nil).Times(1) - // AND creation of the app config file returns no errors - s.fileSystem.EXPECT(). - Create(gomock.Eq(appConfigFile)). - Return(appBuff, nil).Times(1) - // AND creation of the routes config file returns no errors - s.fileSystem.EXPECT(). - Create(gomock.Eq(routesConfigFile)). - Return(routesBuff, nil).Times(1) - // AND creation of the services config file returns no errors - s.fileSystem.EXPECT(). - Create(gomock.Eq(servicesConfigFile)). - Return(servicesBuff, nil).Times(1) + // The returned file map should contain the expected config files. + assert.Contains(t, files, environmentFile) + assert.Contains(t, files, appConfigFile) + assert.Contains(t, files, routesConfigFile) + assert.Contains(t, files, servicesConfigFile) - // WHEN run config files creation - p := newGenericPlatformifier(s.templates, s.fileSystem) - err := p.Platformify(context.Background(), input) - // THEN it doesn't return any errors - assert.NoError(s.T(), err) - // AND the buffers contain configs - assert.NotEmpty(s.T(), envBuff) - assert.NotEmpty(s.T(), appBuff) - assert.NotEmpty(s.T(), routesBuff) - assert.NotEmpty(s.T(), servicesBuff) + // Each file should have non-empty content. + assert.NotEmpty(t, files[environmentFile]) + assert.NotEmpty(t, files[appConfigFile]) + assert.NotEmpty(t, files[routesConfigFile]) + assert.NotEmpty(t, files[servicesConfigFile]) } -func (s *PlatformifyGenericSuiteTester) TestEnvironmentCreationError() { - // GIVEN working directory is a current directory - input := &UserInput{WorkingDirectory: s.cwd} - // AND creating environment file fails - s.fileSystem.EXPECT(). - Create(gomock.Eq(environmentFile)). - Return(nil, errors.New("")).Times(1) - // AND creating other config files work fine - s.fileSystem.EXPECT(). - Create(gomock.Any()). - Return(&MockBuffer{}, nil).AnyTimes() - - // WHEN run config files creation - p := newGenericPlatformifier(s.templates, s.fileSystem) - err := p.Platformify(context.Background(), input) - // THEN it fails - assert.Error(s.T(), err) -} - -func (s *PlatformifyGenericSuiteTester) TestAppConfigCreationError() { - // GIVEN working directory is a current directory - input := &UserInput{WorkingDirectory: s.cwd} - // AND creating app config file fails - s.fileSystem.EXPECT(). - Create(gomock.Eq(appConfigFile)). - Return(nil, errors.New("")).Times(1) - // AND creating other config files work fine - s.fileSystem.EXPECT(). - Create(gomock.Any()). - Return(&MockBuffer{}, nil).AnyTimes() - - // WHEN run config files creation - p := newGenericPlatformifier(s.templates, s.fileSystem) - err := p.Platformify(context.Background(), input) - // THEN it fails - assert.Error(s.T(), err) -} - -func (s *PlatformifyGenericSuiteTester) TestRoutesConfigCreationError() { - // GIVEN working directory is a current directory - input := &UserInput{WorkingDirectory: s.cwd} - // AND creating routes config file fails - s.fileSystem.EXPECT(). - Create(gomock.Eq(routesConfigFile)). - Return(nil, errors.New("")).Times(1) - // AND creating other config files work fine - s.fileSystem.EXPECT(). - Create(gomock.Any()). - Return(&MockBuffer{}, nil).AnyTimes() - - // WHEN run config files creation - p := newGenericPlatformifier(s.templates, s.fileSystem) - err := p.Platformify(context.Background(), input) - // THEN it fails - assert.Error(s.T(), err) -} - -func (s *PlatformifyGenericSuiteTester) TestServicesConfigCreationError() { - // GIVEN working directory is a current directory - input := &UserInput{WorkingDirectory: s.cwd} - // AND creating services config file fails - s.fileSystem.EXPECT(). - Create(gomock.Eq(servicesConfigFile)). - Return(nil, errors.New("")).Times(1) - // AND creating other config files work fine - s.fileSystem.EXPECT(). - Create(gomock.Any()). - Return(&MockBuffer{}, nil).AnyTimes() - - // WHEN run config files creation - p := newGenericPlatformifier(s.templates, s.fileSystem) - err := p.Platformify(context.Background(), input) - // THEN it fails - assert.Error(s.T(), err) -} - -func TestPlatformifyGenericSuite(t *testing.T) { - suite.Run(t, new(PlatformifyGenericSuiteTester)) +func TestGenericPlatformifier_EmptyInput(t *testing.T) { + templates, err := fs.Sub(testGenericTemplatesFS, genericTemplatesDir) + require.NoError(t, err) + + // With minimal input, Platformify should still succeed (templates render without error). + input := &UserInput{} + p := newGenericPlatformifier(templates, fstest.MapFS{}) + files, err := p.Platformify(context.Background(), input) + assert.NoError(t, err) + assert.NotNil(t, files) } diff --git a/platformifier/laravel.go b/platformifier/laravel.go index 7ffd5c6..1adcb3d 100644 --- a/platformifier/laravel.go +++ b/platformifier/laravel.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io/fs" - "path/filepath" "github.com/platformsh/platformify/internal/colors" "github.com/platformsh/platformify/internal/utils" @@ -14,7 +13,7 @@ const ( composerJSONFile = "composer.json" ) -func newLaravelPlatformifier(templates fs.FS, fileSystem FS) *laravelPlatformifier { +func newLaravelPlatformifier(templates, fileSystem fs.FS) *laravelPlatformifier { return &laravelPlatformifier{ templates: templates, fileSystem: fileSystem, @@ -23,19 +22,23 @@ func newLaravelPlatformifier(templates fs.FS, fileSystem FS) *laravelPlatformifi type laravelPlatformifier struct { templates fs.FS - fileSystem FS + fileSystem fs.FS } -func (p *laravelPlatformifier) Platformify(ctx context.Context, input *UserInput) error { +func (p *laravelPlatformifier) Platformify(ctx context.Context, input *UserInput) (map[string][]byte, error) { // Check for the Laravel Bridge. - appRoot := filepath.Join(input.Root, input.ApplicationRoot) - composerJSONPaths := p.fileSystem.Find(appRoot, composerJSONFile, false) + composerJSONPaths := utils.FindAllFiles(p.fileSystem, input.ApplicationRoot, composerJSONFile) for _, composerJSONPath := range composerJSONPaths { - _, required := utils.GetJSONValue([]string{"require", "platformsh/laravel-bridge"}, composerJSONPath, true) + _, required := utils.GetJSONValue( + p.fileSystem, + []string{"require", "platformsh/laravel-bridge"}, + composerJSONPath, + true, + ) if !required { out, _, ok := colors.FromContext(ctx) if !ok { - return fmt.Errorf("output context failed") + return nil, fmt.Errorf("output context failed") } var suggest = "\nPlease use composer to add the Laravel Bridge to your project:\n" @@ -44,5 +47,5 @@ func (p *laravelPlatformifier) Platformify(ctx context.Context, input *UserInput } } - return nil + return map[string][]byte{}, nil } diff --git a/platformifier/models.go b/platformifier/models.go index 6816261..596f556 100644 --- a/platformifier/models.go +++ b/platformifier/models.go @@ -1,6 +1,7 @@ package platformifier import ( + "io/fs" "strings" ) @@ -62,7 +63,6 @@ type Relationship struct { // UserInput contains the configuration from user input. type UserInput struct { Stack Stack - Root string ApplicationRoot string Name string Type string @@ -73,14 +73,14 @@ type UserInput struct { SocketFamily string DeployCommand []string DependencyManagers []string - Locations map[string]map[string]interface{} + Locations map[string]map[string]any Dependencies map[string]map[string]string BuildFlavor string Disk string Mounts map[string]map[string]string Services []Service Relationships map[string]Relationship - WorkingDirectory string + WorkingDirectory fs.FS HasGit bool } diff --git a/platformifier/nextjs.go b/platformifier/nextjs.go deleted file mode 100644 index 5fcc375..0000000 --- a/platformifier/nextjs.go +++ /dev/null @@ -1,20 +0,0 @@ -package platformifier - -import ( - "context" - "io/fs" -) - -func newNextJSPlatformifier(templates fs.FS) *nextJSPlatformifier { - return &nextJSPlatformifier{ - templates: templates, - } -} - -type nextJSPlatformifier struct { - templates fs.FS -} - -func (p *nextJSPlatformifier) Platformify(_ context.Context, _ *UserInput) error { - return nil -} diff --git a/platformifier/platformifier.go b/platformifier/platformifier.go index e886771..51b04e3 100644 --- a/platformifier/platformifier.go +++ b/platformifier/platformifier.go @@ -14,23 +14,16 @@ var ( ) const ( - // Path names passed to open are UTF-8-encoded, - // unrooted, slash-separated sequences of path elements, like "x/y/z". - // Path names MUST NOT contain an element that is "." or ".." or the empty string, - // Paths MUST NOT start or end with a slash: "/x" and "x/" are invalid. genericDir = "templates/generic" upsunDir = "templates/upsun" djangoDir = "templates/django" laravelDir = "templates/laravel" - nextjsDir = "templates/nextjs" ) // A platformifier handles the business logic of a given runtime to platformify. -// -//go:generate mockgen -destination=platformifier_mock_test.go -package=platformifier -source=platformifier.go type platformifier interface { - // Platformify loads and writes the templates to the user's system. - Platformify(ctx context.Context, input *UserInput) error + // Platformify loads and returns the rendered templates. + Platformify(ctx context.Context, input *UserInput) (map[string][]byte, error) } type templateData struct { @@ -39,17 +32,7 @@ type templateData struct { } // New creates Platformifier with the appropriate platformifier stack based on UserInput. -func New(input *UserInput, flavor string, fileSystems ...FS) *Platformifier { - var fileSystem FS - if len(fileSystems) > 0 { - fileSystem = fileSystems[0] - } else { - fileSystem = NewOSFileSystem(input.WorkingDirectory) - } - - // fs.Sub(...) returns an error only if the given path name is invalid. - // Since we determine the path name ourselves in advance, - // there is no need to check for errors in this path name. +func New(input *UserInput, flavor string) *Platformifier { stacks := []platformifier{} templatesDir := genericDir if flavor == "upsun" { @@ -57,21 +40,15 @@ func New(input *UserInput, flavor string, fileSystems ...FS) *Platformifier { } templates, _ := fs.Sub(templatesFS, templatesDir) - stacks = append(stacks, newGenericPlatformifier(templates, fileSystem)) + stacks = append(stacks, newGenericPlatformifier(templates, input.WorkingDirectory)) switch input.Stack { case Django: - // No need to check for errors (see the comment above) templates, _ := fs.Sub(templatesFS, djangoDir) - stacks = append(stacks, newDjangoPlatformifier(templates, fileSystem)) + stacks = append(stacks, newDjangoPlatformifier(templates, input.WorkingDirectory)) case Laravel: - // No need to check for errors (see the comment above) templates, _ := fs.Sub(templatesFS, laravelDir) - stacks = append(stacks, newLaravelPlatformifier(templates, fileSystem)) - case NextJS: - // No need to check for errors (see the comment above) - templates, _ := fs.Sub(templatesFS, nextjsDir) - stacks = append(stacks, newNextJSPlatformifier(templates)) + stacks = append(stacks, newLaravelPlatformifier(templates, input.WorkingDirectory)) } return &Platformifier{ @@ -80,18 +57,23 @@ func New(input *UserInput, flavor string, fileSystems ...FS) *Platformifier { } } -// A Platformifier handles the business logic of a given runtime to platformify. +// Platformifier handles the business logic of a given runtime to platformify. type Platformifier struct { input *UserInput stacks []platformifier } -func (p *Platformifier) Platformify(ctx context.Context) error { +// Platformify runs all stack platformifiers and returns the collected files. +func (p *Platformifier) Platformify(ctx context.Context) (map[string][]byte, error) { + files := make(map[string][]byte) for _, stack := range p.stacks { - err := stack.Platformify(ctx, p.input) + newFiles, err := stack.Platformify(ctx, p.input) if err != nil { - return err + return nil, err + } + for path, contents := range newFiles { + files[path] = contents } } - return nil + return files, nil } diff --git a/platformifier/platformifier_mock_test.go b/platformifier/platformifier_mock_test.go deleted file mode 100644 index 82a4a67..0000000 --- a/platformifier/platformifier_mock_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: platformifier.go - -// Package platformifier is a generated GoMock package. -package platformifier - -import ( - context "context" - reflect "reflect" - - gomock "github.com/golang/mock/gomock" -) - -// Mockplatformifier is a mock of platformifier interface. -type Mockplatformifier struct { - ctrl *gomock.Controller - recorder *MockplatformifierMockRecorder -} - -// MockplatformifierMockRecorder is the mock recorder for Mockplatformifier. -type MockplatformifierMockRecorder struct { - mock *Mockplatformifier -} - -// NewMockplatformifier creates a new mock instance. -func NewMockplatformifier(ctrl *gomock.Controller) *Mockplatformifier { - mock := &Mockplatformifier{ctrl: ctrl} - mock.recorder = &MockplatformifierMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *Mockplatformifier) EXPECT() *MockplatformifierMockRecorder { - return m.recorder -} - -// Platformify mocks base method. -func (m *Mockplatformifier) Platformify(ctx context.Context, input *UserInput) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Platformify", ctx, input) - ret0, _ := ret[0].(error) - return ret0 -} - -// Platformify indicates an expected call of Platformify. -func (mr *MockplatformifierMockRecorder) Platformify(ctx, input interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Platformify", reflect.TypeOf((*Mockplatformifier)(nil).Platformify), ctx, input) -} diff --git a/platformifier/platformifier_test.go b/platformifier/platformifier_test.go index 4651e4d..112a958 100644 --- a/platformifier/platformifier_test.go +++ b/platformifier/platformifier_test.go @@ -5,13 +5,12 @@ import ( "errors" "io/fs" "os" - "reflect" + "path/filepath" "testing" + "testing/fstest" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" "github.com/platformsh/platformify/validator" ) @@ -27,7 +26,6 @@ var ( "DJANGO_SETTINGS_MODULE": "app.settings", "PYTHONUNBUFFERED": "1", }, - Root: "app", BuildFlavor: "none", BuildSteps: []string{ "pip install -r requirements.txt", @@ -101,7 +99,6 @@ var ( Environment: map[string]string{ "JAVA": "19", }, - Root: "app", BuildFlavor: "", BuildSteps: []string{ "mvn install", @@ -144,7 +141,6 @@ var ( Runtime: "php-8.4", ApplicationRoot: "app", Environment: map[string]string{}, - Root: "app", BuildFlavor: "php", BuildSteps: []string{}, DeployCommand: []string{}, @@ -190,9 +186,7 @@ func TestNewPlatformifier(t *testing.T) { require.NoError(t, err) laravelTemplates, err := fs.Sub(templatesFS, laravelDir) require.NoError(t, err) - nextjsTemplates, err := fs.Sub(templatesFS, nextjsDir) - require.NoError(t, err) - fileSystem := NewOSFileSystem("") + emptyFS := fstest.MapFS{} tests := []struct { name string stack Stack @@ -202,247 +196,140 @@ func TestNewPlatformifier(t *testing.T) { name: "generic", stack: Generic, platformifiers: []platformifier{ - &genericPlatformifier{templates: genericTemplates, fileSystem: fileSystem}, + &genericPlatformifier{templates: genericTemplates, fileSystem: emptyFS}, }, }, { name: "django", stack: Django, platformifiers: []platformifier{ - &genericPlatformifier{templates: genericTemplates, fileSystem: fileSystem}, - &djangoPlatformifier{templates: djangoTemplates, fileSystem: fileSystem}, + &genericPlatformifier{templates: genericTemplates, fileSystem: emptyFS}, + &djangoPlatformifier{templates: djangoTemplates, fileSystem: emptyFS}, }, }, { name: "laravel", stack: Laravel, platformifiers: []platformifier{ - &genericPlatformifier{templates: genericTemplates, fileSystem: fileSystem}, - &laravelPlatformifier{templates: laravelTemplates, fileSystem: fileSystem}, + &genericPlatformifier{templates: genericTemplates, fileSystem: emptyFS}, + &laravelPlatformifier{templates: laravelTemplates, fileSystem: emptyFS}, }, }, { name: "nextjs", stack: NextJS, platformifiers: []platformifier{ - &genericPlatformifier{templates: genericTemplates, fileSystem: fileSystem}, - &nextJSPlatformifier{templates: nextjsTemplates}, + &genericPlatformifier{templates: genericTemplates, fileSystem: emptyFS}, }, }, { name: "strapi", stack: Strapi, platformifiers: []platformifier{ - &genericPlatformifier{templates: genericTemplates, fileSystem: fileSystem}, + &genericPlatformifier{templates: genericTemplates, fileSystem: emptyFS}, }, }, { name: "flask", stack: Flask, platformifiers: []platformifier{ - &genericPlatformifier{templates: genericTemplates, fileSystem: fileSystem}, + &genericPlatformifier{templates: genericTemplates, fileSystem: emptyFS}, }, }, { name: "express", stack: Express, platformifiers: []platformifier{ - &genericPlatformifier{templates: genericTemplates, fileSystem: fileSystem}, + &genericPlatformifier{templates: genericTemplates, fileSystem: emptyFS}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // GIVEN user input with given stack - input := &UserInput{Stack: tt.stack} + // GIVEN user input with given stack. + input := &UserInput{Stack: tt.stack, WorkingDirectory: emptyFS} - // WHEN create new platformifier + // WHEN create new platformifier. pfier := New(input, "platform") - // THEN user input inside platformifier should be the same as given + // THEN user input inside platformifier should be the same as given. assert.Equal(t, input, pfier.input) - // AND length of the platformifier's stack must be equal to the length of expected stacks + // AND length of the platformifier's stack must be equal to the length of expected stacks. require.Len(t, pfier.stacks, len(tt.platformifiers)) for i := range pfier.stacks { - // AND the type of each stack should be the same as expected + // AND the type of each stack should be the same as expected. assert.IsType(t, tt.platformifiers[i], pfier.stacks[i]) - assert.True(t, reflect.DeepEqual(tt.platformifiers[i], pfier.stacks[i])) } }) } } -type PlatformifySuiteTester struct { - suite.Suite - - generic *Mockplatformifier - django *Mockplatformifier - laravel *Mockplatformifier - nextjs *Mockplatformifier +// mockPlatformifier is a simple test double for the platformifier interface. +type mockPlatformifier struct { + err error } -func (s *PlatformifySuiteTester) SetupTest() { - ctrl := gomock.NewController(s.T()) - - s.generic = NewMockplatformifier(ctrl) - s.django = NewMockplatformifier(ctrl) - s.laravel = NewMockplatformifier(ctrl) - s.nextjs = NewMockplatformifier(ctrl) +func (m *mockPlatformifier) Platformify(_ context.Context, _ *UserInput) (map[string][]byte, error) { + if m.err != nil { + return nil, m.err + } + return map[string][]byte{}, nil } -func (s *PlatformifySuiteTester) TestSuccessfulPlatformifying() { - // GIVEN empty context - ctx := context.Background() - // AND user input is empty (because it doesn't matter if it's empty or not) - input := &UserInput{} - // AND platformifying generic stack returns no errors - s.generic.EXPECT(). - Platformify(gomock.Eq(ctx), gomock.Eq(input)). - Return(nil).AnyTimes() - // AND platformifying django stack returns no errors - s.django.EXPECT(). - Platformify(gomock.Eq(ctx), gomock.Eq(input)). - Return(nil).AnyTimes() - // AND platformifying laravel stack returns no errors - s.laravel.EXPECT(). - Platformify(gomock.Eq(ctx), gomock.Eq(input)). - Return(nil).AnyTimes() - // AND platformifying nextjs stack returns no errors - s.nextjs.EXPECT(). - Platformify(gomock.Eq(ctx), gomock.Eq(input)). - Return(nil).AnyTimes() - +func TestPlatformifier_Platformify_Success(t *testing.T) { + ok := &mockPlatformifier{} tests := []struct { name string stacks []platformifier }{ - { - name: "empty", - stacks: []platformifier{}, - }, - { - name: "generic", - stacks: []platformifier{s.generic}, - }, - { - name: "django", - stacks: []platformifier{s.django}, - }, - { - name: "laravel", - stacks: []platformifier{s.laravel}, - }, - { - name: "nextjs", - stacks: []platformifier{s.nextjs}, - }, - { - name: "generic+django", - stacks: []platformifier{s.generic, s.django}, - }, - { - name: "generic+laravel", - stacks: []platformifier{s.generic, s.laravel}, - }, - { - name: "generic+nextjs", - stacks: []platformifier{s.generic, s.nextjs}, - }, - { - name: "all", - stacks: []platformifier{s.generic, s.django, s.laravel, s.nextjs}, - }, + {name: "empty", stacks: []platformifier{}}, + {name: "one", stacks: []platformifier{ok}}, + {name: "two", stacks: []platformifier{ok, ok}}, } for _, tt := range tests { - s.T().Run(tt.name, func(t *testing.T) { - // WHEN run platformifying of the given stack + t.Run(tt.name, func(t *testing.T) { p := Platformifier{ - input: input, + input: &UserInput{}, stacks: tt.stacks, } - err := p.Platformify(ctx) - // THEN it doesn't return any errors + files, err := p.Platformify(context.Background()) assert.NoError(t, err) + assert.NotNil(t, files) }) } } -func (s *PlatformifySuiteTester) TestPlatformifyingError() { - // GIVEN empty context - ctx := context.Background() - // AND user input is empty (because it doesn't matter if it's empty or not) - input := &UserInput{} - // AND platformifying generic stack fails - s.generic.EXPECT(). - Platformify(gomock.Eq(ctx), gomock.Eq(input)). - Return(errors.New("")).AnyTimes() - // AND platformifying django stack fails - s.django.EXPECT(). - Platformify(gomock.Eq(ctx), gomock.Eq(input)). - Return(errors.New("")).AnyTimes() - // AND platformifying laravel stack fails - s.laravel.EXPECT(). - Platformify(gomock.Eq(ctx), gomock.Eq(input)). - Return(errors.New("")).AnyTimes() - // AND platformifying nextjs stack fails - s.nextjs.EXPECT(). - Platformify(gomock.Eq(ctx), gomock.Eq(input)). - Return(errors.New("")).AnyTimes() - +func TestPlatformifier_Platformify_Error(t *testing.T) { + fail := &mockPlatformifier{err: errors.New("fail")} + ok := &mockPlatformifier{} tests := []struct { name string stacks []platformifier }{ - { - name: "generic", - stacks: []platformifier{s.generic}, - }, - { - name: "django", - stacks: []platformifier{s.django}, - }, - { - name: "laravel", - stacks: []platformifier{s.laravel}, - }, - { - name: "nextjs", - stacks: []platformifier{s.nextjs}, - }, - { - name: "generic+django", - stacks: []platformifier{s.generic, s.django}, - }, - { - name: "generic+laravel", - stacks: []platformifier{s.generic, s.laravel}, - }, - { - name: "generic+nextjs", - stacks: []platformifier{s.generic, s.nextjs}, - }, - { - name: "all", - stacks: []platformifier{s.generic, s.django, s.laravel, s.nextjs}, - }, + {name: "single failure", stacks: []platformifier{fail}}, + {name: "first fails", stacks: []platformifier{fail, ok}}, } for _, tt := range tests { - s.T().Run(tt.name, func(t *testing.T) { - // WHEN run platformifying of the given stack + t.Run(tt.name, func(t *testing.T) { p := Platformifier{ - input: input, + input: &UserInput{}, stacks: tt.stacks, } - err := p.Platformify(ctx) - // THEN it fails + _, err := p.Platformify(context.Background()) assert.Error(t, err) }) } } -func TestPlatformifySuite(t *testing.T) { - suite.Run(t, new(PlatformifySuiteTester)) +// writeFiles writes the file map to a directory on disk. +func writeFiles(t *testing.T, dir string, files map[string][]byte) { + t.Helper() + for path, contents := range files { + absPath := filepath.Join(dir, path) + require.NoError(t, os.MkdirAll(filepath.Dir(absPath), 0o755)) + require.NoError(t, os.WriteFile(absPath, contents, 0o644)) + } } func TestPlatformifier_Platformify(t *testing.T) { @@ -454,56 +341,30 @@ func TestPlatformifier_Platformify(t *testing.T) { fields fields wantErr bool }{ - { - name: "Django", - fields: fields{ui: djangoStack}, - }, - { - name: "Generic", - fields: fields{ui: genericStack}, - }, - { - name: "Laravel", - fields: fields{ui: laravelStack}, - }, - { - name: "Next.js", - fields: fields{ui: nextJSStack}, - }, - { - name: "Strapi", - fields: fields{ui: strapiStack}, - }, - { - name: "Flask", - fields: fields{ui: flaskStack}, - }, - { - name: "Express", - fields: fields{ui: expressStack}, - }, + {name: "Django", fields: fields{ui: djangoStack}}, + {name: "Generic", fields: fields{ui: genericStack}}, + {name: "Laravel", fields: fields{ui: laravelStack}}, + {name: "Next.js", fields: fields{ui: nextJSStack}}, + {name: "Strapi", fields: fields{ui: strapiStack}}, + {name: "Flask", fields: fields{ui: flaskStack}}, + {name: "Express", fields: fields{ui: expressStack}}, } - // Create a temporary directory to use as the output directory. tempDir, err := os.MkdirTemp("", "yaml_tests") - if err != nil { - t.Fatalf("Failed to create temporary directory: %v", err) - } + require.NoError(t, err) defer os.RemoveAll(tempDir) ctx := context.Background() for _, tt := range tests { dir, err := os.MkdirTemp(tempDir, tt.name) - if err != nil { - t.Fatalf("Failed to create temporary %v directory: %v", tt.name, err) - } - tt.fields.ui.WorkingDirectory = dir + require.NoError(t, err) + tt.fields.ui.WorkingDirectory = os.DirFS(dir) t.Run(tt.name, func(t *testing.T) { - if err := New(tt.fields.ui, "platform").Platformify(ctx); (err != nil) != tt.wantErr { - t.Errorf("Platformifier.Platformify() error = %v, wantErr %v", err, tt.wantErr) + files, pErr := New(tt.fields.ui, "platform").Platformify(ctx) + if (pErr != nil) != tt.wantErr { + t.Errorf("Platformifier.Platformify() error = %v, wantErr %v", pErr, tt.wantErr) } - - // Validate the config. + writeFiles(t, dir, files) if err := validator.ValidateConfig(dir, "platform"); (err != nil) != tt.wantErr { t.Errorf("Platformifier.Platformify() validation error = %v, wantErr %v", err, tt.wantErr) } @@ -520,56 +381,30 @@ func TestPlatformifier_Upsunify(t *testing.T) { fields fields wantErr bool }{ - { - name: "Django", - fields: fields{ui: djangoStack}, - }, - { - name: "Generic", - fields: fields{ui: genericStack}, - }, - { - name: "Laravel", - fields: fields{ui: laravelStack}, - }, - { - name: "Next.js", - fields: fields{ui: nextJSStack}, - }, - { - name: "Strapi", - fields: fields{ui: strapiStack}, - }, - { - name: "Flask", - fields: fields{ui: flaskStack}, - }, - { - name: "Express", - fields: fields{ui: expressStack}, - }, + {name: "Django", fields: fields{ui: djangoStack}}, + {name: "Generic", fields: fields{ui: genericStack}}, + {name: "Laravel", fields: fields{ui: laravelStack}}, + {name: "Next.js", fields: fields{ui: nextJSStack}}, + {name: "Strapi", fields: fields{ui: strapiStack}}, + {name: "Flask", fields: fields{ui: flaskStack}}, + {name: "Express", fields: fields{ui: expressStack}}, } - // Create a temporary directory to use as the output directory. tempDir, err := os.MkdirTemp("", "yaml_tests") - if err != nil { - t.Fatalf("Failed to create temporary directory: %v", err) - } + require.NoError(t, err) defer os.RemoveAll(tempDir) ctx := context.Background() for _, tt := range tests { dir, err := os.MkdirTemp(tempDir, tt.name) - if err != nil { - t.Fatalf("Failed to create temporary %v directory: %v", tt.name, err) - } - tt.fields.ui.WorkingDirectory = dir + require.NoError(t, err) + tt.fields.ui.WorkingDirectory = os.DirFS(dir) t.Run(tt.name, func(t *testing.T) { - if err := New(tt.fields.ui, "upsun").Platformify(ctx); (err != nil) != tt.wantErr { - t.Errorf("Platformifier.Platformify() error = %v, wantErr %v", err, tt.wantErr) + files, pErr := New(tt.fields.ui, "upsun").Platformify(ctx) + if (pErr != nil) != tt.wantErr { + t.Errorf("Platformifier.Platformify() error = %v, wantErr %v", pErr, tt.wantErr) } - - // Validate the config. + writeFiles(t, dir, files) if err := validator.ValidateConfig(dir, "upsun"); (err != nil) != tt.wantErr { t.Errorf("Platformifier.Platformify() validation error = %v, wantErr %v", err, tt.wantErr) } diff --git a/validator/validator.go b/validator/validator.go index 3cd4d50..9335d46 100644 --- a/validator/validator.go +++ b/validator/validator.go @@ -86,11 +86,12 @@ func validatePlatformConfig(path string) error { } foundApp := false - for _, file := range utils.FindAllFiles(path, ".platform.app.yaml") { + dirFS := os.DirFS(path) + for _, file := range utils.FindAllFiles(dirFS, ".", ".platform.app.yaml") { foundApp = true - if _, err := ValidateFile(file, applicationSchema); err != nil { - relPath, _ := filepath.Rel(path, file) - errs = errors.Join(errs, fmt.Errorf("validation failed for %s: %w", relPath, err)) + absFile := filepath.Join(path, file) + if _, err := ValidateFile(absFile, applicationSchema); err != nil { + errs = errors.Join(errs, fmt.Errorf("validation failed for %s: %w", file, err)) } }