blob: e4cdb1daf6ec7a3b97ab43d884893d27567f0558 [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 main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"go.fuchsia.dev/fuchsia/tools/build"
"go.fuchsia.dev/fuchsia/tools/integration/testsharder"
"go.fuchsia.dev/fuchsia/tools/lib/color"
"go.fuchsia.dev/fuchsia/tools/lib/flagmisc"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
)
var (
buildDir string
outputFile string
mode testsharder.Mode = testsharder.Normal
tags flagmisc.StringsValue
modifiersPath string
targetTestCount int
targetDurationSecs int
perTestTimeoutSecs int
maxShardsPerEnvironment int
affectedTestsPath string
affectedTestsMaxAttempts int
affectedTestsMultiplyThreshold int
affectedOnly bool
realmLabel string
hermeticDeps bool
pave bool
)
func usage() {
fmt.Printf(`testsharder [flags]
Shards tests produced by a build.
For more information on the modes in which the testsharder may be run, see
https://pkg.go.dev/go.fuchsia.dev/fuchsia/tools/integration/testsharder#Mode.
`)
}
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.Var(&mode, "mode", "mode in which to run the testsharder (e.g., normal or restricted).")
flag.Var(&tags, "tag", "environment tags on which to filter; only the tests that match all tags will be sharded")
flag.StringVar(&modifiersPath, "modifiers", "", "path to the json manifest containing tests to modify")
flag.IntVar(&targetDurationSecs, "target-duration-secs", 0, "approximate duration that each shard should run in")
flag.IntVar(&maxShardsPerEnvironment, "max-shards-per-env", 8, "maximum shards allowed per environment. If <= 0, no max will be set")
// TODO(fxbug.dev/10456): Support different timeouts for different tests.
flag.IntVar(&perTestTimeoutSecs, "per-test-timeout-secs", 0, "per-test timeout, applied to all tests. If <= 0, no timeout will be set")
// Despite being a misnomer, this argument is still called -max-shard-size
// for legacy reasons. If it becomes confusing, we can create a new
// target_test_count fuchsia.proto field and do a soft transition with the
// recipes to start setting the renamed argument instead.
flag.IntVar(&targetTestCount, "max-shard-size", 0, "target number of tests per shard. If <= 0, will be ignored. Otherwise, tests will be placed into more, smaller shards")
flag.StringVar(&affectedTestsPath, "affected-tests", "", "path to a file containing names of tests affected by the change being tested. One test name per line.")
flag.IntVar(&affectedTestsMaxAttempts, "affected-tests-max-attempts", 2, "maximum attempts for each affected test. Only applied to tests that are not multiplied")
flag.IntVar(&affectedTestsMultiplyThreshold, "affected-tests-multiply-threshold", 0, "if there are <= this many tests in -affected-tests, they may be multplied "+
"(modified to run many times in a separate shard), but only be multiplied if allowed by certain constraints designed to minimize false rejections and bot demand.")
flag.BoolVar(&affectedOnly, "affected-only", false, "whether to create test shards for only the affected tests found in either the modifiers file or the affected-tests file.")
flag.StringVar(&realmLabel, "realm-label", "", "applies this realm label to the output sharded json file generated by testsharder. If empty, no realm label is applied.")
flag.BoolVar(&hermeticDeps, "hermetic-deps", false, "whether to add all the images and blobs used by the shard as dependencies")
flag.BoolVar(&pave, "pave", false, "whether the shards generated should pave or netboot fuchsia")
flag.Usage = usage
}
func main() {
flag.Parse()
l := logger.NewLogger(logger.ErrorLevel, color.NewColor(color.ColorAuto), os.Stdout, os.Stderr, "")
// testsharder is expected to complete quite quickly, so it's generally not
// useful to include timestamps in logs. File names can be helpful though.
l.SetFlags(logger.Lshortfile)
ctx := logger.WithLogger(context.Background(), l)
ctx, cancel := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT)
defer cancel()
if err := execute(ctx); err != nil {
logger.Fatalf(ctx, err.Error())
}
}
func execute(ctx context.Context) error {
if buildDir == "" {
return fmt.Errorf("must specify a Fuchsia build output directory")
}
// The package manifests generated by the build all use paths relative
// to the build directory, so testsharder should change its working
// directory to buildDir.
wd, err := os.Getwd()
if err != nil {
return err
}
if err := os.Chdir(buildDir); err != nil {
return err
}
defer os.Chdir(wd)
targetDuration := time.Duration(targetDurationSecs) * time.Second
if targetTestCount > 0 && targetDuration > 0 {
return fmt.Errorf("max-shard-size and target-duration-secs cannot both be set")
}
perTestTimeout := time.Duration(perTestTimeoutSecs) * time.Second
m, err := build.NewModules(buildDir)
if err != nil {
return err
}
if err = testsharder.ValidateTests(m.TestSpecs(), m.Platforms()); err != nil {
return err
}
opts := &testsharder.ShardOptions{
Mode: mode,
Tags: tags,
}
shards := testsharder.MakeShards(m.TestSpecs(), opts)
testDurations := testsharder.NewTestDurationsMap(m.TestDurations())
var modifiers []testsharder.TestModifier
if modifiersPath != "" {
modifiers, err = testsharder.LoadTestModifiers(modifiersPath)
if err != nil {
return err
}
}
if affectedTestsPath != "" {
affectedModifiers, err := testsharder.AffectedModifiers(m.TestSpecs(), affectedTestsPath, affectedTestsMaxAttempts, affectedTestsMultiplyThreshold)
if err != nil {
return err
}
modifiers = append(modifiers, affectedModifiers...)
}
shards, err = testsharder.ShardAffected(shards, modifiers, affectedOnly)
if err != nil {
return err
}
shards, err = testsharder.MultiplyShards(ctx, shards, modifiers, testDurations, targetDuration, targetTestCount)
if err != nil {
return err
}
shards = testsharder.WithTargetDuration(shards, targetDuration, targetTestCount, maxShardsPerEnvironment, testDurations)
if hermeticDeps {
for _, s := range shards {
testsharder.AddImageDeps(s, m.Images(), pave)
if err := s.CreatePackageRepo(); err != nil {
return err
}
}
}
if err := testsharder.ExtractDeps(shards, m.BuildDir()); err != nil {
return err
}
if realmLabel != "" {
testsharder.ApplyRealmLabel(shards, realmLabel)
}
if perTestTimeout > 0 {
testsharder.ApplyTestTimeouts(shards, perTestTimeout)
}
f := os.Stdout
if outputFile != "" {
var err error
f, err = os.Create(outputFile)
if err != nil {
return fmt.Errorf("unable to create %s: %v", outputFile, err)
}
defer f.Close()
}
encoder := json.NewEncoder(f)
encoder.SetIndent("", " ")
if err := encoder.Encode(&shards); err != nil {
return fmt.Errorf("failed to encode shards: %v", err)
}
return nil
}