blob: 3c02ad5a8545fc61a5b09db6bddd21cae0e1591f [file] [edit]
// Copyright 2020 The Chromium OS 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 planner
import (
"context"
"errors"
"fmt"
"path/filepath"
"chromiumos/tast/internal/logging"
"chromiumos/tast/internal/protocol"
"chromiumos/tast/internal/testcontext"
"chromiumos/tast/internal/testing"
"chromiumos/tast/internal/timing"
)
// fixtureStatus represents a status of a fixture, as well as that of a fixture
// stack. See comments around FixtureStack for details.
type fixtureStatus int
const (
statusRed fixtureStatus = iota // fixture is not set up or torn down
statusGreen // fixture is set up
statusYellow // fixture is set up but last reset failed
)
// String converts fixtureStatus to a string for debugging.
func (s fixtureStatus) String() string {
switch s {
case statusRed:
return "red"
case statusGreen:
return "green"
case statusYellow:
return "yellow"
default:
return fmt.Sprintf("unknown(%d)", int(s))
}
}
// FixtureStack maintains a stack of fixtures and their states.
//
// A fixture stack corresponds to a path from the root of a fixture tree. As we
// traverse a fixture tree, a new child fixture is pushed to the stack by Push,
// or a fixture of the lowest level is popped from the stack by Pop, calling
// their SetUp/TearDown methods as needed.
//
// A fixture is in exactly one of three statuses: green, yellow, and red.
//
// - A fixture is green if it has been successfully set up and never failed to
// reset so far.
// - A fixture is yellow if it has been successfully set up but failed to
// reset.
// - A fixture is red if it has been torn down.
//
// The following diagram illustrates the status transition of a fixture:
//
// OK
// +-------------+
// v |
// +-----+ SetUp OK +-------+ Reset | Fail +--------+
// | red |---------+----->| green |---------+------->| yellow |
// +-----+ | +-------+ +--------+
// ^ ^ ^ | Fail | |
// | | +----------+ | TearDown | TearDown
// | +-----------------------+ |
// +----------------------------------------------------+
//
// FixtureStack maintains the following invariants about fixture statuses:
//
// 1. When there is a yellow fixture in the stack, no other fixtures are red.
// 2. When there is no yellow fixture in the stack, there is an integer k
// (0 <= k <= n; n is the number of fixtures in the stack) where the first
// k fixtures from the bottom of the stack are green and the remaining
// fixtures are red.
//
// A fixture stack can be also in exactly one of three statuses: green, yellow,
// and red.
//
// - A fixture stack is green if all fixtures in the stack are green.
// - A fixture stack is yellow if any fixture in the stack is yellow.
// - A fixture stack is red if any fixture in the stack is red.
//
// An empty fixture stack is green. When SetUp fails on pushing a new fixture
// to an green stack, the stack becomes red until the failed fixture is popped
// from the stack. It is still possible to push more fixtures to the stack, but
// SetUp is not called for those fixtures, and the stack remains red. This
// behavior allows continuing to traverse a fixture tree despite SetUp failures.
// When Reset fails between tests, the stack becomes yellow until the
// bottom-most yellow fixture is popped from the stack. It is not allowed to
// push more fixtures to the stack in this case.
//
// The following diagram illustrates the status transition of a fixture stack:
//
// OK
// +------------+ +-+ Push
// v | v |
// +--------+ Fail Reset +-------+ Push | Fail +-----+
// +->| yellow |<-------+---------| green |--------+------->| red |<-+
// | +--------+ | +-------+ +-----+ |
// | | | OK ^ ^ ^ | |
// | | Pop | | | | | Pop |
// | | +-----------+ | | | |
// +-------+--------------------------+ +----------------------+-----+
//
// A fixture stack is clean or dirty. A stack is initially clean. A clean stack
// can be marked dirty with MarkDirty. It is an error to call MarkDirty on a
// dirty stack. The dirty flag can be cleared by Reset. MarkDirty can be called
// before running a test to make sure Reset is called for sure between tests.
type FixtureStack struct {
cfg *Config
out OutputStream
stack []*statefulFixture // fixtures on a traverse path, root to leaf
dirty bool
}
// NewFixtureStack creates a new empty fixture stack.
func NewFixtureStack(cfg *Config, out OutputStream) *FixtureStack {
return &FixtureStack{cfg: cfg, out: out}
}
// Status returns the current status of the fixture stack.
func (st *FixtureStack) Status() fixtureStatus {
for _, f := range st.stack {
if s := f.Status(); s != statusGreen {
return s
}
}
return statusGreen
}
// Errors returns errors to be reported for tests depending on this fixture
// stack.
//
// If there is no red fixture in the stack, an empty slice is returned.
// Otherwise, this function returns a slice of error messages to be reported
// for tests depending on the fixture stack. An error message is formatted in
// the following way:
//
// [Fixture failure] (fixture name): (original error message)
func (st *FixtureStack) Errors() []*protocol.Error {
for _, f := range st.stack {
if f.Status() == statusRed {
return f.Errors()
}
}
return nil
}
// Val returns the fixture value of the top fixture.
//
// If the fixture stack is empty or red, it returns nil.
func (st *FixtureStack) Val() interface{} {
if len(st.stack) == 0 {
return nil
}
if st.Status() == statusRed {
return nil
}
return st.top().Val()
}
// Push adds a new fixture to the top of the fixture stack.
//
// If the current fixture stack is green, the new fixture's SetUp is called,
// and the resulting fixture stack is either green or red.
//
// If the current fixture stack is red, the new fixture's SetUp is not called
// and the resulting fixture stack is red.
//
// It is an error to call Push for a yellow fixture stack.
func (st *FixtureStack) Push(ctx context.Context, fixt *testing.FixtureInstance) error {
status := st.Status()
if status == statusYellow {
return errors.New("BUG: fixture must not be pushed to a yellow stack")
}
var outDir string
if fixt.Name != "" {
dir, err := createEntityOutDir(st.cfg.OutDir, fixt.Name)
if err != nil {
return err
}
outDir = dir
}
ce := &testcontext.CurrentEntity{
OutDir: outDir,
ServiceDeps: fixt.ServiceDeps,
}
ei := fixt.EntityProto()
fout := newEntityOutputStream(st.out, ei)
ctx = testing.NewContext(ctx, ce, logging.NewFuncSink(func(msg string) { fout.Log(msg) }))
rcfg := &testing.RuntimeConfig{
DataDir: filepath.Join(st.cfg.DataDir, testing.RelativeDataDir(fixt.Pkg)),
OutDir: outDir,
Vars: st.cfg.Features.GetInfra().GetVars(),
CloudStorage: testing.NewCloudStorage(st.cfg.Devservers, st.cfg.TLWServer, st.cfg.DUTName, st.cfg.BuildArtifactsURL),
RemoteData: st.cfg.RemoteData,
FixtValue: st.Val(),
FixtCtx: ctx,
}
root := testing.NewEntityRoot(ce, fixt.Constraints(), rcfg, fout)
f := newStatefulFixture(fixt, root, fout, st.cfg)
st.stack = append(st.stack, f)
if status == statusGreen {
if err := st.top().RunSetUp(ctx); err != nil {
return err
}
}
return nil
}
// Pop removes the top-most fixture from the fixture stack.
//
// If the top-most fixture is green or yellow, its TearDown method is called.
func (st *FixtureStack) Pop(ctx context.Context) error {
f := st.top()
st.stack = st.stack[:len(st.stack)-1]
if f.Status() != statusRed {
if err := f.RunTearDown(ctx); err != nil {
return err
}
}
return nil
}
// Reset resets all fixtures on the stack if the stack is green.
//
// Reset clears the dirty flag of the stack.
//
// Reset is called in bottom-to-top order. If any fixture fails to reset, the
// fixture and fixture stack becomes yellow.
//
// Unless the fixture execution is abandoned, this method returns success even
// if Reset returns an error and the fixture becomes yellow. Callers should
// check Status after calling Reset to see if they can proceed to pushing more
// fixtures on the stack.
//
// If the stack is red, Reset does nothing. If the stack is yellow, it is an
// error to call this method.
func (st *FixtureStack) Reset(ctx context.Context) error {
st.dirty = false
switch st.Status() {
case statusGreen:
case statusRed:
return nil
case statusYellow:
return errors.New("BUG: Reset called for a yellow fixture stack")
}
for _, f := range st.stack {
if err := f.RunReset(ctx); err != nil {
return err
}
switch f.Status() {
case statusGreen:
case statusRed:
return errors.New("BUG: fixture is red after calling Reset")
case statusYellow:
return nil
}
}
return nil
}
// PreTest runs PreTests on the fixtures.
// It returns a post test hook that runs PostTests on the fixtures.
func (st *FixtureStack) PreTest(ctx context.Context, troot *testing.TestEntityRoot) (func(context.Context, *testing.TestEntityRoot) error, error) {
if status := st.Status(); status != statusGreen {
return nil, fmt.Errorf("BUG: PreTest called for a %v fixture", status)
}
var postTests []func(context.Context, *testing.TestEntityRoot) error
for _, f := range st.stack {
pt, err := f.RunPreTest(ctx, troot)
if err != nil {
return nil, err
}
postTests = append(postTests, pt)
}
return func(ctx context.Context, troot *testing.TestEntityRoot) error {
if status := st.Status(); status != statusGreen {
return fmt.Errorf("BUG: PostTest called for a %v fixture", status)
}
for i := len(postTests) - 1; i >= 0; i-- {
if err := postTests[i](ctx, troot); err != nil {
return err
}
}
return nil
}, nil
}
// MarkDirty marks the fixture stack dirty. It returns an error if the stack is
// already dirty.
//
// The dirty flag can be cleared by calling Reset. MarkDirty can be called
// before running a test to make sure Reset is called for sure between tests.
func (st *FixtureStack) MarkDirty() error {
if st.dirty {
return errors.New("BUG: MarkDirty called for a dirty stack")
}
st.dirty = true
return nil
}
// top returns the stateful fixture at the top of the stack.
func (st *FixtureStack) top() *statefulFixture {
if len(st.stack) == 0 {
panic("BUG: top called for an empty stack")
}
return st.stack[len(st.stack)-1]
}
// statefulFixture holds a fixture and some extra variables tracking its states.
type statefulFixture struct {
cfg *Config
fixt *testing.FixtureInstance
root *testing.EntityRoot
fout *entityOutputStream
status fixtureStatus
errs []*protocol.Error
val interface{} // val returned by SetUp
}
// newStatefulFixture creates a new statefulFixture.
func newStatefulFixture(fixt *testing.FixtureInstance, root *testing.EntityRoot, fout *entityOutputStream, cfg *Config) *statefulFixture {
return &statefulFixture{
cfg: cfg,
fixt: fixt,
root: root,
fout: fout,
status: statusRed,
}
}
// Name returns the name of the fixture.
func (f *statefulFixture) Name() string {
return f.fixt.Name
}
// Status returns the current status of the fixture.
func (f *statefulFixture) Status() fixtureStatus {
return f.status
}
// Errors returns errors to be reported for tests depending on the fixture.
//
// If SetUp has not been called for the fixture, an empty slice is returned.
// Otherwise, this function returns a slice of error messages to be reported
// for tests depending on the fixture. An error message is formatted in the
// following way:
//
// [Fixture failure] (fixture name): (original error message)
func (f *statefulFixture) Errors() []*protocol.Error {
return f.errs
}
// Val returns the fixture value obtained on setup.
func (f *statefulFixture) Val() interface{} {
return f.val
}
// RunSetUp calls SetUp of the fixture with a proper context and timeout.
func (f *statefulFixture) RunSetUp(ctx context.Context) error {
if f.Status() != statusRed {
return errors.New("BUG: RunSetUp called for a non-red fixture")
}
ctx = f.root.NewContext(ctx)
s := f.root.NewFixtState()
name := fmt.Sprintf("%s:SetUp", f.fixt.Name)
f.fout.Start(s.OutDir())
var val interface{}
if err := safeCall(ctx, name, f.fixt.SetUpTimeout, f.cfg.GracePeriod(), errorOnPanic(s), func(ctx context.Context) {
entityPreCheck(f.fixt.Data, s)
if s.HasError() {
return
}
val = f.fixt.Impl.SetUp(ctx, s)
}); err != nil {
return err
}
fixtName := f.fixt.Name
if fixtName == "" {
fixtName = f.cfg.StartFixtureName
}
f.errs = rewriteErrorsForTest(f.fout.Errors(), fixtName)
if len(f.errs) > 0 {
// TODO(crbug.com/1127169): Support timing log.
f.fout.End(nil, timing.NewLog())
return nil
}
f.status = statusGreen
f.val = val
return nil
}
// RunTearDown calls TearDown of the fixture with a proper context and timeout.
func (f *statefulFixture) RunTearDown(ctx context.Context) error {
if f.Status() == statusRed {
return errors.New("BUG: RunTearDown called for a red fixture")
}
ctx = f.root.NewContext(ctx)
s := f.root.NewFixtState()
name := fmt.Sprintf("%s:TearDown", f.fixt.Name)
if err := safeCall(ctx, name, f.fixt.TearDownTimeout, f.cfg.GracePeriod(), errorOnPanic(s), func(ctx context.Context) {
f.fixt.Impl.TearDown(ctx, s)
}); err != nil {
return err
}
// TODO(crbug.com/1127169): Support timing log.
f.fout.End(nil, timing.NewLog())
f.status = statusRed
f.val = nil
return nil
}
// RunReset calls Reset of the fixture with a proper context and timeout.
func (f *statefulFixture) RunReset(ctx context.Context) error {
if f.Status() != statusGreen {
return errors.New("BUG: RunReset called for a non-green fixture")
}
ctx = f.root.NewContext(ctx)
name := fmt.Sprintf("%s:Reset", f.fixt.Name)
var resetErr error
onPanic := func(val interface{}) {
resetErr = fmt.Errorf("panic: %v", val)
}
if err := safeCall(ctx, name, f.fixt.ResetTimeout, f.cfg.GracePeriod(), onPanic, func(ctx context.Context) {
resetErr = f.fixt.Impl.Reset(ctx)
}); err != nil {
return err
}
if resetErr != nil {
f.status = statusYellow
f.fout.Log(fmt.Sprintf("Fixture failed to reset: %v; recovering", resetErr))
}
return nil
}
// RunPreTest runs PreTest on the fixture. It returns a post test hook.
func (f *statefulFixture) RunPreTest(ctx context.Context, troot *testing.TestEntityRoot) (func(context.Context, *testing.TestEntityRoot) error, error) {
if status := f.Status(); status != statusGreen {
return nil, fmt.Errorf("BUG: RunPreTest called for a %v fixture", status)
}
doNothing := func(context.Context, *testing.TestEntityRoot) error { return nil }
if troot.HasError() {
// If errors are already reported, PreTest and PostTest will not run.
return doNothing, nil
}
ctx = f.newTestContext(ctx, troot, troot.LogSink())
s := troot.NewFixtTestState(ctx)
name := fmt.Sprintf("%s:PreTest", f.fixt.Name)
if err := safeCall(ctx, name, f.fixt.PreTestTimeout, defaultGracePeriod, errorOnPanic(s), func(ctx context.Context) {
f.fixt.Impl.PreTest(ctx, s)
}); err != nil {
return nil, err
}
if troot.HasError() {
// If errors are reported in PreTest, PostTest will not run.
return doNothing, nil
}
return f.runPostTest, nil
}
func (f *statefulFixture) runPostTest(ctx context.Context, troot *testing.TestEntityRoot) error {
if status := f.Status(); status != statusGreen {
return fmt.Errorf("BUG: RunPostTest called for a %v fixture", status)
}
ctx = f.newTestContext(ctx, troot, troot.LogSink())
s := troot.NewFixtTestState(ctx)
name := fmt.Sprintf("%s:PostTest", f.fixt.Name)
return safeCall(ctx, name, f.fixt.PostTestTimeout, defaultGracePeriod, errorOnPanic(s), func(ctx context.Context) {
f.fixt.Impl.PostTest(ctx, s)
})
}
// newTestContext returns a Context to be passed to PreTest/PostTest of a fixture.
func (f *statefulFixture) newTestContext(ctx context.Context, troot *testing.TestEntityRoot, sink logging.Sink) context.Context {
ce := &testcontext.CurrentEntity{
// OutDir is from the test so that test hooks can save files just like tests.
OutDir: troot.OutDir(),
// ServiceDeps is from the fixture so that test hooks can call gRPC services
// without relying on what tests declare in ServiceDeps.
ServiceDeps: f.fixt.ServiceDeps,
// SoftwareDeps is unavailable because fixtures can't declare software dependencies.
HasSoftwareDeps: false,
}
return testing.NewContext(ctx, ce, sink)
}
// rewriteErrorsForTest rewrites error messages reported by a fixture to be
// suitable for reporting for tests depending on the fixture.
func rewriteErrorsForTest(errs []*protocol.Error, fixtureName string) []*protocol.Error {
newErrs := make([]*protocol.Error, len(errs))
for i, e := range errs {
newErrs[i] = &protocol.Error{
Reason: fmt.Sprintf("[Fixture failure] %s: %s", fixtureName, e.GetReason()),
Location: e.GetLocation(),
}
}
return newErrs
}