diff --git a/cmd/cmd_internal_test.go b/cmd/cmd_internal_test.go index 70173cd..d988546 100644 --- a/cmd/cmd_internal_test.go +++ b/cmd/cmd_internal_test.go @@ -2,7 +2,11 @@ package cmd import ( + "os" + "path/filepath" "testing" + + "github.com/stretchr/testify/assert" ) func TestRoot_testdata(t *testing.T) { @@ -35,9 +39,57 @@ func TestRoot_testdata(t *testing.T) { t.Run(test.testPath, func(t *testing.T) { rootCmd.SetArgs([]string{test.testPath}) exitCode := Execute() - if exitCode != test.exitCode { - t.Errorf("Expected exit code %d, got %d", test.exitCode, exitCode) + assert.Equal(t, test.exitCode, exitCode) + }) + } +} + +func TestNormalise_testdata(t *testing.T) { + for _, test := range []struct { + testPath string + transformsArgs []string + exitCode int + output string + }{ + { + testPath: "../testdata/success_normalise_infotexts.md", + transformsArgs: []string{"-t", "filename=title"}, + exitCode: 0, + output: `# Success: normalise info texts + +The normalise command with ` + "`" + `-t filename=title` + "`" + ` transform argument should remove and ` + "`" + `no_value` + "`" + ` and ` + "`" + `key=value` + "`" + ` args and replace ` + "`" + `filename` + "`" + ` key with ` + "`" + `title` + "`" + `, + +` + "```" + `sh title=true.sh +exit 0 +` + "```" + ` +`, + }, + } { + test := test + t.Run(test.testPath, func(t *testing.T) { + dir, err := os.MkdirTemp("", "example") + if err != nil { + t.Errorf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(dir) + + var args []string + args = append(args, "normalise", "-o", dir) + args = append(args, test.transformsArgs...) + args = append(args, test.testPath) + rootCmd.SetArgs(args) + + exitCode := Execute() + assert.Equal(t, test.exitCode, exitCode) + + outputFile := filepath.Join(dir, filepath.Base(test.testPath)) + assert.FileExists(t, outputFile) + + outputBytes, err := os.ReadFile(filepath.Join(dir, filepath.Base(test.testPath))) + if err != nil { + t.Errorf("Failed to read output file: %v", err) } + assert.Equal(t, test.output, string(outputBytes)) }) } } diff --git a/cmd/normalise.go b/cmd/normalise.go new file mode 100644 index 0000000..513a29f --- /dev/null +++ b/cmd/normalise.go @@ -0,0 +1,36 @@ +package cmd + +import ( + "github.com/UpCloudLtd/mdtest/utils" + "github.com/spf13/cobra" +) + +var ( + transforms []string + outputPath string + + normaliseCmd = &cobra.Command{ + Aliases: []string{"normalize"}, + Use: "normalise", + Short: "Normalise the fenced code block info texts", + Long: "Normalise the fenced code block info texts. By default, removes all info texts defined after the language identifier from the starting code-block fence.", + Args: cobra.MinimumNArgs(1), + } +) + +func init() { + rootCmd.AddCommand(normaliseCmd) + normaliseCmd.Flags().StringArrayVarP(&transforms, "transform", "t", nil, "transform info text key in `old=new` format, e.g., `-t filename=title` would transform `filename` info text to `title` info text") + normaliseCmd.Flags().StringVarP(&outputPath, "output", "o", "", "`directory` where to save the normalised files") + _ = normaliseCmd.MarkFlagRequired("output") + normaliseCmd.RunE = func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + + params := utils.NormalizeParameters{ + OutputPath: outputPath, + Transforms: transforms, + } + + return utils.Normalize(args, params) + } +} diff --git a/testcase/teststep.go b/testcase/teststep.go index e959d0a..404d7f0 100644 --- a/testcase/teststep.go +++ b/testcase/teststep.go @@ -4,6 +4,8 @@ import ( "bufio" "fmt" "strings" + + "github.com/UpCloudLtd/mdtest/utils" ) type StepResult struct { @@ -16,26 +18,6 @@ type Step interface { Execute(*testStatus) StepResult } -func parseOptions(optionsStr string) (string, map[string]string) { - optionsList := strings.Split(optionsStr, " ") - options := make(map[string]string) - - lang := optionsList[0] - for _, option := range optionsList[1:] { - items := strings.SplitN(option, "=", 2) - - key := items[0] - value := "" - if len(items) > 1 { - value = items[1] - } - - options[key] = value - } - - return lang, options -} - func parseCodeBlock(lang string, options map[string]string, content string) (Step, error) { if options["filename"] != "" { return parseFilenameStep(options, content) @@ -57,7 +39,7 @@ func parseStep(scanner *bufio.Scanner) (Step, error) { return nil, fmt.Errorf("current scanner position is not at start of a test step") } - lang, options := parseOptions(line[3:]) + lang, options := utils.ParseOptions(line[3:]) content := "" for scanner.Scan() { diff --git a/testdata/success_normalise_infotexts.md b/testdata/success_normalise_infotexts.md new file mode 100644 index 0000000..fa04d84 --- /dev/null +++ b/testdata/success_normalise_infotexts.md @@ -0,0 +1,7 @@ +# Success: normalise info texts + +The normalise command with `-t filename=title` transform argument should remove and `no_value` and `key=value` args and replace `filename` key with `title`, + +```sh no_value key=value filename=true.sh +exit 0 +``` diff --git a/testrun/testrun.go b/testrun/testrun.go index 8b6a7d8..ce1147a 100644 --- a/testrun/testrun.go +++ b/testrun/testrun.go @@ -3,16 +3,13 @@ package testrun import ( "fmt" "io" - "os" - "path" - "strings" "time" "github.com/UpCloudLtd/mdtest/id" "github.com/UpCloudLtd/mdtest/output" "github.com/UpCloudLtd/mdtest/testcase" + "github.com/UpCloudLtd/mdtest/utils" "github.com/UpCloudLtd/progress" - "github.com/UpCloudLtd/progress/messages" ) type RunParameters struct { @@ -30,57 +27,6 @@ type RunResult struct { TestResults []testcase.TestResult } -type PathWarning struct { - path string - err error -} - -func (warn PathWarning) Message() messages.Update { - return messages.Update{ - Message: fmt.Sprintf("Finding %s", warn.path), - Details: fmt.Sprintf("Error: %s", warn.err.Error()), - Status: messages.MessageStatusWarning, - } -} - -func parseFilePaths(rawPaths []string, depth int) ([]string, []PathWarning) { - paths := []string{} - warnings := []PathWarning{} - for _, rawPath := range rawPaths { - info, err := os.Stat(rawPath) - if err != nil { - warnings = append(warnings, PathWarning{rawPath, err}) - if info == nil { - continue - } - } - - if info.Mode().IsDir() && depth != 0 { - files, err := os.ReadDir(rawPath) - if err != nil { - warnings = append(warnings, PathWarning{rawPath, err}) - } - - dirRawPaths := []string{} - for _, file := range files { - dirRawPaths = append(dirRawPaths, path.Join(rawPath, file.Name())) - } - - dirPaths, dirWarnings := parseFilePaths(dirRawPaths, depth-1) - if dirWarnings != nil { - warnings = append(warnings, dirWarnings...) - } - - paths = append(paths, dirPaths...) - } - - if strings.HasSuffix(rawPath, ".md") { - paths = append(paths, rawPath) - } - } - return paths, warnings -} - func PrintSummary(target io.Writer, run RunResult) { tests := output.Total(len(run.TestResults)) if run.SuccessCount > 0 { @@ -102,7 +48,7 @@ func PrintSummary(target io.Writer, run RunResult) { func Execute(rawPaths []string, params RunParameters) RunResult { started := time.Now() - paths, warnings := parseFilePaths(rawPaths, 1) + paths, warnings := utils.ParseFilePaths(rawPaths, 1) testLog := progress.NewProgress(nil) testLog.Start() diff --git a/utils/files.go b/utils/files.go new file mode 100644 index 0000000..001408f --- /dev/null +++ b/utils/files.go @@ -0,0 +1,81 @@ +package utils + +import ( + "fmt" + "os" + "path" + "strings" + + "github.com/UpCloudLtd/progress/messages" +) + +type PathWarning struct { + path string + err error +} + +func (warn PathWarning) Message() messages.Update { + return messages.Update{ + Message: fmt.Sprintf("Finding %s", warn.path), + Details: fmt.Sprintf("Error: %s", warn.err.Error()), + Status: messages.MessageStatusWarning, + } +} + +func ParseFilePaths(rawPaths []string, depth int) ([]string, []PathWarning) { + paths := []string{} + warnings := []PathWarning{} + for _, rawPath := range rawPaths { + info, err := os.Stat(rawPath) + if err != nil { + warnings = append(warnings, PathWarning{rawPath, err}) + if info == nil { + continue + } + } + + if info.Mode().IsDir() && depth != 0 { + files, err := os.ReadDir(rawPath) + if err != nil { + warnings = append(warnings, PathWarning{rawPath, err}) + } + + dirRawPaths := []string{} + for _, file := range files { + dirRawPaths = append(dirRawPaths, path.Join(rawPath, file.Name())) + } + + dirPaths, dirWarnings := ParseFilePaths(dirRawPaths, depth-1) + if dirWarnings != nil { + warnings = append(warnings, dirWarnings...) + } + + paths = append(paths, dirPaths...) + } + + if strings.HasSuffix(rawPath, ".md") { + paths = append(paths, rawPath) + } + } + return paths, warnings +} + +func ParseOptions(optionsStr string) (string, map[string]string) { + optionsList := strings.Split(optionsStr, " ") + options := make(map[string]string) + + lang := optionsList[0] + for _, option := range optionsList[1:] { + items := strings.SplitN(option, "=", 2) + + key := items[0] + value := "" + if len(items) > 1 { + value = items[1] + } + + options[key] = value + } + + return lang, options +} diff --git a/utils/normalise.go b/utils/normalise.go new file mode 100644 index 0000000..e7bac68 --- /dev/null +++ b/utils/normalise.go @@ -0,0 +1,122 @@ +package utils + +import ( + "bufio" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/UpCloudLtd/progress" + "github.com/UpCloudLtd/progress/messages" +) + +type NormalizeParameters struct { + OutputPath string + Transforms []string +} + +func Normalize(rawPaths []string, params NormalizeParameters) error { + paths, warnings := ParseFilePaths(rawPaths, 1) + + info, err := os.Stat(params.OutputPath) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + if err := os.MkdirAll(params.OutputPath, 0o755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + } + return fmt.Errorf(`failed to stat output path "%s" (%w)`, params.OutputPath, err) + } + if info != nil && !info.IsDir() { + return fmt.Errorf(`output directory "%s" must be a directory`, params.OutputPath) + } + + normLog := progress.NewProgress(nil) + normLog.Start() + + for _, warning := range warnings { + _ = normLog.Push(warning.Message()) + } + + transformMap := make(map[string]string) + for _, s := range params.Transforms { + parts := strings.SplitN(s, "=", 2) + if len(parts) > 1 { + transformMap[parts[0]] = parts[1] + } + } + + for _, path := range paths { + _ = normLog.Push(messages.Update{ + Key: path, + Message: fmt.Sprintf("Normalising %s", path), + Status: messages.MessageStatusStarted, + }) + err := normalize(path, params.OutputPath, transformMap) + if err != nil { + _ = normLog.Push(messages.Update{ + Key: path, + Details: fmt.Sprintf("Error: %s", err.Error()), + Status: messages.MessageStatusError, + }) + } else { + _ = normLog.Push(messages.Update{ + Key: path, + Status: messages.MessageStatusSuccess, + }) + } + } + + normLog.Stop() + return nil +} + +func transformOptions(options, transforms map[string]string) string { + output := "" + for key, value := range options { + if newKey := transforms[key]; newKey != "" { + if value != "" { + output += fmt.Sprintf(" %s=%s", newKey, value) + } else { + output += fmt.Sprintf(" %s", newKey) + } + } + } + + return output +} + +func normalize(path, outputDir string, transforms map[string]string) error { + input, err := os.Open(path) + if err != nil { + return fmt.Errorf(`failed to open input file at "%s" (%w)`, path, err) + } + defer input.Close() + + output, err := os.Create(filepath.Join(outputDir, filepath.Base(path))) + if err != nil { + return fmt.Errorf(`failed to open output file at "%s" (%w)`, path, err) + } + defer output.Close() + + scanner := bufio.NewScanner(input) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "```") { + info := line[3:] + if len(info) > 0 { + lang, options := ParseOptions(info) + line = fmt.Sprintf("```%s%s", lang, transformOptions(options, transforms)) + } + } + _, err = output.WriteString(line + "\n") + if err != nil { + return fmt.Errorf(`failed to write to output file "%s" (%w)`, output.Name(), err) + } + } + + return nil +}