From 4ef338ffeaa062b01ee7dd13b40183973564567a Mon Sep 17 00:00:00 2001 From: Richard Jones Date: Fri, 7 Nov 2025 14:52:57 -0700 Subject: [PATCH 1/3] refactored disparate variables into single config struct; engine.Run returns error instead of bool --- cmd/root.go | 29 +++++++---------------------- internal/engine/config.go | 24 +++++++++++------------- internal/engine/engine.go | 29 ++++++++++++++++++----------- 3 files changed, 36 insertions(+), 46 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 430f480..6daefdc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,19 +5,13 @@ www.aquia.us package cmd import ( - "fmt" "os" - "strings" "github.com/aquia-inc/emberfall/internal/engine" "github.com/spf13/cobra" ) -var ( - configPath string - urlPattern string - methodPattern string -) +var config *engine.Config var rootCmd = &cobra.Command{ Use: "emberfall", @@ -47,18 +41,8 @@ tests: # key:value pairs `, Version: "0.3.2", - Run: func(cmd *cobra.Command, args []string) { - - configPath = strings.TrimSpace(configPath) - conf, err := engine.LoadConfig(configPath) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - if !engine.Run(conf, urlPattern, methodPattern) { - os.Exit(2) - } + RunE: func(cmd *cobra.Command, args []string) error { + return engine.Run(config) }, } @@ -70,8 +54,9 @@ func Execute() { } func init() { + config = &engine.Config{} flags := rootCmd.Flags() - flags.StringVarP(&configPath, "config", "c", "-", "Path to config file. - to read from stdin") - flags.StringVarP(&urlPattern, "url", "u", "", "Regular expression to include only tests with a matching url") - flags.StringVarP(&methodPattern, "method", "m", "", "Regular expression to include only tests with a matching method") + flags.StringVarP(&config.TestsPath, "tests", "t", "-", "Path to tests configuration file. - to read from stdin") + flags.StringVarP(&config.UrlPattern, "url", "u", "", "Regular expression to include only tests with a matching url") + flags.StringVarP(&config.MethodPattern, "method", "m", "", "Regular expression to include only tests with a matching method") } diff --git a/internal/engine/config.go b/internal/engine/config.go index 8934de6..c55f00e 100644 --- a/internal/engine/config.go +++ b/internal/engine/config.go @@ -9,40 +9,38 @@ import ( "gopkg.in/yaml.v3" ) -type config struct { - Tests []*test `yaml:"tests"` +type Config struct { + TestsPath, UrlPattern, MethodPattern string + Tests []*test `yaml:"tests"` } -func LoadConfig(configPath string) (*config, error) { +func (c *Config) LoadTests() error { var ( b []byte err error ) - fmt.Printf("Reading config from %s\n", configPath) + fmt.Printf("Reading config from %s\n", c.TestsPath) var stat fs.FileInfo - if configPath == "-" { + if c.TestsPath == "-" { stat, err = os.Stdin.Stat() if err != nil { - return nil, err + return err } if stat.Size() < 1 { - return nil, fmt.Errorf("no config provided") + return fmt.Errorf("no config provided") } b, err = io.ReadAll(os.Stdin) } else { - b, err = os.ReadFile(configPath) + b, err = os.ReadFile(c.TestsPath) } if err != nil { - return nil, err + return err } - conf := &config{} - err = yaml.Unmarshal(b, conf) - - return conf, err + return yaml.Unmarshal(b, c) } diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 2546a0c..d7ac99a 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -9,8 +9,7 @@ import ( "regexp" ) -// TODO: refactor to put all passed flag values into a single struct -func Run(cfg *config, urlPattern, methodPattern string) bool { +func Run(cfg *Config) error { // reduce memory allocations by reusing as many var ( client = &http.Client{} @@ -22,19 +21,22 @@ func Run(cfg *config, urlPattern, methodPattern string) bool { err error ) - if urlPattern != "" { - includedURLs, err = regexp.Compile(urlPattern) + err = cfg.LoadTests() + if err != nil { + return err + } + + if cfg.UrlPattern != "" { + includedURLs, err = regexp.Compile(cfg.UrlPattern) if err != nil { - fmt.Println(err) - return false + return err } } - if methodPattern != "" { - includedMethods, err = regexp.Compile(methodPattern) + if cfg.MethodPattern != "" { + includedMethods, err = regexp.Compile(cfg.MethodPattern) if err != nil { - fmt.Println(err) - return false + return err } } @@ -136,7 +138,12 @@ func Run(cfg *config, urlPattern, methodPattern string) bool { } // end for fmt.Printf("\n Ran: %d\n Failed: %d\nSkipped: %d\n", ran, failed, skipped) - return (failed == 0) + + if failed > 0 { + return errors.New("tests failed") + } + + return nil } func noRedirect(req *http.Request, via []*http.Request) error { From fa3ceb3326ac4597d7e04ad5e28d6c916246c76f Mon Sep 17 00:00:00 2001 From: Richard Jones Date: Fri, 7 Nov 2025 14:58:30 -0700 Subject: [PATCH 2/3] fix tests --- tests/cli.bats | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/cli.bats b/tests/cli.bats index 2d9d100..94d4932 100644 --- a/tests/cli.bats +++ b/tests/cli.bats @@ -17,113 +17,113 @@ setup() { } @test "SHOULD FAIL without following redirect" { - run ./emberfall --config ./tests/fail-no-follow.yml + run ./emberfall --tests ./tests/fail-no-follow.yml assert_failure assert_output --partial 'FAIL' assert_output --partial 'expected status == 200 got 301' } @test "SHOULD PASS by following redirect" { - run ./emberfall --config ./tests/pass-follow.yml + run ./emberfall --tests ./tests/pass-follow.yml assert_success assert_output --partial 'PASS' } @test "SHOULD PASS with expected headers" { - run ./emberfall --config ./tests/pass-headers.yml + run ./emberfall --tests ./tests/pass-headers.yml assert_success assert_output --partial 'PASS' } @test "SHOULD FAIL with missing headers" { - run ./emberfall --config ./tests/fail-missing-headers.yml + run ./emberfall --tests ./tests/fail-missing-headers.yml assert_failure assert_output --partial 'FAIL' assert_output --partial 'expected header x-no-exist was missing' } @test "SHOULD FAIL with bad url" { - run ./emberfall --config ./tests/fail-bad-url.yml + run ./emberfall --tests ./tests/fail-bad-url.yml assert_failure assert_output --partial 'no such host' } @test "SHOULD PASS with response JSON == request JSON" { - run ./emberfall --config ./tests/pass-req-res-json-match.yml + run ./emberfall --tests ./tests/pass-req-res-json-match.yml assert_success } @test "SHOULD FAIL with response JSON != request JSON" { - run ./emberfall --config ./tests/fail-req-res-json-no-match.yml + run ./emberfall --tests ./tests/fail-req-res-json-no-match.yml assert_failure assert_output --partial 'expected body.json.data.foo == baz got bar' } @test "SHOULD PASS with response text" { - run ./emberfall --config ./tests/pass-req-res-text-match.yml + run ./emberfall --tests ./tests/pass-req-res-text-match.yml assert_success } @test "SHOULD FAIL with response text no match" { - run ./emberfall --config ./tests/fail-req-res-text-no-match.yml + run ./emberfall --tests ./tests/fail-req-res-text-no-match.yml assert_failure assert_output --partial 'expected body.json.data == baz got bar' } @test "SHOULD PASS with 404 on interpolated path" { - run ./emberfall --config ./tests/pass-test-dependencies.yml + run ./emberfall --tests ./tests/pass-test-dependencies.yml assert_success assert_output --partial 'PASS : GET https://postman-echo.com/baz' } @test "SHOULD PASS float equals float" { - run ./emberfall --config ./tests/pass-numbers-float-equals-float.yml + run ./emberfall --tests ./tests/pass-numbers-float-equals-float.yml assert_success } @test "SHOULD PASS int equals int" { - run ./emberfall --config ./tests/pass-numbers-int-equals-int.yml + run ./emberfall --tests ./tests/pass-numbers-int-equals-int.yml assert_success } @test "SHOULD FAIL int equals int" { - run ./emberfall --config ./tests/fail-numbers-int-equals-int.yml + run ./emberfall --tests ./tests/fail-numbers-int-equals-int.yml assert_failure assert_output --partial 'expected body.json.data.num == 1 got 2' } @test "SHOULD FAIL int equals float" { - run ./emberfall --config ./tests/fail-numbers-int-equals-float.yml + run ./emberfall --tests ./tests/fail-numbers-int-equals-float.yml assert_failure assert_output --partial 'expected body.json.data.num == 1 got 1.1' } @test "SHOULD FAIL float equals float" { - run ./emberfall --config ./tests/fail-numbers-float-equals-float.yml + run ./emberfall --tests ./tests/fail-numbers-float-equals-float.yml assert_failure assert_output --partial 'expected body.json.data.num == 2.2 got 3.3' } @test "SHOULD FAIL string equals float" { - run ./emberfall --config ./tests/fail-numbers-string-equals-float.yml + run ./emberfall --tests ./tests/fail-numbers-string-equals-float.yml assert_failure assert_output --partial 'expected body.json.data.num == 1 got 1.1' } @test "SHOULD FAIL string equals int" { - run ./emberfall --config ./tests/fail-numbers-string-equals-int.yml + run ./emberfall --tests ./tests/fail-numbers-string-equals-int.yml assert_failure assert_output --partial 'expected body.json.data.num == 1 got 2' } @test "SHOULD FAIL but body response gets printed" { - run ./emberfall --config ./tests/fail-response-printed.yml + run ./emberfall --tests ./tests/fail-response-printed.yml assert_failure assert_output --partial '"status": 400' } @test "SHOULD PASS include exactly 200" { - run ./emberfall --config ./tests/include-exclude.yml --url 'status/200' + run ./emberfall --tests ./tests/include-exclude.yml --url 'status/200' assert_success assert_output --partial 'PASS : GET https://postman-echo.com/status/200' assert_output --partial 'Ran: 1' @@ -131,7 +131,7 @@ setup() { } @test "SHOULD PASS include all 200 status" { - run ./emberfall --config ./tests/include-exclude.yml -u 'status/2\d{2}' + run ./emberfall --tests ./tests/include-exclude.yml -u 'status/2\d{2}' assert_success assert_output --partial 'PASS : GET https://postman-echo.com/status/200' assert_output --partial 'PASS : GET https://postman-echo.com/status/201' @@ -140,7 +140,7 @@ setup() { } @test "SHOULD PASS include all but 200" { - run ./emberfall --config ./tests/include-exclude.yml -u 'status/[13-5]\d{2}' + run ./emberfall --tests ./tests/include-exclude.yml -u 'status/[13-5]\d{2}' assert_success assert_output --partial 'PASS : GET https://postman-echo.com/status/301' assert_output --partial 'PASS : GET https://postman-echo.com/status/302' @@ -149,13 +149,13 @@ setup() { } @test "SHOULD FAIL include invalid regular expression" { - run ./emberfall --config ./tests/include-exclude.yml -u '[[200' + run ./emberfall --tests ./tests/include-exclude.yml -u '[[200' assert_failure assert_output --partial 'error parsing regexp: missing closing ]: `[[200`' } @test "SHOULD PASS include method POST" { - run ./emberfall --config ./tests/include-exclude.yml -m 'POST' + run ./emberfall --tests ./tests/include-exclude.yml -m 'POST' assert_success assert_output --partial "PASS : POST https://postman-echo.com/status/201" assert_output --partial "Ran: 1" From 7e2aa90b35978bea5b79ccb5776dafa2cd8fdf1e Mon Sep 17 00:00:00 2001 From: Richard Jones Date: Fri, 7 Nov 2025 15:05:33 -0700 Subject: [PATCH 3/3] bump version; fix test --- README.md | 14 +++++++------- cmd/root.go | 2 +- tests/cli.bats | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f361021..c8d485e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Simply declare a list of URLs along with their expected response values, and Emb emberfall [flags] Flags: - -c, --config string Path to config file. - to read from stdin (default "-") + -c, --tests string Path to tests config file. - to read from stdin (default "-") -h, --help help for emberfall -u, --url string Regular expression to include only tests with a matching url -v, --version version for emberfall @@ -22,8 +22,8 @@ Flags: ## Configuring Tests The YAML tests config can be provided in two ways: -- as a file: `emberfall --config path/to/config.yaml` -- piped to stdin: `echo $EMBERFALL_CONFIG | emberfall --config -` +- as a file: `emberfall --tests path/to/config.yaml` +- piped to stdin: `echo $EMBERFALL_CONFIG | emberfall --tests -` Tests are defined in a simple YAML document with the following schema: ```yaml @@ -131,7 +131,7 @@ go install ./... ### Running Tests -Define tests in a YAML file like show above, and run emberfall: `emberfall --config path/to/config.yaml` +Define tests in a YAML file like show above, and run emberfall: `emberfall --tests path/to/config.yaml` ## As a Github Action @@ -139,7 +139,7 @@ Define tests in a YAML file like show above, and run emberfall: `emberfall --con ```yaml uses: "aquia-inc/emberfall@main" with: - version: 0.3.2 + version: 0.4.0 config: # string: YAML tests config inlined file: # string: path/to/tests ``` @@ -153,7 +153,7 @@ This is helpful for either short tests or for testing Emberfall integration with ```yaml uses: "aquia-inc/emberfall@main" with: - version: 0.3.2 + version: 0.4.0 config: | --- tests: @@ -173,6 +173,6 @@ For longer tests it's best to place those in their own file like so ```yaml uses: "aquia-inc/emberfall@main" with: - version: 0.3.2 + version: 0.4.0 file: path/to/tests.yml ``` diff --git a/cmd/root.go b/cmd/root.go index 6daefdc..db219c3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -40,7 +40,7 @@ tests: headers: object # optional, headers expected to be present in the response # key:value pairs `, - Version: "0.3.2", + Version: "0.4.0", RunE: func(cmd *cobra.Command, args []string) error { return engine.Run(config) }, diff --git a/tests/cli.bats b/tests/cli.bats index 94d4932..3c97ef9 100644 --- a/tests/cli.bats +++ b/tests/cli.bats @@ -7,7 +7,7 @@ setup() { @test "--version should be correct" { run ./emberfall --version assert_success - assert_output "emberfall version 0.3.2" + assert_output "emberfall version 0.4.0" } @test "no config SHOULD FAIL" { @@ -119,7 +119,7 @@ setup() { @test "SHOULD FAIL but body response gets printed" { run ./emberfall --tests ./tests/fail-response-printed.yml assert_failure - assert_output --partial '"status": 400' + assert_output --partial '"status":400' } @test "SHOULD PASS include exactly 200" {