blob: 88dba9b1eea45c12cab37d4329c4473927aaf68a [file] [log] [blame]
// Copyright 2020 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 tefmocheck
import (
"fmt"
"path"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"go.fuchsia.dev/fuchsia/tools/build"
"go.fuchsia.dev/fuchsia/tools/testing/runtests"
)
func TestCheck(t *testing.T) {
const killerString = "KILLER STRING"
const exceptString = "Don't die!"
exceptBlock := &logBlock{
startString: "skip_test",
endString: "end_test",
}
exceptBlock2 := &logBlock{
startString: "block_start",
endString: "block_end",
}
c := stringInLogCheck{
String: killerString,
OnlyOnStates: []string{},
ExceptStrings: []string{exceptString},
ExceptBlocks: []*logBlock{exceptBlock, exceptBlock2},
Type: swarmingOutputType,
}
gotName := c.Name()
wantName := "string_in_log/infra_and_test_std_and_klog.txt/KILLER_STRING"
if gotName != wantName {
t.Errorf("c.Name() returned %q, want %q", gotName, wantName)
}
for _, tc := range []struct {
name string
attributeToTest bool
addTag bool
testingOutputs TestingOutputs
typeToCheck logType
skipPassedTask bool
skipAllPassedTests bool
skipPassedTest bool
ignoreFlakes bool
alwaysFlake bool
states []string
swarmingResultState string
shouldMatch bool
wantName string
wantFlake bool
wantTags []build.TestTag
}{
{
name: "should match simple",
testingOutputs: TestingOutputs{
SwarmingOutput: []byte(fmt.Sprintf("PREFIX %s SUFFIX", killerString)),
},
shouldMatch: true,
},
{
name: "exceptSuccessfulSwarmingResult",
testingOutputs: TestingOutputs{
SwarmingOutput: []byte(fmt.Sprintf("PREFIX %s SUFFIX", killerString)),
},
skipPassedTask: true,
swarmingResultState: "COMPLETED",
},
{
name: "should not match if string in other log",
testingOutputs: TestingOutputs{
SerialLogs: [][]byte{[]byte(killerString)},
SwarmingOutput: []byte("gentle string"),
},
},
{
name: "should not match if except_string in log",
testingOutputs: TestingOutputs{
SwarmingOutput: []byte(killerString + exceptString),
},
},
{
name: "should match if string before except_block",
testingOutputs: TestingOutputs{
SwarmingOutput: []byte(fmt.Sprintf("PREFIX %s ... %s output %s SUFFIX", killerString, exceptBlock.startString, exceptBlock.endString)),
},
shouldMatch: true,
},
{
name: "should match if string after except_block",
testingOutputs: TestingOutputs{
SwarmingOutput: []byte(fmt.Sprintf("PREFIX %s output %s ... %s SUFFIX", exceptBlock.startString, exceptBlock.endString, killerString)),
},
shouldMatch: true,
},
{
name: "should not match if string in except_block",
testingOutputs: TestingOutputs{
SwarmingOutput: []byte(
fmt.Sprintf(
"PREFIX %s %s output %s SUFFIX %s %s %s",
exceptBlock.startString, killerString, exceptBlock.endString,
exceptBlock2.startString, killerString, exceptBlock2.endString)),
},
},
{
name: "should match if string in both except_block and outside except_block",
testingOutputs: TestingOutputs{
SwarmingOutput: []byte(fmt.Sprintf(
"PREFIX %s ... %s %s %s %s %s %s SUFFIX",
killerString, exceptBlock.startString, killerString, exceptBlock.endString,
exceptBlock2.startString, killerString, exceptBlock2.endString,
)),
},
shouldMatch: true,
},
{
name: "should not match if string in except_block with missing end string",
testingOutputs: TestingOutputs{
SwarmingOutput: []byte(fmt.Sprintf(
"PREFIX ... %s %s %s %s %s",
exceptBlock.startString, killerString, exceptBlock.endString,
exceptBlock2.startString, killerString,
)),
},
},
{
name: "should match if swarming task state is in expected states",
testingOutputs: TestingOutputs{
SwarmingOutput: []byte(killerString),
},
states: []string{"STATE_1", "STATE_2"},
swarmingResultState: "STATE_1",
shouldMatch: true,
},
{
name: "should not match if swarming task state is not in expected states",
testingOutputs: TestingOutputs{
SwarmingOutput: []byte(killerString),
},
states: []string{"STATE_1", "STATE_2"},
swarmingResultState: "NO_STATE",
},
{
name: "should match per test swarming output",
attributeToTest: true,
testingOutputs: TestingOutputs{
SwarmingOutput: []byte(killerString),
SwarmingOutputPerTest: []TestLog{
{
TestName: "foo-test",
Bytes: []byte(killerString),
FilePath: "foo/log.txt",
Index: 0,
},
},
},
shouldMatch: true,
wantName: path.Join(wantName, "foo-test"),
},
{
name: "should add test name to tag",
attributeToTest: true,
addTag: true,
testingOutputs: TestingOutputs{
SwarmingOutput: []byte(killerString),
SwarmingOutputPerTest: []TestLog{
{
TestName: "foo-test",
Bytes: []byte(killerString),
FilePath: "foo/log.txt",
Index: 0,
},
},
},
shouldMatch: true,
wantName: wantName,
wantTags: []build.TestTag{{Key: "test_name", Value: "foo-test"}},
},
{
name: "should respect except block even if split across tests",
attributeToTest: true,
testingOutputs: TestingOutputs{
SwarmingOutput: []byte(fmt.Sprintf("%s %s %s %s", exceptBlock.startString, killerString, exceptBlock.endString, killerString)),
SwarmingOutputPerTest: []TestLog{
{
TestName: "foo-test",
Bytes: []byte(fmt.Sprintf("%s %s", exceptBlock.startString, killerString)),
FilePath: "foo/log.txt",
Index: 0,
},
{
TestName: "bar-test",
Bytes: []byte(fmt.Sprintf("%s %s", exceptBlock.endString, killerString)),
FilePath: "bar/log.txt",
Index: len(exceptBlock.startString) + len(killerString) + 2,
},
},
},
shouldMatch: true,
wantName: path.Join(wantName, "bar-test"),
},
{
name: "should check all syslogs",
attributeToTest: true,
testingOutputs: TestingOutputs{
Syslogs: [][]byte{[]byte("gentle string"), []byte(killerString)},
},
shouldMatch: true,
typeToCheck: syslogType,
wantName: "string_in_log/syslog.txt/KILLER_STRING",
},
{
name: "should skip if all tests passed",
testingOutputs: TestingOutputs{
TestSummary: &runtests.TestSummary{
Tests: []runtests.TestDetails{
{
Name: "test1",
Result: runtests.TestSuccess,
},
},
},
SerialLogs: [][]byte{[]byte("gentle string"), []byte(killerString)},
},
skipAllPassedTests: true,
swarmingResultState: "COMPLETED",
typeToCheck: serialLogType,
},
{
name: "should skip if ignore flakes",
testingOutputs: TestingOutputs{
TestSummary: &runtests.TestSummary{
Tests: []runtests.TestDetails{
{
Name: "test1",
Result: runtests.TestFailure,
},
{
Name: "test1",
Result: runtests.TestSuccess,
},
},
},
SerialLogs: [][]byte{[]byte("gentle string"), []byte(killerString)},
},
skipAllPassedTests: true,
ignoreFlakes: true,
swarmingResultState: "COMPLETED",
typeToCheck: serialLogType,
},
{
name: "should not skip if a test failed",
testingOutputs: TestingOutputs{
TestSummary: &runtests.TestSummary{
Tests: []runtests.TestDetails{
{
Name: "test1",
Result: runtests.TestFailure,
},
},
},
SerialLogs: [][]byte{[]byte("gentle string"), []byte(killerString)},
},
shouldMatch: true,
skipAllPassedTests: true,
swarmingResultState: "COMPLETED",
typeToCheck: serialLogType,
},
{
name: "should not skip if not ignore flakes",
testingOutputs: TestingOutputs{
TestSummary: &runtests.TestSummary{
Tests: []runtests.TestDetails{
{
Name: "test1",
Result: runtests.TestFailure,
},
{
Name: "test1",
Result: runtests.TestSuccess,
},
},
},
SerialLogs: [][]byte{[]byte("gentle string"), []byte(killerString)},
},
shouldMatch: true,
skipAllPassedTests: true,
swarmingResultState: "COMPLETED",
typeToCheck: serialLogType,
},
{
name: "should skip if passed test",
testingOutputs: TestingOutputs{
TestSummary: &runtests.TestSummary{
Tests: []runtests.TestDetails{
{
Name: "test1",
Result: runtests.TestFailure,
},
{
Name: "test2",
Result: runtests.TestSuccess,
},
},
},
SwarmingOutput: []byte(killerString),
SwarmingOutputPerTest: []TestLog{
{
TestName: "test1",
Bytes: []byte{},
FilePath: "foo/log.txt",
Index: 0,
},
{
TestName: "test2",
Bytes: []byte(killerString),
FilePath: "foo/log.txt",
Index: 0,
},
},
},
skipPassedTest: true,
},
{
name: "should report flake if ignore flakes",
testingOutputs: TestingOutputs{
TestSummary: &runtests.TestSummary{
Tests: []runtests.TestDetails{
{
Name: "test1",
Result: runtests.TestFailure,
},
{
Name: "test1",
Result: runtests.TestSuccess,
},
{
Name: "test2",
Result: runtests.TestFailure,
},
},
},
SwarmingOutput: []byte(killerString + killerString),
SwarmingOutputPerTest: []TestLog{
{
TestName: "test1",
Bytes: []byte(killerString),
FilePath: "foo/log.txt",
Index: 0,
},
{
TestName: "test1",
Bytes: []byte(killerString),
FilePath: "foo/log.txt",
Index: len(killerString),
},
{
TestName: "test2",
Bytes: []byte{},
FilePath: "foo/log.txt",
Index: len(killerString) * 2,
},
},
},
shouldMatch: true,
skipPassedTest: true,
ignoreFlakes: true,
wantFlake: true,
},
{
name: "should report failure over flake",
testingOutputs: TestingOutputs{
TestSummary: &runtests.TestSummary{
Tests: []runtests.TestDetails{
{
Name: "test1",
Result: runtests.TestFailure,
},
{
Name: "test1",
Result: runtests.TestSuccess,
},
{
Name: "test2",
Result: runtests.TestFailure,
},
},
},
SwarmingOutput: []byte(killerString + killerString + killerString),
SwarmingOutputPerTest: []TestLog{
{
TestName: "test1",
Bytes: []byte(killerString),
FilePath: "foo/log.txt",
Index: 0,
},
{
TestName: "test1",
Bytes: []byte(killerString),
FilePath: "foo/log.txt",
Index: len(killerString),
},
{
TestName: "test2",
Bytes: []byte(killerString),
FilePath: "foo/log.txt",
Index: len(killerString) * 2,
},
},
},
shouldMatch: true,
skipPassedTest: true,
ignoreFlakes: true,
},
{
name: "should not skip if not ignore flakes and skipPassedTest",
testingOutputs: TestingOutputs{
TestSummary: &runtests.TestSummary{
Tests: []runtests.TestDetails{
{
Name: "test1",
Result: runtests.TestFailure,
},
{
Name: "test1",
Result: runtests.TestSuccess,
},
},
},
SwarmingOutput: []byte(killerString + killerString),
SwarmingOutputPerTest: []TestLog{
{
TestName: "test1",
Bytes: []byte(killerString),
FilePath: "foo/log.txt",
Index: 0,
},
{
TestName: "test1",
Bytes: []byte(killerString),
FilePath: "foo/log.txt",
Index: len(killerString),
},
},
},
shouldMatch: true,
skipPassedTest: true,
},
{
name: "should skip if skipPassedTask and a test failed",
testingOutputs: TestingOutputs{
TestSummary: &runtests.TestSummary{
Tests: []runtests.TestDetails{
{
Name: "test1",
Result: runtests.TestFailure,
},
},
},
SerialLogs: [][]byte{[]byte("gentle string"), []byte(killerString)},
},
skipPassedTask: true,
swarmingResultState: "COMPLETED",
typeToCheck: serialLogType,
},
{
name: "should always flake if not attributed to test",
testingOutputs: TestingOutputs{
TestSummary: &runtests.TestSummary{
Tests: []runtests.TestDetails{
{
Name: "test1",
Result: runtests.TestFailure,
},
{
Name: "test2",
Result: runtests.TestSuccess,
},
},
},
SwarmingOutput: []byte(killerString),
SwarmingOutputPerTest: []TestLog{
{
TestName: "test1",
Bytes: []byte{},
FilePath: "foo/log.txt",
Index: 0,
},
{
TestName: "test2",
Bytes: []byte(killerString),
FilePath: "foo/log.txt",
Index: 0,
},
},
},
shouldMatch: true,
alwaysFlake: true,
wantFlake: true,
},
{
name: "should always flake if skip passed test and failed test",
testingOutputs: TestingOutputs{
TestSummary: &runtests.TestSummary{
Tests: []runtests.TestDetails{
{
Name: "test1",
Result: runtests.TestSuccess,
},
{
Name: "test2",
Result: runtests.TestFailure,
},
},
},
SwarmingOutput: []byte(killerString),
SwarmingOutputPerTest: []TestLog{
{
TestName: "test1",
Bytes: []byte{},
FilePath: "foo/log.txt",
Index: 0,
},
{
TestName: "test2",
Bytes: []byte(killerString),
FilePath: "foo/log.txt",
Index: 0,
},
},
},
shouldMatch: true,
skipPassedTest: true,
alwaysFlake: true,
wantFlake: true,
},
{
name: "should not flake if skip passed test",
testingOutputs: TestingOutputs{
TestSummary: &runtests.TestSummary{
Tests: []runtests.TestDetails{
{
Name: "test1",
Result: runtests.TestFailure,
},
{
Name: "test2",
Result: runtests.TestSuccess,
},
},
},
SwarmingOutput: []byte(killerString),
SwarmingOutputPerTest: []TestLog{
{
TestName: "test1",
Bytes: []byte{},
FilePath: "foo/log.txt",
Index: 0,
},
{
TestName: "test2",
Bytes: []byte(killerString),
FilePath: "foo/log.txt",
Index: 0,
},
},
},
skipPassedTest: true,
alwaysFlake: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
c := c // Make a copy to avoid modifying shared state.
c.AttributeToTest = tc.attributeToTest
c.AddTag = tc.addTag
c.SkipPassedTask = tc.skipPassedTask
c.SkipAllPassedTests = tc.skipAllPassedTests
c.SkipPassedTest = tc.skipPassedTest
c.IgnoreFlakes = tc.ignoreFlakes
c.AlwaysFlake = tc.alwaysFlake
// It accesses this field for DebugText().
tc.testingOutputs.SwarmingSummary = &SwarmingTaskSummary{
Results: &SwarmingRpcsTaskResult{
TaskId: "abc", State: tc.swarmingResultState,
},
}
c.OnlyOnStates = tc.states
if tc.typeToCheck != "" {
c.Type = tc.typeToCheck
}
if c.Check(&tc.testingOutputs) != tc.shouldMatch {
t.Errorf("c.Check(%q) returned %t, expected %t",
tc.name, !tc.shouldMatch, tc.shouldMatch)
}
gotName := c.Name()
if tc.wantName != "" && gotName != tc.wantName {
t.Errorf("c.Name() returned %q, want %q", gotName, tc.wantName)
}
if tc.attributeToTest && tc.addTag && tc.shouldMatch {
if diff := cmp.Diff(tc.wantTags, c.Tags()); diff != "" {
t.Errorf("c.Tags() returned diff (-want +got): %s", diff)
}
}
c.DebugText() // minimal coverage, check it doesn't crash.
swarmingOutputPerTest := tc.testingOutputs.SwarmingOutputPerTest
gotOutputFiles := c.OutputFiles()
if len(swarmingOutputPerTest) == 0 && len(gotOutputFiles) != 0 {
t.Errorf("c.OutputFiles() returned %s, want []", gotOutputFiles)
}
if len(swarmingOutputPerTest) == 1 && (len(gotOutputFiles) != 1 || swarmingOutputPerTest[0].FilePath != gotOutputFiles[0]) {
t.Errorf("c.OutputFiles() returned %s, want %q", gotOutputFiles, swarmingOutputPerTest[0].FilePath)
}
if tc.wantFlake != c.IsFlake() {
t.Errorf("c.IsFlake() returned %t, expected %t", c.IsFlake(), tc.wantFlake)
}
})
}
}
func TestStringInLogsChecks(t *testing.T) {
t.Run("checks for infra tool logs are bucketed correctly", func(t *testing.T) {
infraTools := []string{"botanist", "testrunner"}
for _, check := range fuchsiaLogChecks() {
for _, tool := range infraTools {
if strings.Contains(check.Name(), tool) {
t.Errorf("Log check mentioning tool %q should go in infraToolLogChecks: %q", tool, check.Name())
}
}
}
})
t.Run("StringInLogsChecks returns all expected checks", func(t *testing.T) {
// This is intentionally brittle; we want to make it difficult for
// people to reorder checks in ways that might degrade tefmocheck's
// ability to root-cause failures.
expected := append(fuchsiaLogChecks(), infraToolLogChecks()...)
got := StringInLogsChecks()
// Crude check to make sure someone doesn't accidentally strip out most
// of StringInLogsChecks .
if len(got) < 50 {
t.Fatalf("StringInLogsChecks returned only %d checks, expected >=50", len(got))
}
// It's ok to have some extra checks preceding the start of the expected
// checks, so trim those off.
got = got[len(got)-len(expected):]
if diff := cmp.Diff(expected, got, cmp.AllowUnexported(stringInLogCheck{}, logBlock{})); diff != "" {
t.Errorf("StringInLogsChecks() returned diff (-want +got): %s", diff)
}
})
}