Skip to content
Merged
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
2 changes: 1 addition & 1 deletion test-results/DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
## Configuration & Environment
- Viper auto-loads `$HOME/.test-results.yaml` when present; command flags override config.
- Key env vars: `SEMAPHORE_PIPELINE_ID`, `SEMAPHORE_WORKFLOW_ID`, `SEMAPHORE_JOB_ID`, `SEMAPHORE_AGENT_MACHINE_TYPE`, etc.; parsers read them to enrich metadata.
- Flags to know: `--parser`, `--ignore-missing`, `--no-compress`, `--suite-prefix`, `--omit-output-for-passed`, `--trim-output-to`, `--name`.
- Flags to know: `--parser`, `--ignore-missing`, `--no-compress`, `--suite-prefix`, `--omit-output-for-passed`, `--trim-output-to`, `--no-trim-output`, `--name`.

## Development Workflow
- Requires Go 1.24+. Use `make test.setup` once to build the `cli` Docker image and fetch module deps.
Expand Down
15 changes: 15 additions & 0 deletions test-results/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,21 @@ The generated tests in a report will sometimes contain a prefix in the name. For
test-results publish --suite-prefix "Elixir." results.xml
```

## Trimming test output

By default, the CLI trims stdout/stderr fields to the last 1000 characters and prepends `...[truncated]...` when trimming occurs. You can change the limit or disable trimming entirely:

```bash
# Keep the last 5000 characters
test-results publish --trim-output-to 5000 results.xml

# Disable trimming
test-results publish --no-trim-output results.xml

# Also disables trimming
test-results publish --trim-output-to 0 results.xml
```

## Multiple reports from one job

If your job generates multiple reports: `integration.xml`, `unit.xml` you can use this command to merge and publish them
Expand Down
3 changes: 2 additions & 1 deletion test-results/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ func init() {
rootCmd.PersistentFlags().StringP("suite-prefix", "S", "", "prefix for each suite")
rootCmd.PersistentFlags().StringP("parser", "p", "auto", "override parser to be used")
rootCmd.PersistentFlags().Bool("no-compress", false, "skip gzip compression for the output")
rootCmd.PersistentFlags().IntP("trim-output-to", "s", 1000, "trim stdout/stderr to last N characters (max 10000), defaults to 1000")
rootCmd.PersistentFlags().IntP("trim-output-to", "s", 1000, "trim stdout/stderr to last N characters, defaults to 1000 (use 0 or --no-trim-output to disable)")
rootCmd.PersistentFlags().Bool("no-trim-output", false, "disable output trimming entirely")

// Cobra also supports local flags, which will only run
// when this action is called directly.
Expand Down
12 changes: 9 additions & 3 deletions test-results/pkg/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -466,16 +466,22 @@ func ApplyOutputTrimming(result *parser.Result, cmd *cobra.Command) {
return
}

// Check if trimming is disabled via --no-trim-output flag
noTrim, err := cmd.Flags().GetBool("no-trim-output")
if err == nil && noTrim {
return
}

trimTo := 1000
maxTrimLength := 10000

trimToFlag, err := cmd.Flags().GetInt("trim-output-to")
if err == nil {
trimTo = trimToFlag
}

if trimTo > maxTrimLength || trimTo <= 0 {
trimTo = maxTrimLength
// If trimTo is 0 or negative, disable trimming
if trimTo <= 0 {
return
}

for i := range result.TestResults {
Expand Down
163 changes: 163 additions & 0 deletions test-results/pkg/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"github.com/semaphoreci/toolbox/test-results/pkg/parser"
"github.com/spf13/cobra"

"github.com/semaphoreci/toolbox/test-results/pkg/cli"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -151,6 +152,168 @@ func TestWriteToTmpFile(t *testing.T) {
})
}

func TestApplyOutputTrimming(t *testing.T) {
longText := func(n int) string {
s := ""
for i := 0; i < n; i++ {
s += "x"
}
return s
}

createTestResult := func(text string) *parser.Result {
return &parser.Result{
TestResults: []parser.TestResults{
{
ID: "test-1",
Name: "Test",
Suites: []parser.Suite{
{
Name: "Suite1",
SystemOut: text,
SystemErr: text,
Tests: []parser.Test{
{
Name: "Test1",
SystemOut: text,
SystemErr: text,
Failure: &parser.Failure{
Message: text,
Body: text,
},
},
},
},
},
},
},
}
}

createCmd := func(trimTo int, noTrim bool) *cobra.Command {
cmd := &cobra.Command{}
cmd.Flags().Int("trim-output-to", trimTo, "")
cmd.Flags().Bool("no-trim-output", noTrim, "")
return cmd
}

t.Run("default trimming to 1000 characters", func(t *testing.T) {
result := createTestResult(longText(2000))
cmd := createCmd(1000, false)

cli.ApplyOutputTrimming(result, cmd)

suite := result.TestResults[0].Suites[0]
assert.True(t, len(suite.SystemOut) <= 1000+len("...[truncated]...\n"))
assert.True(t, len(suite.SystemErr) <= 1000+len("...[truncated]...\n"))
assert.Contains(t, suite.SystemOut, "...[truncated]...")
})

t.Run("custom trim length", func(t *testing.T) {
result := createTestResult(longText(5000))
cmd := createCmd(3000, false)

cli.ApplyOutputTrimming(result, cmd)

suite := result.TestResults[0].Suites[0]
assert.True(t, len(suite.SystemOut) <= 3000+len("...[truncated]...\n"))
assert.Contains(t, suite.SystemOut, "...[truncated]...")
})

t.Run("no trimming when --no-trim-output is set", func(t *testing.T) {
originalText := longText(5000)
result := createTestResult(originalText)
cmd := createCmd(1000, true)

cli.ApplyOutputTrimming(result, cmd)

suite := result.TestResults[0].Suites[0]
assert.Equal(t, originalText, suite.SystemOut)
assert.Equal(t, originalText, suite.SystemErr)
assert.Equal(t, originalText, suite.Tests[0].SystemOut)
assert.Equal(t, originalText, suite.Tests[0].Failure.Message)
})

t.Run("no trimming when --trim-output-to is 0", func(t *testing.T) {
originalText := longText(5000)
result := createTestResult(originalText)
cmd := createCmd(0, false)

cli.ApplyOutputTrimming(result, cmd)

suite := result.TestResults[0].Suites[0]
assert.Equal(t, originalText, suite.SystemOut)
assert.Equal(t, originalText, suite.SystemErr)
})

t.Run("no trimming when --trim-output-to is negative", func(t *testing.T) {
originalText := longText(5000)
result := createTestResult(originalText)
cmd := createCmd(-1, false)

cli.ApplyOutputTrimming(result, cmd)

suite := result.TestResults[0].Suites[0]
assert.Equal(t, originalText, suite.SystemOut)
})

t.Run("text shorter than trim limit is not modified", func(t *testing.T) {
originalText := longText(500)
result := createTestResult(originalText)
cmd := createCmd(1000, false)

cli.ApplyOutputTrimming(result, cmd)

suite := result.TestResults[0].Suites[0]
assert.Equal(t, originalText, suite.SystemOut)
assert.NotContains(t, suite.SystemOut, "...[truncated]...")
})

t.Run("nil result does not panic", func(t *testing.T) {
cmd := createCmd(1000, false)
assert.NotPanics(t, func() {
cli.ApplyOutputTrimming(nil, cmd)
})
})

t.Run("trims failure and error fields", func(t *testing.T) {
result := &parser.Result{
TestResults: []parser.TestResults{
{
ID: "test-1",
Suites: []parser.Suite{
{
Tests: []parser.Test{
{
Failure: &parser.Failure{
Message: longText(2000),
Type: longText(2000),
Body: longText(2000),
},
Error: &parser.Error{
Message: longText(2000),
Type: longText(2000),
Body: longText(2000),
},
},
},
},
},
},
},
}
cmd := createCmd(1000, false)

cli.ApplyOutputTrimming(result, cmd)

test := result.TestResults[0].Suites[0].Tests[0]
assert.Contains(t, test.Failure.Message, "...[truncated]...")
assert.Contains(t, test.Failure.Body, "...[truncated]...")
assert.Contains(t, test.Error.Message, "...[truncated]...")
assert.Contains(t, test.Error.Body, "...[truncated]...")
})
}

func TestWriteToFilePath(t *testing.T) {
tr := parser.TestResults{
ID: "1234",
Expand Down