[testsharder] Begin soft transition s/label/tags
Why:
(1) "label" clashes with the GN label
(2) tags = plural; we will need arbitrary tags/labels to support things
like product configurations
Change-Id: I90fc87b3e68fc99bb8f40d2185458198806cd34b
diff --git a/cmd/testsharder/main.go b/cmd/testsharder/main.go
index 62fb2fa..245e6c6 100644
--- a/cmd/testsharder/main.go
+++ b/cmd/testsharder/main.go
@@ -9,6 +9,7 @@
"log"
"os"
+ "fuchsia.googlesource.com/tools/command"
"fuchsia.googlesource.com/tools/testsharder"
)
@@ -19,14 +20,14 @@
// The filepath to write output to. If unspecified, stdout is used.
outputFile string
- // Label is a key on which to filter environments, which are labeled.
- label string
+ // Tags are keys on which to filter environments, which are labeled.
+ tags command.StringsFlag
)
func init() {
flag.StringVar(&buildDir, "build-dir", "", "path to the fuchsia build directory root (required)")
flag.StringVar(&outputFile, "output-file", "", "path to a file which will contain the shards as JSON, default is stdout")
- flag.StringVar(&label, "label", "", "environment label on which to filter")
+ flag.Var(&tags, "tag", "environment tags on which to filter; only the tests that match all tags will be sharded")
}
func main() {
@@ -51,7 +52,7 @@
}
// Create shards and write them to an output file if specifed, else stdout.
- shards := testsharder.MakeShards(specs, label)
+ shards := testsharder.MakeShards(specs, tags)
f := os.Stdout
if outputFile != "" {
var err error
diff --git a/testsharder/environment.go b/testsharder/environment.go
index d0b2226..c9a4e76 100644
--- a/testsharder/environment.go
+++ b/testsharder/environment.go
@@ -24,7 +24,12 @@
Dimensions DimensionSet `json:"dimensions"`
// Label is a label given to an environment on which the testsharder may filter.
+ //
+ // TODO(joshuaseaton): deprecate in favor of tags.
Label string `json:"label,omitempty"`
+
+ // Tags are keys given to an environment on which the testsharder may filter.
+ Tags []string `json:"tags,omitempty"`
}
// Name returns a name calculated from its specfied properties.
diff --git a/testsharder/shard.go b/testsharder/shard.go
index ea70c9b..a743cd5 100644
--- a/testsharder/shard.go
+++ b/testsharder/shard.go
@@ -4,6 +4,7 @@
package testsharder
import (
+ "fmt"
"sort"
)
@@ -20,30 +21,42 @@
}
// MakeShards is the core algorithm to this tool. It takes a set of test specs and produces
-// a set of shards which may then be converted into Swarming tasks. Environments with a
-// label different from the provided will be ignored.
+// a set of shards which may then be converted into Swarming tasks.
+//
+// Environments that do not match all specified tags will be ignored.
//
// This is the most naive algorithm at the moment. It just merges all tests together which
// have the same environment setting into the same shard.
-func MakeShards(specs []TestSpec, label string) []*Shard {
+func MakeShards(specs []TestSpec, tags []string) []*Shard {
// Collect the order of the shards so our shard ordering is deterministic with
// respect to the input.
- envToSuites := make(map[Environment][]TestSpec)
+ envToSuites := newEnvMap()
envs := []Environment{}
for _, spec := range specs {
for _, env := range spec.Envs {
- if env.Label != label {
+ envTags := env.Tags
+ if env.Label != "" {
+ envTags = append(envTags, env.Label)
+ }
+ if !stringSlicesEq(tags, envTags) {
continue
}
- if _, ok := envToSuites[env]; !ok {
+
+ // Tags should not differ by ordering.
+ sortableTags := sort.StringSlice(tags)
+ sortableTags.Sort()
+ env.Tags = []string(sortableTags)
+
+ specs, ok := envToSuites.get(env)
+ if !ok {
envs = append(envs, env)
}
- envToSuites[env] = append(envToSuites[env], spec)
+ envToSuites.set(env, append(specs, spec))
}
}
shards := make([]*Shard, 0, len(envs))
for _, env := range envs {
- specs := envToSuites[env]
+ specs, _ := envToSuites.get(env)
sort.Slice(specs, func(i, j int) bool {
return specs[i].Test.Path < specs[i].Test.Path
})
@@ -59,3 +72,39 @@
}
return shards
}
+
+// Abstracts a mapping Environment -> []string, as Environment contains non-comparable
+// members (e.g., string slices), which makes it invalid for a map key.
+type envMap struct {
+ m map[string][]TestSpec
+}
+
+func newEnvMap() envMap {
+ return envMap{m: make(map[string][]TestSpec)}
+}
+
+func (em envMap) get(e Environment) ([]TestSpec, bool) {
+ specs, ok := em.m[fmt.Sprintf("%v", e)]
+ return specs, ok
+}
+
+func (em *envMap) set(e Environment, specs []TestSpec) {
+ em.m[fmt.Sprintf("%v", e)] = specs
+}
+
+func stringSlicesEq(s []string, t []string) bool {
+ if len(s) != len(t) {
+ return false
+ }
+ seen := make(map[string]int)
+ for i, _ := range s {
+ seen[s[i]] += 1
+ seen[t[i]] -= 1
+ }
+ for _, v := range seen {
+ if v != 0 {
+ return false
+ }
+ }
+ return true
+}
diff --git a/testsharder/shard_test.go b/testsharder/shard_test.go
index 4bc3ec5..e9ee45c 100644
--- a/testsharder/shard_test.go
+++ b/testsharder/shard_test.go
@@ -1,106 +1,123 @@
// 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_test
+package testsharder
import (
"fmt"
"reflect"
"testing"
-
- "fuchsia.googlesource.com/tools/testsharder"
)
// 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 []*testsharder.Shard) {
+func assertEqual(t *testing.T, expected, actual []*Shard) {
if !reflect.DeepEqual(expected, actual) {
- errMsg := "expected:\n"
+ errMsg := "\nexpected:\n"
for _, shard := range expected {
- errMsg += fmt.Sprintf("%+v,\n", shard)
+ errMsg += fmt.Sprintf("%v,\n", shard)
}
- errMsg += "actual:\n"
+ errMsg += "\nactual:\n"
for _, shard := range actual {
- errMsg += fmt.Sprintf("%+v,\n", shard)
+ errMsg += fmt.Sprintf("%v,\n", shard)
}
t.Fatalf(errMsg)
}
}
func TestMakeShards(t *testing.T) {
- test1 := testsharder.Test{
- Location: "/path/to/binary",
+ env1 := Environment{
+ Dimensions: DimensionSet{DeviceType: "QEMU"},
+ Tags: []string{},
}
- test2 := testsharder.Test{
- Location: "/path/to/binary2",
+ env2 := Environment{
+ Dimensions: DimensionSet{DeviceType: "NUC"},
+ Tags: []string{},
}
- env1 := testsharder.Environment{
- Dimensions: testsharder.DimensionSet{DeviceType: "QEMU"},
+ env3 := Environment{
+ Dimensions: DimensionSet{OS: "Linux"},
+ Tags: []string{},
}
- env2 := testsharder.Environment{
- Dimensions: testsharder.DimensionSet{DeviceType: "NUC"},
- }
- env3 := testsharder.Environment{
- Dimensions: testsharder.DimensionSet{DeviceType: "NUC"},
- Label: "LABEL",
- }
- spec1 := testsharder.TestSpec{
- Test: test1,
- Envs: []testsharder.Environment{env1},
- }
- spec2 := testsharder.TestSpec{
- Test: test1,
- Envs: []testsharder.Environment{env1},
- }
- spec3 := testsharder.TestSpec{
- Test: test2,
- Envs: []testsharder.Environment{env1, env2},
- }
- spec4 := testsharder.TestSpec{
- Test: test2,
- Envs: []testsharder.Environment{env1, env2, env3},
- }
- t.Run("Ensure that environments have names", func(t *testing.T) {
- // This will in turn ensure that the associated shards too have
- // names.
- if env1.Name() == "" {
- t.Fatalf("Environment\n%+v\n has an empty name", env1)
- }
- if env2.Name() == "" {
- t.Fatalf("Environment\n%+v\n has an empty name", env2)
+ t.Run("environments have nonempty names", func(t *testing.T) {
+ envs := []Environment{env1, env2, env3}
+ for _, env := range envs {
+ if env.Name() == "" {
+ t.Fatalf("Environment\n%+v\n has an empty name", env)
+ }
}
})
- t.Run("Ensure env shared", func(t *testing.T) {
- actual := testsharder.MakeShards([]testsharder.TestSpec{spec1, spec2, spec3}, "")
- expected := []*testsharder.Shard{
- // 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.
- {
- Name: env1.Name(),
- // Ensure that we actually specify the test _twice_, that is, don't
- // necessarily deduplicate tests for a shared environment.
- Tests: []testsharder.Test{test1, test1, test2},
- Env: env1,
+ spec := func(id int, envs ...Environment) TestSpec {
+ return TestSpec{
+ Test: Test{
+ Location: fmt.Sprintf("/path/to/test/%d", id),
},
- {
- Name: env2.Name(),
- Tests: []testsharder.Test{test2},
- Env: env2,
- },
+ Envs: envs,
}
+ }
+
+ shard := func(env Environment, ids ...int) *Shard {
+ var tests []Test
+ for _, id := range ids {
+ tests = append(tests, spec(id, env).Test)
+ }
+ return &Shard{
+ Name: env.Name(),
+ Tests: tests,
+ Env: env,
+ }
+ }
+
+ t.Run("tests of same environment are grouped", func(t *testing.T) {
+ actual := MakeShards(
+ []TestSpec{spec(1, env1, env2), spec(2, env1, env3), spec(3, env3)},
+ []string{},
+ )
+ expected := []*Shard{shard(env1, 1, 2), shard(env2, 1), shard(env3, 2, 3)}
assertEqual(t, expected, actual)
})
- t.Run("Ensure label respected", func(t *testing.T) {
- actual := testsharder.MakeShards([]testsharder.TestSpec{spec1, spec2, spec3, spec4}, env3.Label)
- expected := []*testsharder.Shard{
- {
- Name: env3.Name(),
- Tests: []testsharder.Test{test2},
- Env: env3,
+ t.Run("there is no deduplication of tests", func(t *testing.T) {
+ actual := MakeShards(
+ []TestSpec{spec(1, env1), spec(1, env1), spec(1, env1)},
+ []string{},
+ )
+ expected := []*Shard{shard(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(
+ []TestSpec{spec(1, env2, env3), spec(2, env1), spec(3, env3)},
+ []string{},
+ )
+ expected := []*Shard{shard(env2, 1), shard(env3, 1, 3), shard(env1, 2)}
+ assertEqual(t, expected, actual)
+ })
+
+ t.Run("tags are respected", func(t *testing.T) {
+ tagger := func(env Environment, tags ...string) Environment {
+ env2 := env
+ env2.Tags = tags
+ return env2
+ }
+
+ actual := MakeShards(
+ []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")),
},
+ []string{"A", "C"},
+ )
+ expected := []*Shard{
+ // "C", "A" and "A", "C" should define the same tags.
+ shard(tagger(env3, "A", "C"), 4, 5),
}
assertEqual(t, expected, actual)
})