blob: 36743489080d61d5aa6b18eeba4efcee4dfea2c7 [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package resultdb
import (
"os"
"path/filepath"
"slices"
"strings"
"testing"
"time"
"unicode/utf8"
"github.com/google/go-cmp/cmp"
resultpb "go.chromium.org/luci/resultdb/proto/v1"
sinkpb "go.chromium.org/luci/resultdb/sink/proto/v1"
"go.fuchsia.dev/fuchsia/tools/testing/runtests"
)
func boolPtr(b bool) *bool { return &b }
func intPtr(i int) *int { return &i }
// testCaseResults returns the test case results of detail, from which
// synthesizeFixtureResult derives the *fixture's fine_name.
func testCaseResults(t *testing.T, detail *runtests.TestDetails) []*sinkpb.TestResult {
t.Helper()
results, _, _ := testCaseToResultSink(detail.Cases, nil, detail, t.TempDir())
return results
}
func TestClassifyFixture(t *testing.T) {
tests := []struct {
name string
detail *runtests.TestDetails
wantStatus resultpb.TestResult_Status
}{
{
name: "lifecycle fields unset",
detail: &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/foo#meta/foo.cm",
Status: runtests.TestFailure,
},
wantStatus: resultpb.TestResult_STATUS_UNSPECIFIED,
},
{
name: "Scenario 1: clean pass or intentional skip",
detail: &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/bt-host-smoke-test#meta/bt-host-smoke-test.cm",
Status: runtests.TestSuccess,
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(true),
TeardownSucceeded: boolPtr(true),
},
},
wantStatus: resultpb.TestResult_PASSED,
},
{
name: "Scenario 2: case assertion failure with clean fixture",
detail: &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/bt-host-smoke-test#meta/bt-host-smoke-test.cm",
Status: runtests.TestFailure,
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(true),
TeardownSucceeded: boolPtr(true),
},
},
wantStatus: resultpb.TestResult_PASSED,
},
{
name: "setup succeeded without a teardown outcome",
detail: &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/bt-host-smoke-test#meta/bt-host-smoke-test.cm",
Status: runtests.TestSuccess,
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(true),
},
},
wantStatus: resultpb.TestResult_STATUS_UNSPECIFIED,
},
{
name: "setup and teardown succeeded with infra failure suite status is ignored",
detail: &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/bt-host-smoke-test#meta/bt-host-smoke-test.cm",
Status: runtests.TestInfraFailure,
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(true),
TeardownSucceeded: boolPtr(true),
},
},
wantStatus: resultpb.TestResult_STATUS_UNSPECIFIED,
},
{
name: "Scenario 3: setup failed (intrinsic fixture bug)",
detail: &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/bt-host-smoke-test#meta/bt-host-smoke-test.cm",
Status: runtests.TestFailure,
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(false),
},
},
wantStatus: resultpb.TestResult_FAILED,
},
{
name: "Scenario 4: mid-suite abort",
detail: &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/bt-host-smoke-test#meta/bt-host-smoke-test.cm",
Status: runtests.TestAborted,
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(true),
},
},
wantStatus: resultpb.TestResult_STATUS_UNSPECIFIED,
},
{
name: "setup failed with non-failure suite status is ignored",
detail: &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/bt-host-smoke-test#meta/bt-host-smoke-test.cm",
Status: runtests.TestSuccess,
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(false),
},
},
wantStatus: resultpb.TestResult_STATUS_UNSPECIFIED,
},
{
name: "Scenario 5: teardown failed after cases passed",
detail: &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/bt-host-smoke-test#meta/bt-host-smoke-test.cm",
Status: runtests.TestFailure,
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(true),
TeardownSucceeded: boolPtr(false),
},
},
wantStatus: resultpb.TestResult_FAILED,
},
{
name: "Scenario 6: unaffected target skipped",
detail: &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/bt-host-smoke-test#meta/bt-host-smoke-test.cm",
Status: runtests.TestSkipped,
Affected: false,
},
wantStatus: resultpb.TestResult_STATUS_UNSPECIFIED,
},
{
name: "Scenario 7: contaminated environment (noisy neighbor) setup infra fail",
detail: &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/bt-host-smoke-test#meta/bt-host-smoke-test.cm",
Status: runtests.TestInfraFailure,
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(false),
},
},
wantStatus: resultpb.TestResult_EXECUTION_ERRORED,
},
{
name: "teardown infra fail",
detail: &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/bt-host-smoke-test#meta/bt-host-smoke-test.cm",
Status: runtests.TestInfraFailure,
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(true),
TeardownSucceeded: boolPtr(false),
},
},
wantStatus: resultpb.TestResult_EXECUTION_ERRORED,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
status, reason := classifyFixture(tc.detail)
if status != tc.wantStatus {
t.Errorf("classifyFixture() status = %v, want %v (reason %q)", status, tc.wantStatus, reason)
}
if wantReason := status == resultpb.TestResult_FAILED; (reason != "") != wantReason {
t.Errorf("classifyFixture() reason = %q, want non-empty: %v", reason, wantReason)
}
})
}
}
func TestSynthesizeFixtureResult_NoFixture(t *testing.T) {
for name, detail := range map[string]*runtests.TestDetails{
"tefmocheck": {
Name: "testing_failure_mode/serial_log/ panic",
Status: runtests.TestFailure,
TestResult: runtests.TestResult{SetupSucceeded: boolPtr(false)},
IsTestingFailureMode: true,
},
"lifecycle fields unset": {
Name: "fuchsia-pkg://fuchsia.com/foo#meta/foo.cm",
Status: runtests.TestSuccess,
TestResult: runtests.TestResult{
Cases: []runtests.TestCaseResult{{SuiteName: "Suite", CaseName: "Case", Format: "GoogleTest"}},
},
},
} {
if got, skipped := synthesizeFixtureResult(testCaseResults(t, detail), detail); got != nil || skipped != "" {
t.Errorf("%s: synthesizeFixtureResult() = %v, %q, want nil, \"\"", name, got, skipped)
}
}
}
func TestSynthesizeFixtureResult_FineNameAndTestID(t *testing.T) {
const name = "fuchsia-pkg://fuchsia.com/bt-host-smoke-test#meta/bt-host-smoke-test.cm"
tests := []struct {
desc string
detail runtests.TestDetails
wantFineName string
}{
{
desc: "gtest with a single suite uses the suite",
detail: runtests.TestDetails{
TestResult: runtests.TestResult{
Cases: []runtests.TestCaseResult{
{SuiteName: "BtHostTestSuite", CaseName: "TestInitializeSuccess", Format: "GoogleTest"},
{SuiteName: "BtHostTestSuite", CaseName: "TestConnectPeer", Format: "GoogleTest"},
},
},
},
wantFineName: "BtHostTestSuite",
},
{
desc: "gtest with multiple suites reports a single fixture",
detail: runtests.TestDetails{
TestResult: runtests.TestResult{
Cases: []runtests.TestCaseResult{
{SuiteName: "BtHostTestSuite", CaseName: "TestInitializeSuccess", Format: "GoogleTest"},
{SuiteName: "BtHostTestSuite", CaseName: "TestConnectPeer", Format: "GoogleTest"},
{SuiteName: "BtGapTestSuite", CaseName: "TestDiscovery", Format: "GoogleTest"},
},
},
},
wantFineName: "test",
},
{
desc: "gtest without suite names falls back to the scheme default",
detail: runtests.TestDetails{
TestResult: runtests.TestResult{
Cases: []runtests.TestCaseResult{{CaseName: "TestFoo", Format: "GoogleTest"}},
},
},
wantFineName: "test",
},
{
desc: "mobly with a single class uses the class",
detail: runtests.TestDetails{
TestResult: runtests.TestResult{
Cases: []runtests.TestCaseResult{
{SuiteName: "TiltToWakeTest", CaseName: "test_tilt_to_wake", Format: "Mobly"},
},
},
},
wantFineName: "TiltToWakeTest",
},
{
desc: "mobly with a synthetic test parser error falls back to the scheme default",
detail: runtests.TestDetails{
TestResult: runtests.TestResult{
Cases: []runtests.TestCaseResult{
{SuiteName: "TiltToWakeTest", CaseName: "test_tilt_to_wake", Format: "Mobly"},
{SuiteName: "Synthetic", CaseName: "Synthetic", Format: "Mobly"},
},
},
},
wantFineName: "test",
},
{
desc: "rust with a single module uses the module",
detail: runtests.TestDetails{
TestResult: runtests.TestResult{
Cases: []runtests.TestCaseResult{{SuiteName: "tests", CaseName: "test_foo", Format: "Rust"}},
},
},
wantFineName: "tests",
},
{
desc: "rust at the crate root uses the crate root module",
detail: runtests.TestDetails{
TestResult: runtests.TestResult{
Cases: []runtests.TestCaseResult{{CaseName: "test_foo", Format: "Rust"}},
},
},
wantFineName: "crate",
},
{
desc: "rust with multiple modules uses the crate root module",
detail: runtests.TestDetails{
TestResult: runtests.TestResult{
Cases: []runtests.TestCaseResult{
{SuiteName: "tests", CaseName: "test_foo", Format: "Rust"},
{SuiteName: "other_tests", CaseName: "test_bar", Format: "Rust"},
},
},
},
wantFineName: "crate",
},
{
desc: "go uses the Go package",
detail: runtests.TestDetails{
SourceLabel: "//tools/testing/resultdb:resultdb_lib_tests(//build/toolchain:host_x64)",
TestResult: runtests.TestResult{
Cases: []runtests.TestCaseResult{{CaseName: "TestFoo", Format: "Go"}},
},
},
wantFineName: "tools/testing/resultdb",
},
{
desc: "flat uses an empty fine name",
detail: runtests.TestDetails{
TestResult: runtests.TestResult{
Cases: []runtests.TestCaseResult{{SuiteName: "Suite", CaseName: "test_foo", Format: "FTF"}},
},
},
wantFineName: "",
},
{
desc: "single with test cases uses an empty fine name",
detail: runtests.TestDetails{
TestResult: runtests.TestResult{
Cases: []runtests.TestCaseResult{{CaseName: "test_foo", Format: "single"}},
},
},
wantFineName: "",
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
detail := tc.detail
detail.Name = name
detail.Status = runtests.TestFailure
detail.StartTime = time.Unix(1700000000, 0)
detail.SetupSucceeded = boolPtr(false)
// Test cases need a status to be reported, and the *fixture
// shares the fine_name of the reported test cases.
detail.Cases = slices.Clone(detail.Cases)
for i := range detail.Cases {
detail.Cases[i].Status = runtests.TestSkipped
}
r, skipped := synthesizeFixtureResult(testCaseResults(t, &detail), &detail)
if r == nil || skipped != "" {
t.Fatalf("synthesizeFixtureResult() = %v, %q, want a fixture", r, skipped)
}
wantID := name + "/" + tc.wantFineName + ":" + FixtureCaseName
if r.TestId != wantID {
t.Errorf("TestId = %q, want %q", r.TestId, wantID)
}
id := r.TestIdStructured
if id.GetCoarseName() != "" || id.GetFineName() != tc.wantFineName {
t.Errorf("CoarseName, FineName = %q, %q, want \"\", %q", id.GetCoarseName(), id.GetFineName(), tc.wantFineName)
}
if got := id.GetCaseNameComponents(); len(got) != 1 || got[0] != FixtureCaseName {
t.Errorf("CaseNameComponents = %q, want [%q]", got, FixtureCaseName)
}
})
}
}
func TestSynthesizeFixtureResult_FineNameMatchesTestCases(t *testing.T) {
detail := &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/foo#meta/foo.cm",
Status: runtests.TestFailure,
StartTime: time.Unix(1700000000, 0),
TestResult: runtests.TestResult{
TeardownSucceeded: boolPtr(false),
Cases: []runtests.TestCaseResult{
// A leading space is reserved by ResultDB, so the fine_name is
// sanitized.
{SuiteName: " Suite", CaseName: "Case", Status: runtests.TestSuccess, Format: "GoogleTest"},
},
},
}
f, _ := synthesizeFixtureResult(testCaseResults(t, detail), detail)
if f == nil {
t.Fatalf("synthesizeFixtureResult() = nil, want a fixture")
}
caseResults, _, _ := testCaseToResultSink(detail.Cases, nil, detail, t.TempDir())
if len(caseResults) != 1 {
t.Fatalf("got %d case results, want 1", len(caseResults))
}
const wantFineName = "[ Suite]"
if got := caseResults[0].TestIdStructured.GetFineName(); got != wantFineName {
t.Errorf("case FineName = %q, want %q", got, wantFineName)
}
if got := f.TestIdStructured.GetFineName(); got != wantFineName {
t.Errorf("fixture FineName = %q, want %q", got, wantFineName)
}
}
func TestSynthesizeFixtureResult_TestIDTooLong(t *testing.T) {
detail := &runtests.TestDetails{
Name: strings.Repeat("a", MaxTestIDLength),
Status: runtests.TestFailure,
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(false),
Cases: []runtests.TestCaseResult{{SuiteName: "Suite", CaseName: "Case", Format: "GoogleTest"}},
},
}
got, skipped := synthesizeFixtureResult(testCaseResults(t, detail), detail)
if got != nil {
t.Errorf("synthesizeFixtureResult() = %v, want nil", got)
}
// The test case's test ID is also too long, so there is no test case result
// to share a fine_name with, and the *fixture falls back to the scheme
// default.
if want := detail.Name + "/test:" + FixtureCaseName; skipped != want {
t.Errorf("synthesizeFixtureResult() skipped = %q, want %q", skipped, want)
}
}
func TestSynthesizeFixtureResult_Scenarios(t *testing.T) {
t.Run("Scenarios 1 and 2: clean setup and teardown reports PASSED while cases keep their status", func(t *testing.T) {
for _, tc := range []struct {
status runtests.TestStatus
caseStatus runtests.TestStatus
wantCase resultpb.TestResult_Status
}{
{status: runtests.TestSuccess, caseStatus: runtests.TestSuccess, wantCase: resultpb.TestResult_PASSED},
{status: runtests.TestFailure, caseStatus: runtests.TestFailure, wantCase: resultpb.TestResult_FAILED},
} {
detail := &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/bt-host-smoke-test#meta/bt-host-smoke-test.cm",
SourceLabel: "//src/connectivity/bluetooth/tests/bt_host:bt_host_smoke_test",
Status: tc.status,
StartTime: time.Unix(1700000000, 0),
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(true),
TeardownSucceeded: boolPtr(true),
ExitCode: intPtr(0),
FailureReason: runtests.FailureReasonFromMessage("BtHostTestSuite.TestConnectPeer failed"),
Cases: []runtests.TestCaseResult{
{
DisplayName: "BtHostTestSuite.TestConnectPeer",
SuiteName: "BtHostTestSuite",
CaseName: "TestConnectPeer",
Status: tc.caseStatus,
Format: "GoogleTest",
},
},
},
}
f, _ := synthesizeFixtureResult(testCaseResults(t, detail), detail)
if f == nil {
t.Fatalf("%s: synthesizeFixtureResult() = nil, want a fixture", tc.status)
}
if want := detail.Name + "/BtHostTestSuite:" + FixtureCaseName; f.TestId != want {
t.Errorf("%s: fixture TestId = %q, want %q", tc.status, f.TestId, want)
}
if f.StatusV2 != resultpb.TestResult_PASSED {
t.Errorf("%s: fixture StatusV2 = %v, want PASSED", tc.status, f.StatusV2)
}
if f.FailureReason != nil {
t.Errorf("%s: fixture FailureReason = %v, want nil for PASSED", tc.status, f.FailureReason)
}
if f.SummaryHtml != "" {
t.Errorf("%s: fixture SummaryHtml = %q, want empty for PASSED", tc.status, f.SummaryHtml)
}
caseResults, _, _ := testCaseToResultSink(detail.Cases, nil, detail, t.TempDir())
if len(caseResults) != 1 {
t.Fatalf("%s: got %d case results, want 1", tc.status, len(caseResults))
}
if got := caseResults[0].StatusV2; got != tc.wantCase {
t.Errorf("%s: case StatusV2 = %v, want %v", tc.status, got, tc.wantCase)
}
}
})
t.Run("Scenario 4: setup failure reports FAILED with CRASH kind and leaves case statuses unchanged", func(t *testing.T) {
outputRoot := t.TempDir()
artifactRel := "case_log.txt"
if err := os.WriteFile(filepath.Join(outputRoot, artifactRel), []byte("log"), 0o600); err != nil {
t.Fatal(err)
}
detail := &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/bt-host-smoke-test#meta/bt-host-smoke-test.cm",
SourceLabel: "//src/connectivity/bluetooth/tests/bt_host:bt_host_smoke_test",
Status: runtests.TestFailure,
StartTime: time.Unix(1700000000, 0),
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(false),
ExitCode: intPtr(1),
FailureReason: runtests.FailureReasonFromMessage("SetUpTestSuite failed: Unable to bind Bluetooth host realm channel"),
Cases: []runtests.TestCaseResult{
{
DisplayName: "BtHostTestSuite.TestInitializeSuccess",
SuiteName: "BtHostTestSuite",
CaseName: "TestInitializeSuccess",
Status: runtests.TestSkipped,
Format: "GoogleTest",
OutputFiles: []string{artifactRel},
},
{
DisplayName: "BtHostTestSuite.TestConnectPeer",
SuiteName: "BtHostTestSuite",
CaseName: "TestConnectPeer",
Status: runtests.TestSkipped,
Format: "GoogleTest",
OutputFiles: []string{artifactRel},
},
},
},
}
f, _ := synthesizeFixtureResult(testCaseResults(t, detail), detail)
if f == nil {
t.Fatalf("synthesizeFixtureResult() = nil, want a fixture")
}
if f.StatusV2 != resultpb.TestResult_FAILED {
t.Errorf("fixture StatusV2 = %v, want FAILED", f.StatusV2)
}
if f.FailureReason.GetKind() != resultpb.FailureReason_CRASH {
t.Errorf("fixture FailureReason.Kind = %v, want CRASH", f.FailureReason.GetKind())
}
errs := f.FailureReason.GetErrors()
if len(errs) != 1 || errs[0].GetMessage() != "SetUpTestSuite failed: Unable to bind Bluetooth host realm channel" {
t.Errorf("fixture FailureReason.Errors = %v, want exact setup failure message", errs)
}
// Test cases keep the status reported by the test runner. An
// intentionally skipped test case stays SKIPPED.
caseResults, _, _ := testCaseToResultSink(detail.Cases, nil, detail, outputRoot)
if len(caseResults) != 2 {
t.Fatalf("got %d case results, want 2", len(caseResults))
}
for _, c := range caseResults {
if c.StatusV2 != resultpb.TestResult_SKIPPED {
t.Errorf("case %q StatusV2 = %v, want SKIPPED", c.TestId, c.StatusV2)
}
if c.SkippedReason.GetKind() != resultpb.SkippedReason_DISABLED_AT_DECLARATION {
t.Errorf("case %q SkippedReason.Kind = %v, want DISABLED_AT_DECLARATION", c.TestId, c.SkippedReason.GetKind())
}
if len(c.Artifacts) != 1 {
t.Errorf("case %q has %d artifacts, want 1", c.TestId, len(c.Artifacts))
}
}
})
t.Run("Scenario 6: teardown failure reports FAILED with ORDINARY kind while cases stay PASSED", func(t *testing.T) {
detail := &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/bt-host-smoke-test#meta/bt-host-smoke-test.cm",
SourceLabel: "//src/connectivity/bluetooth/tests/bt_host:bt_host_smoke_test",
Status: runtests.TestFailure,
StartTime: time.Unix(1700000000, 0),
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(true),
TeardownSucceeded: boolPtr(false),
FailureReason: runtests.FailureReasonFromMessage("TearDownTestSuite failed: Lingering child component instance detected"),
Cases: []runtests.TestCaseResult{
{
DisplayName: "BtHostTestSuite.TestInitializeSuccess",
SuiteName: "BtHostTestSuite",
CaseName: "TestInitializeSuccess",
Status: runtests.TestSuccess,
Format: "GoogleTest",
},
{
DisplayName: "BtHostTestSuite.TestConnectPeer",
SuiteName: "BtHostTestSuite",
CaseName: "TestConnectPeer",
Status: runtests.TestSuccess,
Format: "GoogleTest",
},
},
},
}
f, _ := synthesizeFixtureResult(testCaseResults(t, detail), detail)
if f == nil {
t.Fatalf("synthesizeFixtureResult() = nil, want a fixture")
}
if f.StatusV2 != resultpb.TestResult_FAILED {
t.Errorf("fixture StatusV2 = %v, want FAILED", f.StatusV2)
}
if f.FailureReason.GetKind() != resultpb.FailureReason_ORDINARY {
t.Errorf("fixture FailureReason.Kind = %v, want ORDINARY", f.FailureReason.GetKind())
}
errs := f.FailureReason.GetErrors()
if len(errs) != 1 || errs[0].GetMessage() != "TearDownTestSuite failed: Lingering child component instance detected" {
t.Errorf("fixture FailureReason.Errors = %v, want exact teardown failure message", errs)
}
caseResults, _, _ := testCaseToResultSink(detail.Cases, nil, detail, t.TempDir())
if len(caseResults) != 2 {
t.Fatalf("got %d case results, want 2", len(caseResults))
}
for _, c := range caseResults {
if c.StatusV2 != resultpb.TestResult_PASSED {
t.Errorf("case %q StatusV2 = %v, want PASSED", c.TestId, c.StatusV2)
}
}
})
t.Run("Scenario 8: contaminated environment reports *fixture as EXECUTION_ERRORED", func(t *testing.T) {
detail := &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/bt-host-smoke-test#meta/bt-host-smoke-test.cm",
SourceLabel: "//src/connectivity/bluetooth/tests/bt_host:bt_host_smoke_test",
Status: runtests.TestInfraFailure,
StartTime: time.Unix(1700000000, 0),
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(false),
ExitCode: intPtr(1),
FailureReason: runtests.FailureReasonFromMessage("SetUpTestSuite failed: Unable to connect to component realm: PEER_CLOSED"),
Cases: []runtests.TestCaseResult{
{
DisplayName: "BtHostTestSuite.TestInitializeSuccess",
SuiteName: "BtHostTestSuite",
CaseName: "TestInitializeSuccess",
Status: runtests.TestSkipped,
Format: "GoogleTest",
},
},
},
}
f, _ := synthesizeFixtureResult(testCaseResults(t, detail), detail)
if f == nil {
t.Fatalf("synthesizeFixtureResult() = nil, want a fixture")
}
if f.StatusV2 != resultpb.TestResult_EXECUTION_ERRORED {
t.Errorf("fixture StatusV2 = %v, want EXECUTION_ERRORED", f.StatusV2)
}
if f.FailureReason != nil {
t.Errorf("fixture FailureReason = %v, want nil for EXECUTION_ERRORED", f.FailureReason)
}
if !strings.Contains(f.SummaryHtml, "PEER_CLOSED") {
t.Errorf("fixture SummaryHtml = %q, want it to contain the setup failure message", f.SummaryHtml)
}
if !strings.HasPrefix(f.SummaryHtml, "<p>Exit code: 1</p>") {
t.Errorf("fixture SummaryHtml = %q, want it to start with the exit code", f.SummaryHtml)
}
})
}
func TestFixtureSummaryHTML_EscapesAndTruncates(t *testing.T) {
detail := &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/foo#meta/foo.cm",
Status: runtests.TestInfraFailure,
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(false),
ExitCode: intPtr(-1),
},
}
detail.FailureReason = &runtests.FailureReason{}
for i := 0; i < 20; i++ {
detail.FailureReason.Errors = append(detail.FailureReason.Errors, &runtests.FailureReasonError{
Message: strings.Repeat("<&>", 200),
})
}
got := fixtureSummaryHTML(detail)
if len(got) > MaxSummaryHTMLLength {
t.Errorf("len(summary) = %d, want <= %d", len(got), MaxSummaryHTMLLength)
}
if !strings.HasPrefix(got, "<p>Exit code: -1</p><pre>") {
t.Errorf("summary does not start with the exit code followed by an error message: %q", got[:40])
}
if strings.Contains(got, "<&>") {
t.Errorf("summary contains unescaped text")
}
if strings.Count(got, "<pre>") != strings.Count(got, "</pre>") {
t.Errorf("summary has an element cut in half")
}
if strings.Contains(got, "<pre>...</pre>") {
t.Errorf("summary contains an error message of which nothing fits")
}
// Verify messages longer than MaxFailureReasonLength (1024 bytes) are not
// prematurely truncated when they fit within MaxSummaryHTMLLength (4096 bytes).
longTrace := strings.Repeat("x", 2500)
longDetail := &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/foo#meta/foo.cm",
Status: runtests.TestInfraFailure,
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(false),
FailureReason: runtests.FailureReasonFromMessage(longTrace),
},
}
gotLong := fixtureSummaryHTML(longDetail)
if want := "<pre>" + longTrace + "</pre>"; gotLong != want {
t.Errorf("expected 2500-byte message to be preserved in full as the whole SummaryHtml, got len=%d", len(gotLong))
}
// Without an exit code or error messages, the summary is empty.
emptyDetail := &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/foo#meta/foo.cm",
Status: runtests.TestInfraFailure,
TestResult: runtests.TestResult{SetupSucceeded: boolPtr(false)},
}
if got := fixtureSummaryHTML(emptyDetail); got != "" {
t.Errorf("fixtureSummaryHTML() = %q, want \"\"", got)
}
}
func TestHasFixture(t *testing.T) {
results := []*sinkpb.TestResult{
{TestIdStructured: &sinkpb.TestIdentifier{FineName: "Suite", CaseNameComponents: []string{"Case"}}},
{TestIdStructured: &sinkpb.TestIdentifier{FineName: "", CaseNameComponents: []string{FixtureCaseName}}},
// Top-level results of tests with cases have no structured ID.
{TestId: "fuchsia-pkg://fuchsia.com/foo#meta/foo.cm"},
}
for _, tc := range []struct {
fineName string
want bool
}{
{fineName: "", want: true},
{fineName: "test", want: false},
{fineName: "Suite", want: false},
} {
if got := hasFixture(results, tc.fineName); got != tc.want {
t.Errorf("hasFixture(%q) = %v, want %v", tc.fineName, got, tc.want)
}
}
}
func TestTestToResultSink_FixtureNotDuplicated(t *testing.T) {
for _, tc := range []struct {
desc string
detail *runtests.TestDetails
wantTestID string
}{
{
desc: "top-level result of a test without test cases is the fixture",
detail: &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/setup-fail#meta/setup-fail.cm",
Status: runtests.TestFailure,
StartTime: time.Unix(1700000000, 0),
TestResult: runtests.TestResult{SetupSucceeded: boolPtr(false)},
},
wantTestID: "fuchsia-pkg://fuchsia.com/setup-fail#meta/setup-fail.cm",
},
{
desc: "top-level result of a passing test without test cases is the fixture",
detail: &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/pass#meta/pass.cm",
Status: runtests.TestSuccess,
StartTime: time.Unix(1700000000, 0),
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(true),
TeardownSucceeded: boolPtr(true),
},
},
wantTestID: "fuchsia-pkg://fuchsia.com/pass#meta/pass.cm",
},
{
desc: "fixture is synthesized for a test with test cases",
detail: &runtests.TestDetails{
Name: "fuchsia-pkg://fuchsia.com/setup-fail#meta/setup-fail.cm",
Status: runtests.TestFailure,
StartTime: time.Unix(1700000000, 0),
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(false),
Cases: []runtests.TestCaseResult{
{SuiteName: "Suite", CaseName: "Case", Status: runtests.TestSkipped, Format: "GoogleTest"},
},
},
},
wantTestID: "fuchsia-pkg://fuchsia.com/setup-fail#meta/setup-fail.cm/Suite:" + FixtureCaseName,
},
} {
t.Run(tc.desc, func(t *testing.T) {
results, _, _ := testToResultSink(tc.detail, nil, t.TempDir())
var fixtureIDs []string
for _, r := range results {
if slices.Equal(r.GetTestIdStructured().GetCaseNameComponents(), []string{FixtureCaseName}) {
fixtureIDs = append(fixtureIDs, r.TestId)
}
}
if diff := cmp.Diff([]string{tc.wantTestID}, fixtureIDs); diff != "" {
t.Errorf("*fixture test IDs mismatch (-want +got):\n%s", diff)
}
})
}
}
func TestTruncateEscapedHTML(t *testing.T) {
for _, tc := range []struct {
str string
max int
want string
}{
{str: "abc", max: 10, want: "abc"},
// Room for "..." is always reserved.
{str: "ab", max: 3, want: "..."},
{str: "abcdef", max: 3, want: "..."},
{str: "abcdefgh", max: 6, want: "abc..."},
// Truncates str rather than the escaped string, so entities are kept
// whole.
{str: "<ab", max: 8, want: "&lt;a..."},
{str: "<b>", max: 9, want: "&lt;b..."},
{str: "<b>", max: 8, want: "&lt;b..."},
{str: "<<<<", max: 9, want: "&lt;..."},
{str: "<<<<", max: 6, want: "..."},
// str is truncated at rune boundaries, never mid-character.
{str: "日本語", max: 8, want: "日..."},
{str: "日本語", max: 9, want: "日本..."},
{str: "日本語", max: 5, want: "..."},
// Mixed multibyte and escaped characters.
{str: "日<本", max: 10, want: "日&lt;..."},
// Long input is clipped up front.
{str: strings.Repeat("日", 100000), max: 12, want: "日日日..."},
} {
got := truncateEscapedHTML(tc.str, tc.max)
if got != tc.want {
t.Errorf("truncateEscapedHTML(%q, %d) = %q, want %q", tc.str, tc.max, got, tc.want)
}
if len(got) > tc.max {
t.Errorf("truncateEscapedHTML(%q, %d) returned %d bytes, want <= %d", tc.str, tc.max, len(got), tc.max)
}
if !utf8.ValidString(got) {
t.Errorf("truncateEscapedHTML(%q, %d) = %q, which is not valid UTF-8", tc.str, tc.max, got)
}
}
}
func TestSummaryToResultSink_FixtureSynthesis(t *testing.T) {
longName := strings.Repeat("a", MaxTestIDLength)
summary := &runtests.TestSummary{Tests: []runtests.TestDetails{
// Scenario 1: Clean pass -> 1 fixture (PASSED)
{
Name: "fuchsia-pkg://fuchsia.com/pass#meta/pass.cm",
Status: runtests.TestSuccess,
StartTime: time.Unix(1700000000, 0),
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(true),
TeardownSucceeded: boolPtr(true),
Cases: []runtests.TestCaseResult{
{SuiteName: "PassSuite", CaseName: "Case1", Status: runtests.TestSuccess, Format: "GoogleTest"},
},
},
},
// Scenario 2: Case failure with clean fixture -> 1 fixture (PASSED)
{
Name: "fuchsia-pkg://fuchsia.com/case-fail#meta/case-fail.cm",
Status: runtests.TestFailure,
StartTime: time.Unix(1700000000, 0),
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(true),
TeardownSucceeded: boolPtr(true),
Cases: []runtests.TestCaseResult{
{SuiteName: "CaseFailSuite", CaseName: "Case1", Status: runtests.TestFailure, Format: "GoogleTest"},
},
},
},
// Lifecycle fields unset -> no fixture
{
Name: "fuchsia-pkg://fuchsia.com/no-lifecycle#meta/no-lifecycle.cm",
Status: runtests.TestSuccess,
StartTime: time.Unix(1700000000, 0),
TestResult: runtests.TestResult{
Cases: []runtests.TestCaseResult{
{SuiteName: "NoLifecycleSuite", CaseName: "Case1", Status: runtests.TestSuccess, Format: "GoogleTest"},
},
},
},
// Scenario 4: Setup failed -> 1 fixture (FAILED)
{
Name: "fuchsia-pkg://fuchsia.com/setup-fail#meta/setup-fail.cm",
Status: runtests.TestFailure,
StartTime: time.Unix(1700000000, 0),
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(false),
Cases: []runtests.TestCaseResult{
{SuiteName: "SetupFailSuite", CaseName: "Case1", Status: runtests.TestSkipped, Format: "GoogleTest"},
},
},
},
// Setup failed before test case enumeration -> no separate fixture,
// since the top-level result already uses the *fixture case name.
{
Name: "fuchsia-pkg://fuchsia.com/setup-fail-no-cases#meta/setup-fail-no-cases.cm",
Status: runtests.TestFailure,
StartTime: time.Unix(1700000000, 0),
TestResult: runtests.TestResult{SetupSucceeded: boolPtr(false)},
},
// Scenario 6: Teardown failed -> 1 fixture (FAILED) for all suites
{
Name: "fuchsia-pkg://fuchsia.com/teardown-fail#meta/teardown-fail.cm",
Status: runtests.TestFailure,
StartTime: time.Unix(1700000000, 0),
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(true),
TeardownSucceeded: boolPtr(false),
Cases: []runtests.TestCaseResult{
{SuiteName: "TeardownFailSuiteA", CaseName: "Case1", Status: runtests.TestSuccess, Format: "GoogleTest"},
{SuiteName: "TeardownFailSuiteB", CaseName: "Case1", Status: runtests.TestSuccess, Format: "GoogleTest"},
},
},
},
// Scenario 8: Noisy neighbor -> 1 fixture (EXECUTION_ERRORED)
{
Name: "fuchsia-pkg://fuchsia.com/noisy#meta/noisy.cm",
Status: runtests.TestInfraFailure,
StartTime: time.Unix(1700000000, 0),
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(false),
Cases: []runtests.TestCaseResult{
{SuiteName: "NoisySuite", CaseName: "Case1", Status: runtests.TestSkipped, Format: "GoogleTest"},
},
},
},
// Setup failed, but the fixture's test ID is too long -> skipped
{
Name: longName,
Status: runtests.TestFailure,
StartTime: time.Unix(1700000000, 0),
TestResult: runtests.TestResult{
SetupSucceeded: boolPtr(false),
Cases: []runtests.TestCaseResult{
{SuiteName: "LongSuite", CaseName: "Case1", Status: runtests.TestSkipped, Format: "GoogleTest"},
},
},
},
}}
results, _, testsSkipped := SummaryToResultSink(summary, nil, t.TempDir())
fixtureStatuses := map[string]resultpb.TestResult_Status{}
var fixtureIDs []string
for _, r := range results {
if strings.HasSuffix(r.TestId, ":"+FixtureCaseName) {
fixtureIDs = append(fixtureIDs, r.TestId)
fixtureStatuses[r.TestId] = r.StatusV2
}
}
wantFixtureStatuses := map[string]resultpb.TestResult_Status{
"fuchsia-pkg://fuchsia.com/pass#meta/pass.cm/PassSuite:" + FixtureCaseName: resultpb.TestResult_PASSED,
"fuchsia-pkg://fuchsia.com/case-fail#meta/case-fail.cm/CaseFailSuite:" + FixtureCaseName: resultpb.TestResult_PASSED,
"fuchsia-pkg://fuchsia.com/setup-fail#meta/setup-fail.cm/SetupFailSuite:" + FixtureCaseName: resultpb.TestResult_FAILED,
"fuchsia-pkg://fuchsia.com/teardown-fail#meta/teardown-fail.cm/test:" + FixtureCaseName: resultpb.TestResult_FAILED,
"fuchsia-pkg://fuchsia.com/noisy#meta/noisy.cm/NoisySuite:" + FixtureCaseName: resultpb.TestResult_EXECUTION_ERRORED,
}
wantFixtureIDs := []string{
"fuchsia-pkg://fuchsia.com/pass#meta/pass.cm/PassSuite:" + FixtureCaseName,
"fuchsia-pkg://fuchsia.com/case-fail#meta/case-fail.cm/CaseFailSuite:" + FixtureCaseName,
"fuchsia-pkg://fuchsia.com/setup-fail#meta/setup-fail.cm/SetupFailSuite:" + FixtureCaseName,
"fuchsia-pkg://fuchsia.com/teardown-fail#meta/teardown-fail.cm/test:" + FixtureCaseName,
"fuchsia-pkg://fuchsia.com/noisy#meta/noisy.cm/NoisySuite:" + FixtureCaseName,
}
if diff := cmp.Diff(wantFixtureIDs, fixtureIDs); diff != "" {
t.Errorf("fixture test IDs mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(wantFixtureStatuses, fixtureStatuses); diff != "" {
t.Errorf("fixture statuses mismatch (-want +got):\n%s", diff)
}
// The long-named test's case is also skipped, so only compare *fixtures.
// With no test case result to share a fine_name with, the *fixture falls
// back to the scheme default.
var skippedFixtureIDs []string
for _, id := range testsSkipped {
if strings.HasSuffix(id, ":"+FixtureCaseName) {
skippedFixtureIDs = append(skippedFixtureIDs, id)
}
}
if diff := cmp.Diff([]string{longName + "/test:" + FixtureCaseName}, skippedFixtureIDs); diff != "" {
t.Errorf("skipped fixture test IDs mismatch (-want +got):\n%s", diff)
}
}