Skip to content

Commit 96387bb

Browse files
Backport of Add more details to JUnit terraform test output to describe why a test was skipped into v1.11 (#36347)
* Add ability for TestJUnitXMLFile to access data about whether the test runner was Stopped * Add details to XML describing why a Run was skipped * Fix wording * Code consistency changes * Move all JUnit-related code down to where it's used Away from the Views section of the code where it was relevant before * Move JUnit-related error and warning diags to above where cancellable contexts are created * Fix wording of user feedback * Fix test to match updated skipped message text * Fix test * Add missing changes from a1716b8 --------- Co-authored-by: Sarah French <[email protected]>
1 parent 07c1138 commit 96387bb

File tree

7 files changed

+280
-156
lines changed

7 files changed

+280
-156
lines changed

internal/backend/local/test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ func (runner *TestSuiteRunner) Stop() {
8686
runner.Stopped = true
8787
}
8888

89+
func (runner *TestSuiteRunner) IsStopped() bool {
90+
return runner.Stopped
91+
}
92+
8993
func (runner *TestSuiteRunner) Cancel() {
9094
runner.Cancelled = true
9195
}

internal/cloud/test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ func (runner *TestSuiteRunner) Stop() {
109109
runner.Stopped = true
110110
}
111111

112+
func (runner *TestSuiteRunner) IsStopped() bool {
113+
return runner.Stopped
114+
}
115+
112116
func (runner *TestSuiteRunner) Cancel() {
113117
runner.Cancelled = true
114118
}

internal/command/junit/junit.go

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ type TestJUnitXMLFile struct {
4040

4141
// A config loader is required to access sources, which are used with diagnostics to create XML content
4242
configLoader *configload.Loader
43+
44+
// A pointer to the containing test suite runner is needed to monitor details like the command being stopped
45+
testSuiteRunner moduletest.TestSuiteRunner
4346
}
4447

4548
type JUnit interface {
@@ -55,10 +58,11 @@ var _ JUnit = (*TestJUnitXMLFile)(nil)
5558
// point of being asked to write a conclusion. Otherwise it will create the
5659
// file at that time. If creating or overwriting the file fails, a subsequent
5760
// call to method Err will return information about the problem.
58-
func NewTestJUnitXMLFile(filename string, configLoader *configload.Loader) *TestJUnitXMLFile {
61+
func NewTestJUnitXMLFile(filename string, configLoader *configload.Loader, testSuiteRunner moduletest.TestSuiteRunner) *TestJUnitXMLFile {
5962
return &TestJUnitXMLFile{
60-
filename: filename,
61-
configLoader: configLoader,
63+
filename: filename,
64+
configLoader: configLoader,
65+
testSuiteRunner: testSuiteRunner,
6266
}
6367
}
6468

@@ -69,7 +73,7 @@ func (v *TestJUnitXMLFile) Save(suite *moduletest.Suite) tfdiags.Diagnostics {
6973

7074
// Prepare XML content
7175
sources := v.configLoader.Parser().Sources()
72-
xmlSrc, err := junitXMLTestReport(suite, sources)
76+
xmlSrc, err := junitXMLTestReport(suite, v.testSuiteRunner.IsStopped(), sources)
7377
if err != nil {
7478
diags = diags.Append(&hcl.Diagnostic{
7579
Severity: hcl.DiagError,
@@ -130,7 +134,7 @@ type testCase struct {
130134
Timestamp string `xml:"timestamp,attr,omitempty"`
131135
}
132136

133-
func junitXMLTestReport(suite *moduletest.Suite, sources map[string][]byte) ([]byte, error) {
137+
func junitXMLTestReport(suite *moduletest.Suite, suiteRunnerStopped bool, sources map[string][]byte) ([]byte, error) {
134138
var buf bytes.Buffer
135139
enc := xml.NewEncoder(&buf)
136140
enc.EncodeToken(xml.ProcInst{
@@ -182,7 +186,7 @@ func junitXMLTestReport(suite *moduletest.Suite, sources map[string][]byte) ([]b
182186
},
183187
})
184188

185-
for _, run := range file.Runs {
189+
for i, run := range file.Runs {
186190
// Each run is a "test case".
187191

188192
testCase := testCase{
@@ -201,9 +205,10 @@ func junitXMLTestReport(suite *moduletest.Suite, sources map[string][]byte) ([]b
201205
}
202206
switch run.Status {
203207
case moduletest.Skip:
208+
message, body := getSkipDetails(i, file, suiteRunnerStopped)
204209
testCase.Skipped = &withMessage{
205-
// FIXME: Is there something useful we could say here about
206-
// why the test was skipped?
210+
Message: message,
211+
Body: body,
207212
}
208213
case moduletest.Fail:
209214
testCase.Failure = &withMessage{
@@ -248,6 +253,37 @@ func junitXMLTestReport(suite *moduletest.Suite, sources map[string][]byte) ([]b
248253
return buf.Bytes(), nil
249254
}
250255

256+
// getSkipDetails checks data about the test suite, file and runs to determine why a given run was skipped
257+
// Test can be skipped due to:
258+
// 1. terraform test recieving an interrupt from users; all unstarted tests will be skipped
259+
// 2. A previous run in a file has failed, causing subsequent run blocks to be skipped
260+
func getSkipDetails(runIndex int, file *moduletest.File, suiteStopped bool) (string, string) {
261+
if suiteStopped {
262+
// Test suite experienced an interrupt
263+
// This block only handles graceful Stop interrupts, as Cancel interrupts will prevent a JUnit file being produced at all
264+
message := "Testcase skipped due to an interrupt"
265+
body := "Terraform received an interrupt and stopped gracefully. This caused all remaining testcases to be skipped"
266+
267+
return message, body
268+
}
269+
270+
if file.Status == moduletest.Error {
271+
// Overall test file marked as errored in the context of a skipped test means tests have been skipped after
272+
// a previous test/run blocks has errored out
273+
for i := runIndex; i >= 0; i-- {
274+
if file.Runs[i].Status == moduletest.Error {
275+
// Skipped due to error in previous run within the file
276+
message := "Testcase skipped due to a previous testcase error"
277+
body := fmt.Sprintf("Previous testcase %q ended in error, which caused the remaining tests in the file to be skipped", file.Runs[i].Name)
278+
return message, body
279+
}
280+
}
281+
}
282+
283+
// Unhandled case: This results in <skipped></skipped> with no attributes or body
284+
return "", ""
285+
}
286+
251287
func suiteFilesAsSortedList(files map[string]*moduletest.File) []*moduletest.File {
252288
fileNames := make([]string, len(files))
253289
i := 0
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
package junit
4+
5+
import (
6+
"bytes"
7+
"fmt"
8+
"os"
9+
"testing"
10+
11+
"github.com/hashicorp/terraform/internal/moduletest"
12+
)
13+
14+
func Test_TestJUnitXMLFile_save(t *testing.T) {
15+
16+
cases := map[string]struct {
17+
filename string
18+
expectError bool
19+
}{
20+
"can save output to the specified filename": {
21+
filename: func() string {
22+
td := t.TempDir()
23+
return fmt.Sprintf("%s/output.xml", td)
24+
}(),
25+
},
26+
"returns an error when given a filename that isn't absolute or relative": {
27+
filename: "~/output.xml",
28+
expectError: true,
29+
},
30+
}
31+
32+
for tn, tc := range cases {
33+
t.Run(tn, func(t *testing.T) {
34+
j := TestJUnitXMLFile{
35+
filename: tc.filename,
36+
}
37+
38+
xml := []byte(`<?xml version="1.0" encoding="UTF-8"?><testsuites>
39+
<testsuite name="example_1.tftest.hcl" tests="1" skipped="0" failures="0" errors="0">
40+
<testcase name="true_is_true" classname="example_1.tftest.hcl" time="0.005381209"></testcase>
41+
</testsuite>
42+
</testsuites>`)
43+
44+
diags := j.save(xml)
45+
46+
if diags.HasErrors() {
47+
if !tc.expectError {
48+
t.Fatalf("got unexpected error: %s", diags.Err())
49+
}
50+
// return early if testing error case
51+
return
52+
}
53+
54+
if !diags.HasErrors() && tc.expectError {
55+
t.Fatalf("expected an error but got none")
56+
}
57+
58+
fileContent, err := os.ReadFile(tc.filename)
59+
if err != nil {
60+
t.Fatalf("unexpected error opening file")
61+
}
62+
63+
if !bytes.Equal(fileContent, xml) {
64+
t.Fatalf("wanted XML:\n%s\n got XML:\n%s\n", string(xml), string(fileContent))
65+
}
66+
})
67+
}
68+
}
69+
70+
func Test_suiteFilesAsSortedList(t *testing.T) {
71+
cases := map[string]struct {
72+
Suite *moduletest.Suite
73+
ExpectedNames map[int]string
74+
}{
75+
"no test files": {
76+
Suite: &moduletest.Suite{},
77+
},
78+
"3 test files ordered in map": {
79+
Suite: &moduletest.Suite{
80+
Status: moduletest.Skip,
81+
Files: map[string]*moduletest.File{
82+
"test_file_1.tftest.hcl": {
83+
Name: "test_file_1.tftest.hcl",
84+
Status: moduletest.Skip,
85+
Runs: []*moduletest.Run{},
86+
},
87+
"test_file_2.tftest.hcl": {
88+
Name: "test_file_2.tftest.hcl",
89+
Status: moduletest.Skip,
90+
Runs: []*moduletest.Run{},
91+
},
92+
"test_file_3.tftest.hcl": {
93+
Name: "test_file_3.tftest.hcl",
94+
Status: moduletest.Skip,
95+
Runs: []*moduletest.Run{},
96+
},
97+
},
98+
},
99+
ExpectedNames: map[int]string{
100+
0: "test_file_1.tftest.hcl",
101+
1: "test_file_2.tftest.hcl",
102+
2: "test_file_3.tftest.hcl",
103+
},
104+
},
105+
"3 test files unordered in map": {
106+
Suite: &moduletest.Suite{
107+
Status: moduletest.Skip,
108+
Files: map[string]*moduletest.File{
109+
"test_file_3.tftest.hcl": {
110+
Name: "test_file_3.tftest.hcl",
111+
Status: moduletest.Skip,
112+
Runs: []*moduletest.Run{},
113+
},
114+
"test_file_1.tftest.hcl": {
115+
Name: "test_file_1.tftest.hcl",
116+
Status: moduletest.Skip,
117+
Runs: []*moduletest.Run{},
118+
},
119+
"test_file_2.tftest.hcl": {
120+
Name: "test_file_2.tftest.hcl",
121+
Status: moduletest.Skip,
122+
Runs: []*moduletest.Run{},
123+
},
124+
},
125+
},
126+
ExpectedNames: map[int]string{
127+
0: "test_file_1.tftest.hcl",
128+
1: "test_file_2.tftest.hcl",
129+
2: "test_file_3.tftest.hcl",
130+
},
131+
},
132+
}
133+
134+
for tn, tc := range cases {
135+
t.Run(tn, func(t *testing.T) {
136+
list := suiteFilesAsSortedList(tc.Suite.Files)
137+
138+
if len(tc.ExpectedNames) != len(tc.Suite.Files) {
139+
t.Fatalf("expected there to be %d items, got %d", len(tc.ExpectedNames), len(tc.Suite.Files))
140+
}
141+
142+
if len(tc.ExpectedNames) == 0 {
143+
return
144+
}
145+
146+
for k, v := range tc.ExpectedNames {
147+
if list[k].Name != v {
148+
t.Fatalf("expected element %d in sorted list to be named %s, got %s", k, v, list[k].Name)
149+
}
150+
}
151+
})
152+
}
153+
}

0 commit comments

Comments
 (0)