From c6efc5af5b797b00ac2ebc43224cf151de17b977 Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 14 Jul 2025 11:13:20 +0200 Subject: [PATCH 01/12] Generalizing golden script tests --- golden/config.go | 24 ++-- golden/dag.go | 10 +- golden/{bash.go => script.go} | 109 ++++++++++++------ run/tests/http/async/main_test.go | 2 +- run/tests/http/error_log/main_test.go | 2 +- run/tests/http/sync/main_test.go | 2 +- run/tests/http/too_many_requests/main_test.go | 2 +- run/tests/simple/main_test.go | 2 +- 8 files changed, 95 insertions(+), 58 deletions(-) rename golden/{bash.go => script.go} (73%) diff --git a/golden/config.go b/golden/config.go index a6ab2d73..85cbf818 100644 --- a/golden/config.go +++ b/golden/config.go @@ -65,29 +65,33 @@ type Config struct { ExecutionConfig *ExecutionConfig } -// BashConfig defines the configuration for a golden bash test. -type BashConfig struct { +// ScriptConfig defines the configuration for a golden script test. +type ScriptConfig struct { // DisplayStdout indicates whether to display or suppress stdout. DisplayStdout bool // DisplayStderr indicates whether to display or suppress stderr. DisplayStderr bool // OutputProcessConfig defines how to process the output before comparison. OutputProcessConfig OutputProcessConfig + // ScriptExtensions is a list of script file extensions to be considered for + // golden file tests alongside their respective command to execute them. + // The key is the file extension, and the value is the command to execute + // the script. If not provided, it defaults to bash. I.e.: {".sh": "bash"}. + ScriptExtensions map[string]string // GoldenExtension is the file extension to use for the golden file. If not // provided, then the default extension (.golden) is used. GoldenExtension string // Envs specifies the environment variables to set for execution. Envs [][2]string - // PostProcessFunctions defines a list of functions to be executed after the bash - // script has been run. This can be used to make use of the output of the bash script - // and perform additional operations on it. The functions are executed in the order - // they are defined and are not used for comparison. + // PostProcessFunctions defines a list of functions to be executed after the + // script has been run. This can be used to make use of the output of the + // script and perform additional operations on it. The functions are + // executed in the order they are defined and are not used for comparison. PostProcessFunctions []func(goldenFile string) error - // WorkingDir is the directory where the bash script(s) will be - // executed. + // WorkingDir is the directory where the script(s) will be executed. WorkingDir string - // WaitBefore adds a delay before running the bash script. This is useful - // when throttling is needed, e.g., when dealing with rate limiting. + // WaitBefore adds a delay before running the script. This is useful when + // throttling is needed, e.g., when dealing with rate limiting. WaitBefore time.Duration } diff --git a/golden/dag.go b/golden/dag.go index 5a2c9cdf..ec5525f8 100644 --- a/golden/dag.go +++ b/golden/dag.go @@ -10,12 +10,12 @@ import ( type DagTestCase struct { Name string Needs []string - Config *BashConfig + Config *ScriptConfig Path string } // DagTest runs a set of test cases in topological order. -// Each test case is a BashTest, and the test cases are connected by their +// Each test case is a ScriptTest, and the test cases are connected by their // dependencies. If a test case has dependencies, it will only be run after all // of its dependencies have been run. // @@ -25,13 +25,13 @@ type DagTestCase struct { // { // name: "app-create", // needs: []string{}, -// config: BashConfig{ /**/ }, +// config: ScriptConfig{ /**/ }, // path: "app-create", // }, // { // name: "app-push", // needs: []string{"app-create"}, -// config: BashConfig{ /**/ }, +// config: ScriptConfig{ /**/ }, // path: "app-push", // }, // } @@ -71,7 +71,7 @@ func DagTest(t *testing.T, cases []DagTestCase) { var wg sync.WaitGroup for _, nextCase := range next { wg.Add(1) - config := BashConfig{} + config := ScriptConfig{} if nextCase.Config != nil { config = *nextCase.Config } diff --git a/golden/bash.go b/golden/script.go similarity index 73% rename from golden/bash.go rename to golden/script.go index e171e3f4..7e099916 100644 --- a/golden/bash.go +++ b/golden/script.go @@ -7,7 +7,7 @@ import ( "os" "os/exec" "path/filepath" - "strings" + "slices" "testing" "time" @@ -15,61 +15,94 @@ import ( "github.com/sergi/go-diff/diffmatchpatch" ) -// BashTest executes a golden file test for a bash command. It walks over +type scriptTest struct { + // Path is the path to the script file. + Path string + // Command is the command to execute the script. + Command string +} + +// ScriptTest executes a golden file test for a script command. It walks over // the goldenDir to gather all .sh scripts present in the dir. It then executes // each of the scripts and compares expected vs. actual outputs. If // displayStdout or displayStderr are true, the output of each script will be // composed of the resulting stderr + stdout. -func BashTest( +func ScriptTest( t *testing.T, goldenDir string, - bashConfig BashConfig, + scriptConfig ScriptConfig, ) { // Fail immediately, if dir does not exist if stat, err := os.Stat(goldenDir); err != nil || !stat.IsDir() { t.Fatalf("dir %s does not exist", goldenDir) } - // Collect bash scripts. - var scripts []string + // Collect scripts. + extensions := make([]string, 0, len(scriptConfig.ScriptExtensions)) + for ext := range scriptConfig.ScriptExtensions { + extensions = append(extensions, ext) + } + var scripts []scriptTest fn := func(path string, _ os.FileInfo, _ error) error { - // Only consider .sh files - if strings.HasSuffix(path, ".sh") { - scripts = append(scripts, path) + // Check if the file should be considered as a script to test. + extension := filepath.Ext(path) + if slices.Contains(extensions, extension) { + scripts = append(scripts, scriptTest{ + Path: path, + Command: scriptConfig.ScriptExtensions[extension], + }) } - return nil } if err := filepath.Walk(goldenDir, fn); err != nil { t.Fatal("error walking over files: ", err) } - cwd, err := os.Getwd() - if err != nil { - t.Fatal("error getting current working directory: ", err) + // If no scripts were found, fail the test. + if len(scripts) == 0 { + t.Fatal("no scripts found in directory: ", goldenDir) } // Execute a golden file test for each script. Make the script path // absolute to avoid issues with custom working directories. + cwd, err := os.Getwd() + if err != nil { + t.Fatal("error getting current working directory: ", err) + } for _, script := range scripts { - BashTestFile(t, filepath.Join(cwd, script), bashConfig) + ScriptTestFile(t, script.Command, filepath.Join(cwd, script.Path), scriptConfig) } // Post-process files containing volatile data. - postProcessVolatileData(t, bashConfig) + postProcessVolatileData(t, scriptConfig) } -// BashTestFile executes a golden file test for a single bash script. The -// script is executed and the expected output is compared with the actual -// output. +// BashTestFile executes a golden file test for a single bash script. The script +// is executed and the expected output is compared with the actual output. func BashTestFile( t *testing.T, script string, - bashConfig BashConfig, + bashConfig ScriptConfig, +) { + ScriptTestFile( + t, + "bash", + script, + bashConfig, + ) +} + +// ScriptTestFile executes a golden file test for a single script. The script is +// executed and the expected output is compared with the actual output. +func ScriptTestFile( + t *testing.T, + command string, + script string, + scriptConfig ScriptConfig, ) { ext := goldenExtension - if bashConfig.GoldenExtension != "" { - ext = bashConfig.GoldenExtension + if scriptConfig.GoldenExtension != "" { + ext = scriptConfig.GoldenExtension } goldenFilePath := script + ext // Function run by the test. @@ -86,34 +119,34 @@ func BashTestFile( t.Fatalf("script %s does not exist", script) } - // Execute a bash command which consists of executing a .sh file. - cmd := exec.Command("bash", script) + // Execute the script using the provided command. + cmd := exec.Command(command, script) // Pass environment and add custom environment variables cmd.Env = os.Environ() - for _, e := range bashConfig.Envs { + for _, e := range scriptConfig.Envs { cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", e[0], e[1])) } // Set custom working directory if provided. - if bashConfig.WorkingDir != "" { - cmd.Dir = bashConfig.WorkingDir + if scriptConfig.WorkingDir != "" { + cmd.Dir = scriptConfig.WorkingDir } // Run the command and gather the output bytes. - out, err := runCmd(cmd, bashConfig.DisplayStdout, bashConfig.DisplayStderr) + out, err := runCmd(cmd, scriptConfig.DisplayStdout, scriptConfig.DisplayStderr) if err != nil { t.Fatal(err) } // Process the output data before comparison. - got := processOutput(t, out, goldenFilePath, bashConfig.OutputProcessConfig) + got := processOutput(t, out, goldenFilePath, scriptConfig.OutputProcessConfig) // Write the output bytes to a .golden file, if the test is being // updated - if *update || bashConfig.OutputProcessConfig.AlwaysUpdate { + if *update || scriptConfig.OutputProcessConfig.AlwaysUpdate { if err := os.WriteFile(goldenFilePath, []byte(got), 0o644); err != nil { - t.Fatal("error writing bash output to file: ", err) + t.Fatal("error writing script output to file: ", err) } } @@ -138,16 +171,16 @@ func BashTestFile( } // Delay the execution of the test to adhere for rate limits. - if bashConfig.WaitBefore > 0 { - t.Logf("delaying test execution for %v", bashConfig.WaitBefore) - <-time.After(bashConfig.WaitBefore) + if scriptConfig.WaitBefore > 0 { + t.Logf("delaying test execution for %v", scriptConfig.WaitBefore) + <-time.After(scriptConfig.WaitBefore) } // Test is executed. t.Run(script, f) // Run post-process functions. - for _, f := range bashConfig.PostProcessFunctions { + for _, f := range scriptConfig.PostProcessFunctions { err := f(goldenFilePath) if err != nil { t.Fatalf("error running post-process function: %v", err) @@ -233,10 +266,10 @@ func processOutput( func postProcessVolatileData( t *testing.T, - bashConfig BashConfig, + scriptConfig ScriptConfig, ) { // Post-process files containing volatile data. - for _, file := range bashConfig.OutputProcessConfig.VolatileDataFiles { + for _, file := range scriptConfig.OutputProcessConfig.VolatileDataFiles { // Read the file. out, err := os.ReadFile(file) if err != nil { @@ -245,11 +278,11 @@ func postProcessVolatileData( got := string(out) // Replace default volatile content with a placeholder. - if !bashConfig.OutputProcessConfig.KeepVolatileData { + if !scriptConfig.OutputProcessConfig.KeepVolatileData { got = regexReplaceAllDefault(got) } // Apply custom volatile regex replacements. - for _, r := range bashConfig.OutputProcessConfig.VolatileRegexReplacements { + for _, r := range scriptConfig.OutputProcessConfig.VolatileRegexReplacements { got = regexReplaceCustom(got, r.Replacement, r.Regex) } diff --git a/run/tests/http/async/main_test.go b/run/tests/http/async/main_test.go index 72f83412..1dfa2f85 100644 --- a/run/tests/http/async/main_test.go +++ b/run/tests/http/async/main_test.go @@ -18,7 +18,7 @@ func TestMain(m *testing.M) { // the output is compared against the expected one. func TestGoldenBash(t *testing.T) { // Execute the rest of the bash commands. - golden.BashTest(t, "./bash", golden.BashConfig{ + golden.ScriptTest(t, "./bash", golden.ScriptConfig{ DisplayStdout: true, DisplayStderr: true, OutputProcessConfig: golden.OutputProcessConfig{ diff --git a/run/tests/http/error_log/main_test.go b/run/tests/http/error_log/main_test.go index 59ae3df7..e5ad9455 100644 --- a/run/tests/http/error_log/main_test.go +++ b/run/tests/http/error_log/main_test.go @@ -18,7 +18,7 @@ func TestMain(m *testing.M) { // the output is compared against the expected one. func TestGoldenBash(t *testing.T) { // Execute the rest of the bash commands. - golden.BashTest(t, "./bash", golden.BashConfig{ + golden.ScriptTest(t, "./bash", golden.ScriptConfig{ DisplayStdout: true, DisplayStderr: true, OutputProcessConfig: golden.OutputProcessConfig{ diff --git a/run/tests/http/sync/main_test.go b/run/tests/http/sync/main_test.go index ccf9bdc7..40b831cb 100644 --- a/run/tests/http/sync/main_test.go +++ b/run/tests/http/sync/main_test.go @@ -18,7 +18,7 @@ func TestMain(m *testing.M) { // the output is compared against the expected one. func TestGoldenBash(t *testing.T) { // Execute the rest of the bash commands. - golden.BashTest(t, "./bash", golden.BashConfig{ + golden.ScriptTest(t, "./bash", golden.ScriptConfig{ DisplayStdout: true, DisplayStderr: true, }) diff --git a/run/tests/http/too_many_requests/main_test.go b/run/tests/http/too_many_requests/main_test.go index ccf9bdc7..40b831cb 100644 --- a/run/tests/http/too_many_requests/main_test.go +++ b/run/tests/http/too_many_requests/main_test.go @@ -18,7 +18,7 @@ func TestMain(m *testing.M) { // the output is compared against the expected one. func TestGoldenBash(t *testing.T) { // Execute the rest of the bash commands. - golden.BashTest(t, "./bash", golden.BashConfig{ + golden.ScriptTest(t, "./bash", golden.ScriptConfig{ DisplayStdout: true, DisplayStderr: true, }) diff --git a/run/tests/simple/main_test.go b/run/tests/simple/main_test.go index c013fef3..4fe5e78f 100644 --- a/run/tests/simple/main_test.go +++ b/run/tests/simple/main_test.go @@ -40,7 +40,7 @@ func TestGolden(t *testing.T) { // the output is compared against the expected one. func TestGoldenBash(t *testing.T) { // Execute the rest of the bash commands. - golden.BashTest(t, "./bash", golden.BashConfig{ + golden.ScriptTest(t, "./bash", golden.ScriptConfig{ DisplayStdout: true, DisplayStderr: true, }) From 408dbbd4c99b2bd6d695aa2fbe18ac89d769b435 Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 14 Jul 2025 11:23:29 +0200 Subject: [PATCH 02/12] Change default behavior --- golden/config.go | 5 ++++- golden/script.go | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/golden/config.go b/golden/config.go index 85cbf818..a6ac1834 100644 --- a/golden/config.go +++ b/golden/config.go @@ -76,7 +76,10 @@ type ScriptConfig struct { // ScriptExtensions is a list of script file extensions to be considered for // golden file tests alongside their respective command to execute them. // The key is the file extension, and the value is the command to execute - // the script. If not provided, it defaults to bash. I.e.: {".sh": "bash"}. + // the script. A typical definition for bash scripts looks like this: + // ScriptExtensions: map[string]string{ + // ".sh": "bash", + // } ScriptExtensions map[string]string // GoldenExtension is the file extension to use for the golden file. If not // provided, then the default extension (.golden) is used. diff --git a/golden/script.go b/golden/script.go index 7e099916..e909bd2e 100644 --- a/golden/script.go +++ b/golden/script.go @@ -37,6 +37,11 @@ func ScriptTest( t.Fatalf("dir %s does not exist", goldenDir) } + // If no script extensions are provided, we fail the test. + if len(scriptConfig.ScriptExtensions) == 0 { + t.Fatal("no script extensions provided in script config") + } + // Collect scripts. extensions := make([]string, 0, len(scriptConfig.ScriptExtensions)) for ext := range scriptConfig.ScriptExtensions { From acece148fd5829ca39d6646f11473508cef36a94 Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 14 Jul 2025 12:06:06 +0200 Subject: [PATCH 03/12] Provide BashTest convenience function --- golden/script.go | 23 +++++++++++++++---- run/tests/http/async/main_test.go | 2 +- run/tests/http/error_log/main_test.go | 2 +- run/tests/http/sync/main_test.go | 2 +- run/tests/http/too_many_requests/main_test.go | 2 +- run/tests/simple/main_test.go | 2 +- 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/golden/script.go b/golden/script.go index e909bd2e..18f751e0 100644 --- a/golden/script.go +++ b/golden/script.go @@ -22,11 +22,24 @@ type scriptTest struct { Command string } -// ScriptTest executes a golden file test for a script command. It walks over -// the goldenDir to gather all .sh scripts present in the dir. It then executes -// each of the scripts and compares expected vs. actual outputs. If -// displayStdout or displayStderr are true, the output of each script will be -// composed of the resulting stderr + stdout. +// BashTest calls ScriptTest with bash as the command to execute the scripts and +// the file extension .sh. This is a convenience function for bash scripts. +func BashTest( + t *testing.T, + goldenDir string, + scriptConfig ScriptConfig, +) { + scriptConfig.ScriptExtensions = map[string]string{ + ".sh": "bash", + } + ScriptTest(t, goldenDir, scriptConfig) +} + +// ScriptTest executes a golden file test for scripts. It walks over the +// goldenDir to gather all .sh scripts present in the dir. It then executes each +// of the scripts and compares expected vs. actual outputs. If displayStdout or +// displayStderr are true, the output of each script will be composed of the +// resulting stderr + stdout. func ScriptTest( t *testing.T, goldenDir string, diff --git a/run/tests/http/async/main_test.go b/run/tests/http/async/main_test.go index 1dfa2f85..e337f6e5 100644 --- a/run/tests/http/async/main_test.go +++ b/run/tests/http/async/main_test.go @@ -18,7 +18,7 @@ func TestMain(m *testing.M) { // the output is compared against the expected one. func TestGoldenBash(t *testing.T) { // Execute the rest of the bash commands. - golden.ScriptTest(t, "./bash", golden.ScriptConfig{ + golden.BashTest(t, "./bash", golden.ScriptConfig{ DisplayStdout: true, DisplayStderr: true, OutputProcessConfig: golden.OutputProcessConfig{ diff --git a/run/tests/http/error_log/main_test.go b/run/tests/http/error_log/main_test.go index e5ad9455..c39a46ba 100644 --- a/run/tests/http/error_log/main_test.go +++ b/run/tests/http/error_log/main_test.go @@ -18,7 +18,7 @@ func TestMain(m *testing.M) { // the output is compared against the expected one. func TestGoldenBash(t *testing.T) { // Execute the rest of the bash commands. - golden.ScriptTest(t, "./bash", golden.ScriptConfig{ + golden.BashTest(t, "./bash", golden.ScriptConfig{ DisplayStdout: true, DisplayStderr: true, OutputProcessConfig: golden.OutputProcessConfig{ diff --git a/run/tests/http/sync/main_test.go b/run/tests/http/sync/main_test.go index 40b831cb..da625544 100644 --- a/run/tests/http/sync/main_test.go +++ b/run/tests/http/sync/main_test.go @@ -18,7 +18,7 @@ func TestMain(m *testing.M) { // the output is compared against the expected one. func TestGoldenBash(t *testing.T) { // Execute the rest of the bash commands. - golden.ScriptTest(t, "./bash", golden.ScriptConfig{ + golden.BashTest(t, "./bash", golden.ScriptConfig{ DisplayStdout: true, DisplayStderr: true, }) diff --git a/run/tests/http/too_many_requests/main_test.go b/run/tests/http/too_many_requests/main_test.go index 40b831cb..da625544 100644 --- a/run/tests/http/too_many_requests/main_test.go +++ b/run/tests/http/too_many_requests/main_test.go @@ -18,7 +18,7 @@ func TestMain(m *testing.M) { // the output is compared against the expected one. func TestGoldenBash(t *testing.T) { // Execute the rest of the bash commands. - golden.ScriptTest(t, "./bash", golden.ScriptConfig{ + golden.BashTest(t, "./bash", golden.ScriptConfig{ DisplayStdout: true, DisplayStderr: true, }) diff --git a/run/tests/simple/main_test.go b/run/tests/simple/main_test.go index 4fe5e78f..746c4205 100644 --- a/run/tests/simple/main_test.go +++ b/run/tests/simple/main_test.go @@ -40,7 +40,7 @@ func TestGolden(t *testing.T) { // the output is compared against the expected one. func TestGoldenBash(t *testing.T) { // Execute the rest of the bash commands. - golden.ScriptTest(t, "./bash", golden.ScriptConfig{ + golden.BashTest(t, "./bash", golden.ScriptConfig{ DisplayStdout: true, DisplayStderr: true, }) From 9691ac3d9cb73c9c09302abc177a7a8c4d1a965a Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 14 Jul 2025 13:47:24 +0200 Subject: [PATCH 04/12] Providing default config constructors --- golden/config.go | 19 +++++++++++++++++++ golden/script.go | 11 ++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/golden/config.go b/golden/config.go index a6ac1834..92610e09 100644 --- a/golden/config.go +++ b/golden/config.go @@ -11,6 +11,25 @@ import ( const goldenExtension = ".golden" +// NewConfig creates a new Config with default values. +func NewConfig() Config { + return Config{ + GoldenExtension: goldenExtension, + } +} + +// NewScriptConfig creates a new ScriptConfig with default values. +func NewScriptConfig() ScriptConfig { + return ScriptConfig{ + DisplayStdout: true, + DisplayStderr: true, + ScriptExtensions: map[string]string{ + ".sh": "bash", + }, + GoldenExtension: goldenExtension, + } +} + // Config lets a user configure the golden file tests. type Config struct { // VerifyFunc is used to validate output against input, if provided. diff --git a/golden/script.go b/golden/script.go index 18f751e0..f4c33c46 100644 --- a/golden/script.go +++ b/golden/script.go @@ -35,11 +35,12 @@ func BashTest( ScriptTest(t, goldenDir, scriptConfig) } -// ScriptTest executes a golden file test for scripts. It walks over the -// goldenDir to gather all .sh scripts present in the dir. It then executes each -// of the scripts and compares expected vs. actual outputs. If displayStdout or -// displayStderr are true, the output of each script will be composed of the -// resulting stderr + stdout. +// ScriptTest executes a golden file test for scripts defined via +// ScriptExtensions in the config. It walks over the goldenDir to gather all +// scripts present in the dir (based on the extensions given by +// ScriptExtensions). It then executes each of the scripts and compares expected +// vs. actual outputs. If displayStdout or displayStderr are true, the output of +// each script will be composed of the resulting stderr + stdout. func ScriptTest( t *testing.T, goldenDir string, From ffcd43141ce6f3266aa1417f081c5b9ecef48825 Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 14 Jul 2025 13:48:03 +0200 Subject: [PATCH 05/12] Use default instead of empty config --- golden/dag.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/golden/dag.go b/golden/dag.go index ec5525f8..c5eeda48 100644 --- a/golden/dag.go +++ b/golden/dag.go @@ -71,7 +71,7 @@ func DagTest(t *testing.T, cases []DagTestCase) { var wg sync.WaitGroup for _, nextCase := range next { wg.Add(1) - config := ScriptConfig{} + config := NewScriptConfig() if nextCase.Config != nil { config = *nextCase.Config } From 00a3461172b9f2e93eda9dd689885e48587b1772 Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 14 Jul 2025 14:15:16 +0200 Subject: [PATCH 06/12] Use custom type over map --- golden/config.go | 23 ++++++++++++++++------- golden/script.go | 14 +++++++++----- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/golden/config.go b/golden/config.go index 92610e09..60487b13 100644 --- a/golden/config.go +++ b/golden/config.go @@ -23,8 +23,8 @@ func NewScriptConfig() ScriptConfig { return ScriptConfig{ DisplayStdout: true, DisplayStderr: true, - ScriptExtensions: map[string]string{ - ".sh": "bash", + ScriptExtensions: []ScriptExtension{ + {Extension: ".sh", Command: "bash"}, }, GoldenExtension: goldenExtension, } @@ -94,12 +94,11 @@ type ScriptConfig struct { OutputProcessConfig OutputProcessConfig // ScriptExtensions is a list of script file extensions to be considered for // golden file tests alongside their respective command to execute them. - // The key is the file extension, and the value is the command to execute - // the script. A typical definition for bash scripts looks like this: - // ScriptExtensions: map[string]string{ - // ".sh": "bash", + // A typical definition for bash scripts looks like this: + // ScriptExtensions: []ScriptExtension{ + // {Extension: ".sh", Command: "bash"}, // } - ScriptExtensions map[string]string + ScriptExtensions []ScriptExtension // GoldenExtension is the file extension to use for the golden file. If not // provided, then the default extension (.golden) is used. GoldenExtension string @@ -117,6 +116,16 @@ type ScriptConfig struct { WaitBefore time.Duration } +// ScriptExtension defines a script file extension and the command to execute +// it. This is used in the ScriptConfig to define which scripts should be +// considered for golden file tests and how to execute them. +type ScriptExtension struct { + // Extension is the file extension of the script, e.g., ".sh". + Extension string + // Command is the command to execute the script, e.g., "bash". + Command string +} + // TransientField represents a field that is transient, this is, dynamic in // nature. Examples of such fields include durations, times, versions, etc. // Transient fields are replaced in golden file tests to always obtain the same diff --git a/golden/script.go b/golden/script.go index f4c33c46..38343d4d 100644 --- a/golden/script.go +++ b/golden/script.go @@ -29,8 +29,8 @@ func BashTest( goldenDir string, scriptConfig ScriptConfig, ) { - scriptConfig.ScriptExtensions = map[string]string{ - ".sh": "bash", + scriptConfig.ScriptExtensions = []ScriptExtension{ + ScriptExtension{Extension: ".sh", Command: "bash"}, } ScriptTest(t, goldenDir, scriptConfig) } @@ -58,8 +58,12 @@ func ScriptTest( // Collect scripts. extensions := make([]string, 0, len(scriptConfig.ScriptExtensions)) - for ext := range scriptConfig.ScriptExtensions { - extensions = append(extensions, ext) + for _, ext := range scriptConfig.ScriptExtensions { + extensions = append(extensions, ext.Extension) + } + commands := make(map[string]string, len(scriptConfig.ScriptExtensions)) + for _, ext := range scriptConfig.ScriptExtensions { + commands[ext.Extension] = ext.Command } var scripts []scriptTest fn := func(path string, _ os.FileInfo, _ error) error { @@ -68,7 +72,7 @@ func ScriptTest( if slices.Contains(extensions, extension) { scripts = append(scripts, scriptTest{ Path: path, - Command: scriptConfig.ScriptExtensions[extension], + Command: commands[extension], }) } return nil From 345f1afecb3ac8fc7c2974fcbe85cf9d7d38aab8 Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 14 Jul 2025 14:22:44 +0200 Subject: [PATCH 07/12] Removing unnecessary typing --- golden/script.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/golden/script.go b/golden/script.go index 38343d4d..c07b1d3c 100644 --- a/golden/script.go +++ b/golden/script.go @@ -30,7 +30,7 @@ func BashTest( scriptConfig ScriptConfig, ) { scriptConfig.ScriptExtensions = []ScriptExtension{ - ScriptExtension{Extension: ".sh", Command: "bash"}, + {Extension: ".sh", Command: "bash"}, } ScriptTest(t, goldenDir, scriptConfig) } From 9587687c13ffebf0bc4c1e5cd6cb3f778f1293c6 Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 14 Jul 2025 15:18:08 +0200 Subject: [PATCH 08/12] Allow prefix args --- golden/config.go | 5 +++++ golden/script.go | 31 +++++++++++++++++++------------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/golden/config.go b/golden/config.go index 60487b13..2d62f4c7 100644 --- a/golden/config.go +++ b/golden/config.go @@ -124,6 +124,11 @@ type ScriptExtension struct { Extension string // Command is the command to execute the script, e.g., "bash". Command string + // PrefixArgs are the arguments to be passed to the command before the + // script file name. This is useful for commands that require additional + // arguments, e.g., "powershell" requires "-File" before the script file + // name. + PrefixArgs []string } // TransientField represents a field that is transient, this is, dynamic in diff --git a/golden/script.go b/golden/script.go index c07b1d3c..e5787d54 100644 --- a/golden/script.go +++ b/golden/script.go @@ -7,7 +7,6 @@ import ( "os" "os/exec" "path/filepath" - "slices" "testing" "time" @@ -20,6 +19,9 @@ type scriptTest struct { Path string // Command is the command to execute the script. Command string + // PrefixArgs are the arguments to be passed to the command before the + // script file name. + PrefixArgs []string } // BashTest calls ScriptTest with bash as the command to execute the scripts and @@ -57,24 +59,29 @@ func ScriptTest( } // Collect scripts. - extensions := make([]string, 0, len(scriptConfig.ScriptExtensions)) + definitions := make(map[string]ScriptExtension) for _, ext := range scriptConfig.ScriptExtensions { - extensions = append(extensions, ext.Extension) - } - commands := make(map[string]string, len(scriptConfig.ScriptExtensions)) - for _, ext := range scriptConfig.ScriptExtensions { - commands[ext.Extension] = ext.Command + if ext.Extension == "" || ext.Command == "" { + t.Fatal("script extension has empty extension or command") + } + if _, exists := definitions[ext.Extension]; exists { + t.Fatalf("script extension %s already defined", ext.Extension) + } + definitions[ext.Extension] = ext } var scripts []scriptTest fn := func(path string, _ os.FileInfo, _ error) error { // Check if the file should be considered as a script to test. extension := filepath.Ext(path) - if slices.Contains(extensions, extension) { - scripts = append(scripts, scriptTest{ - Path: path, - Command: commands[extension], - }) + if _, exists := definitions[extension]; !exists { + return nil // Skip this file, it's not a script we want to test. } + // Add to the list of scripts to test. + scripts = append(scripts, scriptTest{ + Path: path, + Command: definitions[extension].Command, + PrefixArgs: definitions[extension].PrefixArgs, + }) return nil } if err := filepath.Walk(goldenDir, fn); err != nil { From eecd22ea9acd673dfc494bdf767c2b504067d699 Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Thu, 17 Jul 2025 02:00:44 +0200 Subject: [PATCH 09/12] Fixing dag multi script support --- golden/dag.go | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/golden/dag.go b/golden/dag.go index c5eeda48..b8a230fd 100644 --- a/golden/dag.go +++ b/golden/dag.go @@ -2,6 +2,7 @@ package golden import ( "fmt" + "path/filepath" "sync" "testing" ) @@ -76,11 +77,17 @@ func DagTest(t *testing.T, cases []DagTestCase) { config = *nextCase.Config } - nextCase := nextCase + // Get the script extension for the test case. + ext, err := dagGetScriptExtension(nextCase.Path, config) + if err != nil { + t.Fatal(err) + } + + nextCase := nextCase // Capture the variable for the goroutine. go func() { + defer wg.Done() // Run the test case. - BashTestFile(t, nextCase.Path, config) - wg.Done() + ScriptTestFile(t, ext.Command, nextCase.Path, config) }() } @@ -100,6 +107,18 @@ func DagTest(t *testing.T, cases []DagTestCase) { } } +func dagGetScriptExtension(path string, config ScriptConfig) (ScriptExtension, error) { + // Get extension from the path. + ext := filepath.Ext(path) + // Search for fitting script definition among config.ScriptExtensions. + for _, def := range config.ScriptExtensions { + if def.Extension == ext { + return def, nil + } + } + return ScriptExtension{}, fmt.Errorf("no script definition found for path %s with extension %s", path, ext) +} + func validate(cases []DagTestCase) error { // Ensure that all cases have unique names. names := make(map[string]bool) From 0e10fc3db65e4b83c4be32c59bd1d8f2b8d80a97 Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Thu, 17 Jul 2025 02:16:47 +0200 Subject: [PATCH 10/12] Use default script extensions if not provided --- golden/dag.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/golden/dag.go b/golden/dag.go index b8a230fd..3e39115c 100644 --- a/golden/dag.go +++ b/golden/dag.go @@ -76,6 +76,10 @@ func DagTest(t *testing.T, cases []DagTestCase) { if nextCase.Config != nil { config = *nextCase.Config } + if len(config.ScriptExtensions) == 0 { + // Default script extension if none is provided. + config.ScriptExtensions = []ScriptExtension{{Extension: ".sh", Command: "bash"}} + } // Get the script extension for the test case. ext, err := dagGetScriptExtension(nextCase.Path, config) From 7bbea3d0b34b48bfe82648bdafb58beea2924a1f Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Thu, 17 Jul 2025 22:53:40 +0200 Subject: [PATCH 11/12] Adding EOL handling support --- golden/config.go | 22 ++++++++++++++++++++++ golden/eol.go | 20 ++++++++++++++++++++ golden/file.go | 2 ++ golden/script.go | 4 ++++ 4 files changed, 48 insertions(+) create mode 100644 golden/eol.go diff --git a/golden/config.go b/golden/config.go index 2d62f4c7..00d608c0 100644 --- a/golden/config.go +++ b/golden/config.go @@ -116,6 +116,24 @@ type ScriptConfig struct { WaitBefore time.Duration } +// NewLineStyle defines the style of new lines to be used on the output before +// comparison. +type NewLineStyle string + +const ( + // NewLineStyleUntouched indicates that the output should be kept untouched + // and not modified. + NewLineStyleUntouched NewLineStyle = "" + // NewLineStyleLF indicates that the output should be converted to LF (Line + // Feed) before comparison. This is the default style used in Unix-like + // systems. + NewLineStyleLF NewLineStyle = "LF" + // NewLineStyleCRLF indicates that the output should be converted to CRLF + // (Carriage Return + Line Feed) before comparison. This is the default + // style used in Windows systems. + NewLineStyleCRLF NewLineStyle = "CRLF" +) + // ScriptExtension defines a script file extension and the command to execute // it. This is used in the ScriptConfig to define which scripts should be // considered for golden file tests and how to execute them. @@ -214,6 +232,10 @@ type OutputProcessConfig struct { // KeepVolatileData indicates whether to keep or replace frequently // changing data. KeepVolatileData bool + // NewLineStyle defines the new line style to be used on the output. I.e., + // the output can be kept untouched, or converted to LF or CRLF before + // comparison. + NewLineStyle NewLineStyle // TransientFields are keys that hold values which are transient (dynamic) // in nature, such as the elapsed time, version, start time, etc. Transient // fields have a special parsing in the .golden file and they are diff --git a/golden/eol.go b/golden/eol.go new file mode 100644 index 00000000..52d54b13 --- /dev/null +++ b/golden/eol.go @@ -0,0 +1,20 @@ +package golden + +import ( + "bytes" +) + +// convertNewLineStyle converts the new line style of the content based on the +// specified NewLineStyle. +func convertNewLineStyle(content []byte, style NewLineStyle) []byte { + switch style { + case NewLineStyleUntouched: + return content // Keep the original line endings + case NewLineStyleLF: + return bytes.ReplaceAll(content, []byte{'\r', '\n'}, []byte{'\n'}) + case NewLineStyleCRLF: + return bytes.ReplaceAll(content, []byte{'\n'}, []byte{'\r', '\n'}) + default: + return content // Default case, keep as is + } +} diff --git a/golden/file.go b/golden/file.go index 60025400..09500bb6 100644 --- a/golden/file.go +++ b/golden/file.go @@ -169,6 +169,8 @@ func comparison( ) } + actualBytes = convertNewLineStyle(actualBytes, config.OutputProcessConfig.NewLineStyle) + outputWithTransient := map[string]any{} flattenedOutput := map[string]any{} if !config.CompareConfig.TxtParse { diff --git a/golden/script.go b/golden/script.go index e5787d54..730a84f7 100644 --- a/golden/script.go +++ b/golden/script.go @@ -279,6 +279,10 @@ func processOutput( } } + // Apply new line style (if specified). + out = convertNewLineStyle(out, config.NewLineStyle) + + // Work on string from here on. got := string(out) // Apply regex replacements for volatile data. From da476d2fcd35e789d4beee422e7ecde53c522a2d Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Sun, 17 Aug 2025 22:15:38 +0200 Subject: [PATCH 12/12] Remove unnecessary newline --- golden/dag.go | 1 - 1 file changed, 1 deletion(-) diff --git a/golden/dag.go b/golden/dag.go index 31ebdeeb..c7f8ef60 100644 --- a/golden/dag.go +++ b/golden/dag.go @@ -3,7 +3,6 @@ package golden import ( "fmt" "path/filepath" - "strings" "sync" "testing"