Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions cmd/cmd_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ func testdataExpectedJUnitXML() string {
if runtime.GOOS == "windows" {
timeoutExitCodeFailure = "\n <failure>expected exit code 0, got 3221225786</failure>"
}
return fmt.Sprintf(`<testsuite name="Test JUnit XML output" tests="9" failures="5" errors="1" skipped="0" time="ELAPSED" timestamp="STARTED">

// The warn_* tests fail because --jobs is set to 1 and they are executed after the timeout test file.
return fmt.Sprintf(`<testsuite name="Test JUnit XML output" tests="11" failures="7" errors="1" skipped="0" time="ELAPSED" timestamp="STARTED">
<testcase classname="Test JUnit XML output" name="Fail: expected 0, got 3" time="ELAPSED">
<failure>expected exit code 0, got 3</failure>
<system-out># Step 1:&#xA;+ exit 3&#xA;</system-out>
Expand All @@ -35,7 +37,7 @@ func testdataExpectedJUnitXML() string {
</testcase>
<testcase classname="Test JUnit XML output" name="Fail: test environment variable values" time="ELAPSED">
<failure>expected exit code 0, got 1</failure>
<system-out># Step 1:&#xA;berry=banana&#xA;fruit=apple&#xA;# Step 2:&#xA;+ test banana = strawberry&#xA;</system-out>
<system-out># Step 1:&#xA;+ berry=banana&#xA;berry=banana&#xA;+ fruit=apple&#xA;fruit=apple&#xA;+ berry_fruit=&#34;${berry}-${fruit}&#34;&#xA;berry_fruit=banana-apple&#xA;# Step 2:&#xA;+ test banana = strawberry&#xA;</system-out>
</testcase>
<testcase classname="Test JUnit XML output" name="Fail: failing step, skipped step, and (failing) cleanup step" time="ELAPSED">
<failure>expected exit code 0, got 4</failure>
Expand All @@ -50,12 +52,20 @@ func testdataExpectedJUnitXML() string {
<system-out># Step 1:&#xA;# No output&#xA;</system-out>
</testcase>
<testcase classname="Test JUnit XML output" name="Success: skip sh step with when expression" time="ELAPSED">
<system-out># Step 1:&#xA;+ exit 0&#xA;# Step 2:&#xA;# Skipped&#xA;# Step 3:&#xA;VAR=VALUE&#xA;# Step 4:&#xA;%s&#xA;Environment variable VAR is set to VALUE&#xA;# Step 5:&#xA;# Skipped&#xA;</system-out>
<system-out># Step 1:&#xA;+ exit 0&#xA;# Step 2:&#xA;# Skipped&#xA;# Step 3:&#xA;+ VAR=VALUE&#xA;VAR=VALUE&#xA;# Step 4:&#xA;%s&#xA;Environment variable VAR is set to VALUE&#xA;# Step 5:&#xA;# Skipped&#xA;</system-out>
</testcase>
<testcase classname="Test JUnit XML output" name="Sleep" time="ELAPSED">
<failure>test run timeout exceeded</failure>%s
<system-out># Step 1:&#xA;+ sleep 600&#xA;# Step 2:&#xA;# Skipped&#xA;</system-out>
</testcase>
<testcase classname="Test JUnit XML output" name="Warn: variable key should not contain whitespace" time="ELAPSED">
<failure>test run timeout exceeded</failure>
<system-out># Step 1:&#xA;+ VAR with whitespace=value&#xA;VAR with whitespace=value&#xA;# Warning: variable key should not contain whitespace: VAR with whitespace=value&#xA;</system-out>
</testcase>
<testcase classname="Test JUnit XML output" name="Warn: variable values that contain whitespace should be quoted" time="ELAPSED">
<failure>test run timeout exceeded</failure>
<system-out># Step 1:&#xA;+ VAR=Value with whitespace&#xA;VAR=Value with whitespace&#xA;# Warning: variable values that contain whitespace should be quoted: VAR=Value with whitespace&#xA;</system-out>
</testcase>
</testsuite>`, xEchoCommandWithQuotes, timeoutExitCodeFailure)
}

Expand Down Expand Up @@ -106,7 +116,7 @@ func TestRoot_testdata(t *testing.T) {
},
{
testPath: "../testdata",
exitCode: 6,
exitCode: 8,
extraArgs: []string{"--name", "Test JUnit XML output"},
junitXML: testdataExpectedJUnitXML(),
},
Expand All @@ -124,6 +134,30 @@ func TestRoot_testdata(t *testing.T) {
extraArgs: []string{"-e", "berry=strawberry", "--env", "fruit=orange"},
exitCode: 0,
},
{
testPath: "../testdata/warn_variable_key_whitespace.md",
exitCode: 0,
},
{
testPath: "../testdata/warn_variable_value_whitespace.md",
exitCode: 0,
},
{
testPath: "../testdata/warn_variable_key_whitespace.md",
extraArgs: []string{"--warnings-as-errors"},
exitCode: 1,
junitXML: `<testsuite name="Test JUnit XML output" tests="1" failures="1" errors="0" skipped="0" time="ELAPSED" timestamp="STARTED">
<testcase classname="Test JUnit XML output" name="Warn: variable key should not contain whitespace" time="ELAPSED">
<failure>variable key should not contain whitespace: VAR with whitespace=value</failure>
<system-out># Step 1:&#xA;+ VAR with whitespace=value&#xA;VAR with whitespace=value&#xA;</system-out>
</testcase>
</testsuite>`,
},
{
testPath: "../testdata/warn_variable_value_whitespace.md",
extraArgs: []string{"--warnings-as-errors"},
exitCode: 1,
},
} {
t.Run(test.testPath, func(t *testing.T) {
args := []string{"--timeout", "1s"}
Expand Down
25 changes: 14 additions & 11 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import (
)

var (
env []string
name string
numberOfJobs int
timeout time.Duration
junitXML string
env []string
name string
numberOfJobs int
timeout time.Duration
warningsAsErrors bool
junitXML string

rootCmd = &cobra.Command{
Use: "mdtest [flags] path ...",
Expand All @@ -30,17 +31,19 @@ func init() {
rootCmd.Flags().StringVar(&name, "name", "", "name for the testsuite to be printed into the console and to be used as the testsuite name in JUnit XML report")
rootCmd.Flags().StringVarP(&junitXML, "junit-xml", "x", "", "generate JUnit XML report to the specified `path`")
rootCmd.Flags().DurationVar(&timeout, "timeout", 0, "timeout for the test run as a `duration` string, e.g., 1s, 1m, 1h")
rootCmd.Flags().BoolVar(&warningsAsErrors, "warnings-as-errors", false, "treat warnings as errors, i.e., fail the test run if any warnings are encountered")
rootCmd.RunE = func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
cmd.SilenceErrors = true

params := testrun.RunParameters{
Env: env,
Name: name,
NumberOfJobs: numberOfJobs,
OutputTarget: rootCmd.OutOrStdout(),
Timeout: timeout,
JUnitXML: junitXML,
Env: env,
Name: name,
NumberOfJobs: numberOfJobs,
OutputTarget: rootCmd.OutOrStdout(),
Timeout: timeout,
JUnitXML: junitXML,
WarningsAsErrors: warningsAsErrors,
}

res := testrun.Execute(args, params)
Expand Down
49 changes: 46 additions & 3 deletions examples/04_setting_env_variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ test -z "$example_var"
The above test will fail because $example_var is not set (or it is empty). Let's defined it in a env block.

```env
example_var=Example value
example_var="Example value"

another_var=Another value
another_var="Another value"
```

The variable is now defined in following test steps.
Expand All @@ -23,4 +23,47 @@ test -n "$example_var"
test -n "$another_var"
```

The environment variables defined in `env` code-block can be overridden by defining environment variable with the same name in the `mdtest` command using `-e`/`--env` parameter, for example `--env TARGET=TEST`.
The variable definitions can reference other variables by using `$key` or `${key}` syntax. For example:

```env
FRUIT=apple
COLOR=red
UNQUOTED=${COLOR}-${FRUIT}
SINGLE_QUOTED='${COLOR}-${FRUIT}'
DOUBLE_QUOTED="${COLOR}-${FRUIT}"
```

We can use `test` command to verify that the variables are expanded as expected:

```sh
test "$UNQUOTED" = "red-apple"
test "$SINGLE_QUOTED" = '${COLOR}-${FRUIT}'
test "$DOUBLE_QUOTED" = "red-apple"
```

Use `#` character at the beginning of a line to add comments to `env` code block.

```env
# This is a comment
#commented=value

COLOR=#7b00ff
```

The first two lines in the above code block are ignored, so `commented` variable is not defined in following test steps.

```sh
test -z "$commented"
test "$COLOR" = "#7b00ff"
```

## Variable precedence

The environment variables defined in `env` code-block can be overridden by defining environment variable with the same name in the `mdtest` command using `-e`/`--env` parameter, for example `--env TARGET=TEST`.

Full precedence order of environment variables is as follows (from lowest to highest):

1. Environment variables from the parent process
2. Environment variables defined in `env` code blocks
3. Environment variables defined in `mdtest` command using `-e`/`--env` parameter
4. Built-in environment variables (e.g. `MDTEST_VERSION` and `MDTEST_WORKSPACE`)
85 changes: 82 additions & 3 deletions testcase/envstep.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ package testcase

import (
"context"
"errors"
"fmt"
"maps"
"os"
"regexp"
"strings"

"github.com/UpCloudLtd/mdtest/utils"
Expand All @@ -13,12 +18,86 @@ type envStep struct {

var _ Step = envStep{}

var (
whitespaceRe = regexp.MustCompile(`\s`)
singlequoteRe = regexp.MustCompile(`^'[^']*'$`)
doublequoteRe = regexp.MustCompile(`^".*"$`)
)

func parseValue(value string, env EnvBySource, envUpdates []string) (string, error) {
var err error

// Do not expand variables in single quotes, but remove the quotes
if singlequoteRe.MatchString(value) {
return value[1 : len(value)-1], nil
}

// Remove double quotes
if doublequoteRe.MatchString(value) {
value = value[1 : len(value)-1]
} else if whitespaceRe.MatchString(value) {
// Return an error (with the parsed value) if unquoted value contains whitespace, as it would fail when executed in shell. The error is shown as a warning in the test output.
err = errors.New("variable values that contain whitespace should be quoted")
}

// Expand variables in unquoted and double-quoted values
return os.Expand(value, func(key string) string {
env = maps.Clone(env)
env[EnvSourceTestcase] = append(env[EnvSourceTestcase], envUpdates...)

m := utils.EnvEntriesAsMap(env.Merge())
return m[key]
}), err
}

func (s envStep) Execute(_ context.Context, t *testStatus) StepResult {
t.Env[EnvSourceTestcase] = append(t.Env[EnvSourceTestcase], s.envUpdates...)
var envUpdates []string
var output strings.Builder
var warnings []string

for _, line := range s.envUpdates {
trimmed := strings.TrimSpace(line)

// Skip empty lines without adding them to output
if trimmed == "" {
continue
}

// Skip comments after adding them to output
if strings.HasPrefix(trimmed, "#") {
output.WriteString(fmt.Sprintf("%s\n", trimmed))
continue
}

output.WriteString(fmt.Sprintf("+ %s\n", trimmed))

parts := strings.SplitN(trimmed, "=", 2)
if whitespaceRe.MatchString(parts[0]) {
warnings = append(warnings, fmt.Sprintf(`variable key should not contain whitespace: %s`, trimmed))
}

if len(parts) < 2 {
envUpdates = append(envUpdates, trimmed)
output.WriteString(fmt.Sprintf("%s=", parts[0]))
continue
}

value, err := parseValue(parts[1], t.Env, envUpdates)
if err != nil {
warnings = append(warnings, fmt.Sprintf(`%s: %s`, err.Error(), trimmed))
}
envUpdate := fmt.Sprintf("%s=%s", parts[0], value)

envUpdates = append(envUpdates, envUpdate)
output.WriteString(fmt.Sprintln(envUpdate))
}

t.Env[EnvSourceTestcase] = append(t.Env[EnvSourceTestcase], envUpdates...)

return StepResult{
Status: StepStatusSuccess,
Output: strings.Join(s.envUpdates, "\n"),
Status: StepStatusSuccess,
Output: output.String(),
Warnings: warnings,
}
}

Expand Down
Loading