blob: 4160f077587b9ebaaf93721ecd2af806ac75c577 [file] [log] [blame]
// Copyright 2019 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 testsharder
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
"testing"
"github.com/google/go-cmp/cmp"
"go.fuchsia.dev/fuchsia/tools/build"
)
var barTestModifier = TestModifier{
Name: "bar",
TotalRuns: 2,
}
var bazTestModifier = TestModifier{
Name: "baz_host_tests",
OS: linux,
}
func TestLoadTestModifiers(t *testing.T) {
areEqual := func(a, b []ModifierMatch) bool {
stringify := func(modifier ModifierMatch) string {
return fmt.Sprintf("%#v", modifier)
}
sort := func(list []ModifierMatch) {
sort.Slice(list[:], func(i, j int) bool {
return stringify(list[i]) < stringify(list[j])
})
}
sort(a)
sort(b)
return reflect.DeepEqual(a, b)
}
tmpDir := t.TempDir()
initial := []TestModifier{barTestModifier, bazTestModifier}
modifiersPath := filepath.Join(tmpDir, "test_modifiers.json")
m, err := os.Create(modifiersPath)
if err != nil {
t.Fatal(err)
}
defer m.Close()
if err := json.NewEncoder(m).Encode(&initial); err != nil {
t.Fatal(err)
}
linuxEnv := build.Environment{Dimensions: build.DimensionSet{"os": linux, "cpu": x64}}
aemuEnv := build.Environment{Dimensions: build.DimensionSet{"device_type": "AEMU"}}
otherDeviceEnv := build.Environment{Dimensions: build.DimensionSet{"device_type": "other-device"}}
specs := []build.TestSpec{
{
Test: build.Test{Name: "baz_host_tests", OS: linux, CPU: x64},
Envs: []build.Environment{linuxEnv},
},
{
Test: build.Test{Name: "bar_tests_hti", OS: linux, CPU: x64},
Envs: []build.Environment{aemuEnv},
},
{
Test: build.Test{Name: "bar_tests", OS: fuchsia, CPU: x64},
Envs: []build.Environment{aemuEnv, otherDeviceEnv},
},
}
actual, err := LoadTestModifiers(context.Background(), specs, modifiersPath)
if err != nil {
t.Fatalf("failed to load test modifiers: %v", err)
}
makeModifierMatch := func(test string, env build.Environment, modifier TestModifier) ModifierMatch {
return ModifierMatch{
Test: test,
Env: env,
Modifier: modifier,
}
}
bazOut := makeModifierMatch("baz_host_tests", linuxEnv, bazTestModifier)
barHTIOut := makeModifierMatch("bar_tests_hti", aemuEnv, barTestModifier)
barOut := makeModifierMatch("bar_tests", aemuEnv, barTestModifier)
barOut2 := makeModifierMatch("bar_tests", otherDeviceEnv, barTestModifier)
expected := []ModifierMatch{barHTIOut, barOut, barOut2, bazOut}
if !areEqual(expected, actual) {
t.Fatalf("test modifiers not properly loaded:\nexpected:\n%+v\nactual:\n%+v", expected, actual)
}
}
func TestAffectedModifiers(t *testing.T) {
affectedTests := []string{
"affected-arm64", "affected-linux", "affected-mac", "affected-host+target", "affected-AEMU", "affected-other-device",
}
const maxAttempts = 2
t.Run("not multiplied if over threshold", func(t *testing.T) {
mods, err := AffectedModifiers(nil, affectedTests, maxAttempts, len(affectedTests)-1)
if err != nil {
t.Errorf("AffectedModifiers() returned failed: %v", err)
}
for _, m := range mods {
mod := m.Modifier
if mod.MaxAttempts != maxAttempts {
t.Errorf("%s.MaxAttempts is %d, want %d", mod.Name, mod.MaxAttempts, maxAttempts)
}
if !mod.Affected {
t.Errorf("%s.Affected is false, want true", mod.Name)
}
if mod.TotalRuns >= 0 {
t.Errorf("%s.TotalRuns is %d, want -1", mod.Name, mod.TotalRuns)
}
}
})
t.Run("multiplied", func(t *testing.T) {
specs := []build.TestSpec{
{Test: build.Test{Name: "affected-arm64", OS: linux, CPU: "arm64"}},
{Test: build.Test{Name: "affected-linux", OS: linux, CPU: x64}},
{Test: build.Test{Name: "affected-mac", OS: "mac", CPU: x64}},
{
Test: build.Test{Name: "affected-host+target", OS: linux, CPU: x64},
Envs: []build.Environment{{Dimensions: build.DimensionSet{"device_type": "AEMU"}}},
},
{
Test: build.Test{Name: "affected-AEMU", OS: fuchsia, CPU: x64},
Envs: []build.Environment{{Dimensions: build.DimensionSet{"device_type": "AEMU"}}},
},
{
Test: build.Test{Name: "affected-other-device", OS: fuchsia, CPU: x64},
Envs: []build.Environment{{Dimensions: build.DimensionSet{"device_type": "other-device"}}},
},
{
Test: build.Test{Name: "affected-AEMU-and-other-device", OS: fuchsia, CPU: x64},
Envs: []build.Environment{
{Dimensions: build.DimensionSet{"device_type": "AEMU"}},
{Dimensions: build.DimensionSet{"device_type": "other-device"}},
},
},
{Test: build.Test{Name: "not-affected"}},
}
nameToShouldBeMultiplied := map[string]bool{
"affected-arm64-Linux": false,
"affected-linux-Linux": true,
"affected-mac-Mac": false,
"affected-host+target-AEMU": false,
"affected-AEMU-AEMU": true,
"affected-other-device-other-device": false,
"affected-AEMU-and-other-device-AEMU": true,
"affected-AEMU-and-other-device-other-device": false,
}
mods, err := AffectedModifiers(specs, affectedTests, maxAttempts, len(affectedTests))
if err != nil {
t.Errorf("AffectedModifiers() returned failed: %v", err)
}
for _, m := range mods {
mod := m.Modifier
shouldBeMultiplied := nameToShouldBeMultiplied[fmt.Sprintf("%s-%s", m.Test, environmentName(m.Env))]
if shouldBeMultiplied {
if mod.MaxAttempts != 0 {
t.Errorf("%s.MaxAttempts is %d, want 0", mod.Name, mod.MaxAttempts)
}
if mod.TotalRuns != 0 {
t.Errorf("%s.TotalRuns is %d, want 0", mod.Name, mod.MaxAttempts)
}
} else {
if mod.MaxAttempts != maxAttempts {
t.Errorf("%s.MaxAttempts is %d, want %d", mod.Name, mod.MaxAttempts, maxAttempts)
}
if !mod.Affected {
t.Errorf("%s.Affected is false, want true", mod.Name)
}
if mod.TotalRuns >= 0 {
t.Errorf("%s.TotalRuns is %d, want -1", mod.Name, mod.TotalRuns)
}
}
}
})
}
func TestMatchModifiersToTests(t *testing.T) {
aemuEnv := build.Environment{Dimensions: build.DimensionSet{"device_type": "AEMU"}}
otherDeviceEnv := build.Environment{Dimensions: build.DimensionSet{"device_type": "other-device"}}
makeTestSpecs := func(count int, envs []build.Environment) []build.TestSpec {
var specs []build.TestSpec
for i := 0; i < count; i++ {
specs = append(specs, build.TestSpec{
Test: build.Test{Name: fullTestName(i, "fuchsia"), OS: fuchsia, CPU: x64},
Envs: envs,
})
}
return specs
}
cases := []struct {
name string
specs []build.TestSpec
multipliers []TestModifier
expected []ModifierMatch
err error
}{
{
name: "one match per test-env",
specs: makeTestSpecs(1, []build.Environment{aemuEnv, otherDeviceEnv}),
multipliers: []TestModifier{
{Name: "0", TotalRuns: 1},
},
expected: []ModifierMatch{
{
Test: fullTestName(0, "fuchsia"), Env: aemuEnv,
Modifier: TestModifier{Name: "0", TotalRuns: 1},
},
{
Test: fullTestName(0, "fuchsia"), Env: otherDeviceEnv,
Modifier: TestModifier{Name: "0", TotalRuns: 1},
},
},
},
{
name: "uses regex matches if no tests match exactly",
specs: makeTestSpecs(1, []build.Environment{aemuEnv}),
multipliers: []TestModifier{
{Name: "0", TotalRuns: 1},
},
expected: []ModifierMatch{{
Test: fullTestName(0, "fuchsia"), Env: aemuEnv,
Modifier: TestModifier{Name: "0", TotalRuns: 1},
}},
},
{
name: "prefer exact match over regex match",
specs: []build.TestSpec{
{
Test: build.Test{Name: fullTestName(1, "fuchsia"), OS: fuchsia, CPU: x64},
Envs: []build.Environment{aemuEnv},
},
{
Test: build.Test{Name: fullTestName(10, "fuchsia"), OS: fuchsia, CPU: x64},
Envs: []build.Environment{aemuEnv},
},
},
multipliers: []TestModifier{
{Name: fullTestName(1, "fuchsia"), TotalRuns: 1},
},
expected: []ModifierMatch{{
Test: fullTestName(1, "fuchsia"), Env: aemuEnv,
Modifier: TestModifier{Name: fullTestName(1, "fuchsia"), TotalRuns: 1},
}},
},
{
name: "rejects multiplier that matches too many tests",
specs: makeTestSpecs(maxMatchesPerMultiplier+1, []build.Environment{aemuEnv}),
multipliers: []TestModifier{
{Name: "fuchsia-pkg", TotalRuns: 1},
},
err: errTooManyMultiplierMatches,
},
{
name: "rejects invalid multiplier regex",
specs: makeTestSpecs(1, []build.Environment{aemuEnv}),
multipliers: []TestModifier{
{Name: "["},
},
err: errInvalidMultiplierRegex,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
actual, err := matchModifiersToTests(context.Background(), tc.specs, tc.multipliers)
if !errors.Is(err, tc.err) {
t.Fatalf("got err: %s, want %s", err, tc.err)
}
if diff := cmp.Diff(actual, tc.expected); diff != "" {
t.Fatalf("unexpected ModifierMatches: (-got +want):\n%s", diff)
}
})
}
}
// mkTempFile returns a new temporary file with the specified content that will
// be cleaned up automatically.
func mkTempFile(t *testing.T, content string) string {
name := filepath.Join(t.TempDir(), "foo")
if err := os.WriteFile(name, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
return name
}