From 4a06b783ed60c29c9e125c69b7b94a97cb227423 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 10 Jan 2025 11:59:05 +0000 Subject: [PATCH] Make junitXMLTestReport output deterministic by iterating over a slice instead of a map, add test --- internal/command/artifact/junit.go | 21 +++++- internal/command/artifact/junit_test.go | 92 +++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 internal/command/artifact/junit_test.go diff --git a/internal/command/artifact/junit.go b/internal/command/artifact/junit.go index b4a68eac58c7..65c925c8fabb 100644 --- a/internal/command/artifact/junit.go +++ b/internal/command/artifact/junit.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/xml" "os" + "slices" "strconv" "strings" @@ -98,7 +99,9 @@ func junitXMLTestReport(suite *moduletest.Suite) ([]byte, error) { errorsName := xml.Name{Local: "errors"} enc.EncodeToken(xml.StartElement{Name: suitesName}) - for _, file := range suite.Files { + + sortedFiles := suiteFilesAsSortedList(suite.Files) // to ensure consistent ordering in XML + for _, file := range sortedFiles { // Each test file is modelled as a "test suite". // First we'll count the number of tests and number of failures/errors @@ -224,3 +227,19 @@ func junitXMLTestReport(suite *moduletest.Suite) ([]byte, error) { enc.Close() return buf.Bytes(), nil } + +func suiteFilesAsSortedList(files map[string]*moduletest.File) []*moduletest.File { + fileNames := make([]string, len(files)) + i := 0 + for k := range files { + fileNames[i] = k + i++ + } + slices.Sort(fileNames) + + sortedFiles := make([]*moduletest.File, len(files)) + for i, name := range fileNames { + sortedFiles[i] = files[name] + } + return sortedFiles +} diff --git a/internal/command/artifact/junit_test.go b/internal/command/artifact/junit_test.go new file mode 100644 index 000000000000..ac00b406e6ab --- /dev/null +++ b/internal/command/artifact/junit_test.go @@ -0,0 +1,92 @@ +package artifact + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/moduletest" +) + +func Test_suiteFilesAsSortedList(t *testing.T) { + cases := map[string]struct { + Suite *moduletest.Suite + ExpectedNames map[int]string + }{ + "no test files": { + Suite: &moduletest.Suite{}, + }, + "3 test files ordered in map": { + Suite: &moduletest.Suite{ + Status: moduletest.Skip, + Files: map[string]*moduletest.File{ + "test_file_1.tftest.hcl": { + Name: "test_file_1.tftest.hcl", + Status: moduletest.Skip, + Runs: []*moduletest.Run{}, + }, + "test_file_2.tftest.hcl": { + Name: "test_file_2.tftest.hcl", + Status: moduletest.Skip, + Runs: []*moduletest.Run{}, + }, + "test_file_3.tftest.hcl": { + Name: "test_file_3.tftest.hcl", + Status: moduletest.Skip, + Runs: []*moduletest.Run{}, + }, + }, + }, + ExpectedNames: map[int]string{ + 0: "test_file_1.tftest.hcl", + 1: "test_file_2.tftest.hcl", + 2: "test_file_3.tftest.hcl", + }, + }, + "3 test files unordered in map": { + Suite: &moduletest.Suite{ + Status: moduletest.Skip, + Files: map[string]*moduletest.File{ + "test_file_3.tftest.hcl": { + Name: "test_file_3.tftest.hcl", + Status: moduletest.Skip, + Runs: []*moduletest.Run{}, + }, + "test_file_1.tftest.hcl": { + Name: "test_file_1.tftest.hcl", + Status: moduletest.Skip, + Runs: []*moduletest.Run{}, + }, + "test_file_2.tftest.hcl": { + Name: "test_file_2.tftest.hcl", + Status: moduletest.Skip, + Runs: []*moduletest.Run{}, + }, + }, + }, + ExpectedNames: map[int]string{ + 0: "test_file_1.tftest.hcl", + 1: "test_file_2.tftest.hcl", + 2: "test_file_3.tftest.hcl", + }, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + list := suiteFilesAsSortedList(tc.Suite.Files) + + if len(tc.ExpectedNames) != len(tc.Suite.Files) { + t.Fatalf("expected there to be %d items, got %d", len(tc.ExpectedNames), len(tc.Suite.Files)) + } + + if len(tc.ExpectedNames) == 0 { + return + } + + for k, v := range tc.ExpectedNames { + if list[k].Name != v { + t.Fatalf("expected element %d in sorted list to be named %s, got %s", k, v, list[k].Name) + } + } + }) + } +}