//go:build linux || darwin package terraform import ( "bufio" "bytes" _ "embed" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/tools/txtar" "github.com/coder/coder/v2/coderd/database" terraform_internal "github.com/coder/coder/v2/provisioner/terraform/internal" "github.com/coder/coder/v2/provisionersdk/proto" ) var ( //go:embed testdata/timings-aggregation/simple.txtar inputSimple []byte //go:embed testdata/timings-aggregation/init.txtar inputInit []byte //go:embed testdata/timings-aggregation/error.txtar inputError []byte //go:embed testdata/timings-aggregation/complete.txtar inputComplete []byte //go:embed testdata/timings-aggregation/incomplete.txtar inputIncomplete []byte //go:embed testdata/timings-aggregation/faster-than-light.txtar inputFasterThanLight []byte //go:embed testdata/timings-aggregation/multiple-resource-actions.txtar multipleResourceActions []byte ) func TestAggregation(t *testing.T) { t.Parallel() tests := []struct { name string input []byte }{ { name: "init", input: inputInit, }, { name: "simple", input: inputSimple, }, { name: "error", input: inputError, }, { name: "complete", input: inputComplete, }, { name: "incomplete", input: inputIncomplete, }, { name: "faster-than-light", input: inputFasterThanLight, }, { name: "multiple-resource-actions", input: multipleResourceActions, }, } // nolint:paralleltest // Not since go v1.22. for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() // txtar is a text-based archive format used in the stdlib for simple and elegant tests. // // We ALWAYS expect that the archive contains two or more "files": // 1. JSON logs generated by a terraform execution, one per line, *one file per stage* // N. Expected resulting timings in JSON form, one per line arc := txtar.Parse(tc.input) require.GreaterOrEqual(t, len(arc.Files), 2) t.Logf("%s: %s", t.Name(), arc.Comment) var actualTimings []*proto.Timing // The last "file" MUST contain the expected timings. expectedTimings := arc.Files[len(arc.Files)-1] // Iterate over the initial "files" and extract their timings according to their stage. for i := 0; i < len(arc.Files)-1; i++ { file := arc.Files[i] stage := database.ProvisionerJobTimingStage(file.Name) require.Truef(t, stage.Valid(), "%q is not a valid stage name; acceptable values: %v", file.Name, database.AllProvisionerJobTimingStageValues()) agg := newTimingAggregator(stage) ingestAllSpans(t, file.Data, agg) actualTimings = append(actualTimings, agg.aggregate()...) } // Ensure that the expected timings were produced. expected := terraform_internal.ParseTimingLines(t, expectedTimings.Data) terraform_internal.StableSortTimings(t, actualTimings) // To reduce flakiness. if !assert.True(t, terraform_internal.TimingsAreEqual(t, expected, actualTimings)) { t.Log("expected:") printTimings(t, expected) t.Log("actual:") printTimings(t, actualTimings) } }) } } func ingestAllSpans(t *testing.T, input []byte, aggregator *timingAggregator) { t.Helper() scanner := bufio.NewScanner(bytes.NewBuffer(input)) for scanner.Scan() { line := scanner.Bytes() log := parseTerraformLogLine(line) if log == nil { continue } ts, span, err := extractTimingSpan(log) if err != nil { // t.Logf("%s: failed span extraction on line: %q", err, line) continue } require.NotZerof(t, ts, "failed on line: %q", line) require.NotNilf(t, span, "failed on line: %q", line) aggregator.ingest(ts, span) } require.NoError(t, scanner.Err()) } func printTimings(t *testing.T, timings []*proto.Timing) { t.Helper() for _, a := range timings { terraform_internal.PrintTiming(t, a) } }