blob: 50ee76482c03eb870f1036abe43fada383c85cb0 [file] [log] [blame]
// Copyright 2018 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 (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"go.fuchsia.dev/fuchsia/tools/build"
fintpb "go.fuchsia.dev/fuchsia/tools/integration/fint/proto"
"go.fuchsia.dev/fuchsia/tools/integration/testsharder/metadata"
"go.fuchsia.dev/fuchsia/tools/lib/jsonutil"
)
const (
// The name of the blobs directory within a package repository.
blobsDirName = "blobs"
)
// Note that just printing a list of shard pointers will print a list of memory addresses,
// which would make for an unhelpful error message.
func assertEqual(t *testing.T, expected, actual []*Shard) {
t.Helper()
opts := cmp.Options{
// We don't care about ordering of shards.
cmpopts.SortSlices(func(s1, s2 *Shard) bool {
if s1.Name == s2.Name {
return environmentKey(s1.Env) < environmentKey(s2.Env)
}
return s1.Name < s2.Name
}),
cmp.FilterValues(func(s1, s2 fintpb.SetArtifacts_Metadata) bool {
return true
}, cmp.Ignore()),
}
if diff := cmp.Diff(expected, actual, opts...); diff != "" {
t.Fatalf("shards mismatch: (-want + got):\n%s", diff)
}
}
func fullTestName(id int, os string) string {
if os == "fuchsia" {
return fmt.Sprintf("fuchsia-pkg://fuchsia.com/test%d", id)
}
return fmt.Sprintf("/path/to/test%d", id)
}
func makeTest(id int, os string) Test {
return Test{
Test: build.Test{
Name: fullTestName(id, os),
PackageURL: fullTestName(id, "fuchsia"),
Path: fullTestName(id, "linux"),
OS: os,
Label: "//src/sys:foo_test(//build/toolchain/fuchsia:x64)",
},
Runs: 1,
}
}
func makeTestWithMetadata(id int, os string, testMetadata metadata.TestMetadata) Test {
test := makeTest(id, os)
test.Metadata = testMetadata
return test
}
func spec(id int, envs ...build.Environment) build.TestSpec {
return build.TestSpec{
Test: makeTest(id, "fuchsia").Test,
Envs: envs,
ExpectsSSH: true,
}
}
func fuchsiaShard(env build.Environment, ids ...int) *Shard {
return shard(env, "fuchsia", ids...)
}
func fuchsiaShardWithMetadata(env build.Environment, testMetadata metadata.TestMetadata, ids ...int) *Shard {
return shardWithMetadata(env, "fuchsia", testMetadata, ids...)
}
func shard(env build.Environment, os string, ids ...int) *Shard {
var tests []Test
for _, id := range ids {
tests = append(tests, makeTest(id, os))
}
return &Shard{
Name: environmentName(env),
Tests: tests,
Env: populateEnvDefaults(env, "x64", false),
ExpectsSSH: true,
HostCPU: GetHostCPU(env, env.Emulator.Accel == build.AccelNone),
}
}
func shardWithMetadata(env build.Environment, os string, testMetadata metadata.TestMetadata, ids ...int) *Shard {
var tests []Test
for _, id := range ids {
tests = append(tests, makeTestWithMetadata(id, os, testMetadata))
}
return &Shard{
Name: environmentName(env),
Tests: tests,
Env: populateEnvDefaults(env, "x64", false),
ExpectsSSH: true,
HostCPU: GetHostCPU(env, env.Emulator.Accel == build.AccelNone),
}
}
func TestMakeShards(t *testing.T) {
env1 := build.Environment{
Dimensions: build.DimensionSet{"device_type": "QEMU", "cpu": "x64"},
Tags: []string{},
}
env2 := build.Environment{
Dimensions: build.DimensionSet{"device_type": "NUC", "cpu": "x64"},
Tags: []string{},
}
env3 := build.Environment{
Dimensions: build.DimensionSet{"os": "Linux", "cpu": "x64"},
Tags: []string{},
}
env4 := build.Environment{
Dimensions: build.DimensionSet{"device_type": "AEMU", "cpu": "arm64"},
Tags: []string{},
}
env5 := build.Environment{
Dimensions: build.DimensionSet{"device_type": "AEMU"},
Emulator: build.EmulatorInfo{
Name: "emu-min",
Device: "min",
},
Tags: []string{},
}
env6 := build.Environment{
Dimensions: build.DimensionSet{"device_type": "AEMU"},
Emulator: build.EmulatorInfo{
Name: "emu-min",
Device: "min",
Accel: "none",
},
Tags: []string{},
}
basicOpts := &ShardOptions{
Tags: []string{},
}
t.Run("environments have nonempty names", func(t *testing.T) {
envs := []build.Environment{env1, env2, env3}
for _, env := range envs {
if environmentName(env) == "" {
t.Fatalf("build.Environment\n%+v\n has an empty name", env)
}
}
})
t.Run("tests of same environment are grouped", func(t *testing.T) {
actual := MakeShards(
[]build.TestSpec{spec(1, env1, env2), spec(2, env1, env3), spec(3, env3)},
nil,
basicOpts,
make(map[string]metadata.TestMetadata),
)
expected := []*Shard{fuchsiaShard(env1, 1, 2), fuchsiaShard(env2, 1), fuchsiaShard(env3, 2, 3)}
assertEqual(t, expected, actual)
})
t.Run("tests with diff emulator envs should be separate", func(t *testing.T) {
actual := MakeShards(
[]build.TestSpec{spec(1, env5), spec(2, env5), spec(3, env6)},
nil,
basicOpts,
make(map[string]metadata.TestMetadata),
)
// env5 and env6 only differ by the accel mode but should end up having
// the same name. They should still be sharded separately due to the
// different emulator configuration. Shard names are checked for duplication
// later in main.go.
expected := []*Shard{fuchsiaShard(env5, 1, 2), fuchsiaShard(env6, 3)}
assertEqual(t, expected, actual)
})
t.Run("metadata is added to tests", func(t *testing.T) {
testMetadata := metadata.TestMetadata{
Owners: []string{"carverforbes@google.com"},
ComponentID: 1478143,
}
metadataMap := make(map[string]metadata.TestMetadata)
metadataMap["fuchsia-pkg://fuchsia.com/test4"] = testMetadata
actual := MakeShards(
[]build.TestSpec{spec(1, env1, env2), spec(2, env1, env3), spec(3, env3), spec(4, env4)},
nil,
basicOpts,
metadataMap,
)
expected := []*Shard{fuchsiaShard(env1, 1, 2), fuchsiaShard(env2, 1), fuchsiaShard(env3, 2, 3), fuchsiaShardWithMetadata(env4, testMetadata, 4)}
assertEqual(t, expected, actual)
})
t.Run("there is no deduplication of tests", func(t *testing.T) {
actual := MakeShards(
[]build.TestSpec{spec(1, env1), spec(1, env1), spec(1, env1)},
nil,
basicOpts,
make(map[string]metadata.TestMetadata),
)
expected := []*Shard{fuchsiaShard(env1, 1, 1, 1)}
assertEqual(t, expected, actual)
})
// Ensure that the order of the shards is the order in which their
// corresponding environments appear in the input. This is the simplest
// deterministic order we can produce for the shards.
t.Run("shards are ordered", func(t *testing.T) {
actual := MakeShards(
[]build.TestSpec{spec(1, env2, env3), spec(2, env1), spec(3, env3)},
nil,
basicOpts,
make(map[string]metadata.TestMetadata),
)
expected := []*Shard{fuchsiaShard(env2, 1), fuchsiaShard(env3, 1, 3), fuchsiaShard(env1, 2)}
assertEqual(t, expected, actual)
})
t.Run("tags are respected", func(t *testing.T) {
tagger := func(env build.Environment, tags ...string) build.Environment {
env2 := env
env2.Tags = tags
return env2
}
actual := MakeShards(
[]build.TestSpec{
spec(1, tagger(env1, "A")),
spec(2, tagger(env1, "A", "B", "C")),
spec(3, tagger(env2, "B", "C")),
spec(4, tagger(env3, "C", "A")),
spec(5, tagger(env3, "A", "C")),
},
nil,
&ShardOptions{
Tags: []string{"A", "C"},
},
make(map[string]metadata.TestMetadata),
)
expected := []*Shard{
// "C", "A" and "A", "C" should define the same tags.
fuchsiaShard(tagger(env3, "A", "C"), 4, 5),
}
assertEqual(t, expected, actual)
})
t.Run("different service accounts get different shards", func(t *testing.T) {
withAcct := func(env build.Environment, acct string) build.Environment {
env2 := env
env2.ServiceAccount = acct
return env2
}
actual := MakeShards(
[]build.TestSpec{
spec(1, env1),
spec(1, withAcct(env1, "acct1")),
spec(1, withAcct(env1, "acct2")),
},
nil,
basicOpts,
make(map[string]metadata.TestMetadata),
)
expected := []*Shard{
fuchsiaShard(env1, 1),
fuchsiaShard(withAcct(env1, "acct1"), 1),
fuchsiaShard(withAcct(env1, "acct2"), 1),
}
assertEqual(t, expected, actual)
})
t.Run("netboot envs get different shards", func(t *testing.T) {
withNetboot := func(env build.Environment) build.Environment {
env2 := env
env2.Netboot = true
return env2
}
actual := MakeShards(
[]build.TestSpec{
spec(1, env1),
spec(1, withNetboot(env1)),
},
nil,
basicOpts,
make(map[string]metadata.TestMetadata),
)
expected := []*Shard{
fuchsiaShard(env1, 1),
fuchsiaShard(withNetboot(env1), 1),
}
assertEqual(t, expected, actual)
})
t.Run("isolated tests are in separate shards", func(t *testing.T) {
isolate := func(test build.TestSpec) build.TestSpec {
test.Test.Isolated = true
test.ExpectsSSH = false
return test
}
actual := MakeShards(
[]build.TestSpec{
isolate(spec(1, env1, env2)),
spec(2, env1),
spec(3, env1),
isolate(spec(4, env2)),
},
nil,
basicOpts,
make(map[string]metadata.TestMetadata),
)
isolateShard := func(shard *Shard, index int) *Shard {
shard.Name = fmt.Sprintf("%s-%s", shard.Name, normalizeTestName(shard.Tests[0].Test.Name))
for i := range shard.Tests {
shard.Tests[i].Test.Isolated = true
}
shard.ExpectsSSH = false
return shard
}
expected := []*Shard{
isolateShard(fuchsiaShard(env1, 1), 1),
isolateShard(fuchsiaShard(env2, 1), 1),
fuchsiaShard(env1, 2, 3),
isolateShard(fuchsiaShard(env2, 4), 2),
}
assertEqual(t, expected, actual)
})
t.Run("values from test-list.json are copied over", func(t *testing.T) {
testListEntry := build.TestListEntry{
Name: fullTestName(1, "fuchsia"),
Tags: []build.TestTag{
{
Key: "key",
Value: "value",
},
},
Execution: build.ExecutionDef{Realm: "/some/realm"},
}
actual := MakeShards(
[]build.TestSpec{
spec(1, env1, env2),
spec(2, env1),
spec(3, env2),
},
map[string]build.TestListEntry{
fullTestName(1, "fuchsia"): testListEntry,
},
basicOpts,
make(map[string]metadata.TestMetadata),
)
makeTestWithTagsAndRealm := func(id int, tags []build.TestTag, realm string) Test {
test := makeTest(id, "fuchsia")
test.Tags = tags
test.Realm = realm
return test
}
expected := []*Shard{
{
Name: environmentName(env1),
Tests: []Test{makeTestWithTagsAndRealm(1, testListEntry.Tags, "/some/realm"), makeTest(2, "fuchsia")},
Env: populateEnvDefaults(env1, "x64", false),
ExpectsSSH: true,
HostCPU: GetHostCPU(env1, false),
}, {
Name: environmentName(env2),
Tests: []Test{makeTestWithTagsAndRealm(1, testListEntry.Tags, "/some/realm"), makeTest(3, "fuchsia")},
Env: populateEnvDefaults(env2, "x64", false),
ExpectsSSH: true,
HostCPU: GetHostCPU(env2, false),
},
}
assertEqual(t, expected, actual)
})
t.Run("shard with package repo", func(t *testing.T) {
buildDir := t.TempDir()
var blobMerkle, indirectBlobMerkle build.MerkleRoot
for i := 0; i < 32; i++ {
blobMerkle[i] = byte(1)
indirectBlobMerkle[i] = byte(2)
}
withPackageManifest := func(test build.TestSpec) build.TestSpec {
packageManifestPath := fmt.Sprintf("path/to/%s/package_manifest.json", test.Name)
test.PackageManifests = []string{packageManifestPath}
absPath := filepath.Join(buildDir, packageManifestPath)
if err := os.MkdirAll(filepath.Dir(absPath), 0o700); err != nil {
t.Fatal(err)
}
// Write inner subpackage
subpackageName := test.Name + "subpackage"
subpackageManifestPath := fmt.Sprintf("path/to/%s/package_manifest.json", subpackageName)
subAbsPath := filepath.Join(buildDir, subpackageManifestPath)
if err := os.MkdirAll(filepath.Dir(subAbsPath), 0o700); err != nil {
t.Fatal(err)
}
subpackageManifest := build.PackageManifest{
Version: "1",
Blobs: []build.PackageBlobInfo{
{Merkle: indirectBlobMerkle},
},
}
if err := jsonutil.WriteToFile(subAbsPath, subpackageManifest); err != nil {
t.Fatal(err)
}
packageManifest := build.PackageManifest{
Version: "1",
Blobs: []build.PackageBlobInfo{
{Merkle: blobMerkle},
},
Subpackages: []build.PackageSubpackageInfo{
{Name: subpackageName, ManifestPath: subpackageManifestPath},
},
}
if err := jsonutil.WriteToFile(absPath, packageManifest); err != nil {
t.Fatal(err)
}
return test
}
actual := MakeShards(
[]build.TestSpec{
withPackageManifest(spec(1, env1, env2)),
spec(2, env1),
spec(3, env1),
},
nil,
basicOpts,
make(map[string]metadata.TestMetadata),
)
// Create regular blob.
writeBlob := func(blobsDir string, blobMerkle build.MerkleRoot) {
if err := os.MkdirAll(blobsDir, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(blobsDir, blobMerkle.String()), []byte("blob"), 0o700); err != nil {
t.Fatal(err)
}
}
writeBlob(filepath.Join(buildDir, blobsDirName), blobMerkle)
writeBlob(filepath.Join(buildDir, blobsDirName), indirectBlobMerkle)
for _, s := range actual {
if err := s.CreatePackageRepo(buildDir, "", true); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(buildDir, s.PkgRepo, blobsDirName, blobMerkle.String())); err != nil {
t.Error(err)
}
}
makeTestWithPackageManifest := func(id int) Test {
test := makeTest(id, "fuchsia")
test.PackageManifests = []string{fmt.Sprintf("path/to/%s/package_manifest.json", test.Name)}
return test
}
expected := []*Shard{
{
Name: environmentName(env1),
Tests: []Test{makeTestWithPackageManifest(1), makeTest(2, "fuchsia"), makeTest(3, "fuchsia")},
Env: populateEnvDefaults(env1, "x64", false),
ExpectsSSH: true,
PkgRepo: fmt.Sprintf("repo_%s", environmentName(env1)),
Deps: []string{fmt.Sprintf("repo_%s", environmentName(env1))},
HostCPU: GetHostCPU(env1, false),
}, {
Name: environmentName(env2),
Tests: []Test{makeTestWithPackageManifest(1)},
Env: populateEnvDefaults(env2, "x64", false),
ExpectsSSH: true,
PkgRepo: fmt.Sprintf("repo_%s", environmentName(env2)),
Deps: []string{fmt.Sprintf("repo_%s", environmentName(env2))},
HostCPU: GetHostCPU(env2, false),
},
}
assertEqual(t, expected, actual)
// Create delivery blobs.
deliveryBlobConfig := build.DeliveryBlobConfig{
Type: 1,
}
if err := jsonutil.WriteToFile(filepath.Join(buildDir, deliveryBlobConfigName), deliveryBlobConfig); err != nil {
t.Fatal(err)
}
writeBlob(filepath.Join(buildDir, blobsDirName, "1"), blobMerkle)
writeBlob(filepath.Join(buildDir, blobsDirName, "1"), indirectBlobMerkle)
for _, s := range actual {
if err := s.CreatePackageRepo(buildDir, "", true); err != nil {
t.Fatal(err)
}
// Check that delivery blobs are used.
if _, err := os.Stat(filepath.Join(buildDir, s.PkgRepo, blobsDirName, "1", blobMerkle.String())); err != nil {
t.Error(err)
}
if _, err := os.Stat(filepath.Join(buildDir, s.PkgRepo, blobsDirName, blobMerkle.String())); !os.IsNotExist(err) {
t.Errorf("got err: %s; want file not exist err", err)
}
// Check that the subpackage blob was included too
if _, err := os.Stat(filepath.Join(buildDir, s.PkgRepo, blobsDirName, "1", indirectBlobMerkle.String())); err != nil {
t.Error(err)
}
if _, err := os.Stat(filepath.Join(buildDir, s.PkgRepo, blobsDirName, indirectBlobMerkle.String())); !os.IsNotExist(err) {
t.Errorf("got err: %s; want file not exist err", err)
}
}
// The PkgRepo dirname should be the same so no change is expected in the shards.
assertEqual(t, expected, actual)
})
}
func TestMakeShardNamesUnique(t *testing.T) {
shards := []*Shard{
{
Name: "foo",
Env: build.Environment{
Dimensions: build.DimensionSet{
"same_key_and_value": "1",
},
},
},
{
Name: "foo",
Env: build.Environment{
Dimensions: build.DimensionSet{
"same_key_and_value": "1",
"same_key": "1",
"other_key": "blah",
},
},
},
{
Name: "foo",
Env: build.Environment{
Dimensions: build.DimensionSet{
"same_key_and_value": "1",
"same_key": "2",
},
},
},
{
// Should not be updated.
Name: "bar",
Env: build.Environment{
Dimensions: build.DimensionSet{
"a": "1",
"b": "1",
},
},
},
}
makeShardNamesUnique(shards)
wantNames := []string{
"foo",
"foo-other_key:blah-same_key:1",
"foo-same_key:2",
"bar",
}
var gotNames []string
for _, shard := range shards {
gotNames = append(gotNames, shard.Name)
}
if diff := cmp.Diff(wantNames, gotNames); diff != "" {
t.Errorf("Wrong shard names (-want +got):\n%s", diff)
}
}