blob: 90a80db536c1587b4f3262d2b6ea71406f828c9d [file] [log] [blame]
package td
import (
"context"
"errors"
"fmt"
"os"
"strings"
"testing"
"github.com/stretchr/testify/require"
"go.skia.org/infra/go/deepequal/assertdeep"
"go.skia.org/infra/go/exec"
"go.skia.org/infra/go/testutils/unittest"
"go.skia.org/infra/go/util"
)
// mockExec mocks out subprocesses named "true" with a success result and all
// others with a failure. Returns the new context and a counter indicating how
// many times the run function was called.
func mockExec(ctx context.Context) (context.Context, *int) {
mockRun := &exec.CommandCollector{}
runCount := 0
mockRun.SetDelegateRun(func(ctx context.Context, cmd *exec.Command) error {
runCount++
if cmd.Name == "true" {
return nil
}
return errors.New("Command exited with exit status 1: ")
})
return WithExecRunFn(ctx, mockRun.Run), &runCount
}
func TestDefer(t *testing.T) {
unittest.MediumTest(t)
// Verify that we handle panics properly.
res := RunTestSteps(t, true, func(ctx context.Context) error {
panic("halp")
})
require.Equal(t, res.Result, STEP_RESULT_EXCEPTION)
res = RunTestSteps(t, true, func(ctx context.Context) error {
return Do(ctx, nil, func(ctx context.Context) error {
return Do(ctx, nil, func(ctx context.Context) error {
panic("halp")
})
})
})
got := 0
res.Recurse(func(s *StepReport) bool {
require.Equal(t, s.Result, STEP_RESULT_EXCEPTION)
require.Equal(t, 1, len(s.Exceptions))
require.Equal(t, "Caught panic: halp", s.Exceptions[0])
got++
return true
})
require.Equal(t, 3, got)
// Verify that our defer works properly.
var id string
res = RunTestSteps(t, false, func(ctx context.Context) error {
// This is an example of a function which runs as a step.
return Do(ctx, Props("parent"), func(ctx context.Context) error {
return func(ctx context.Context) error {
ctx = StartStep(ctx, Props("should fail"))
defer EndStep(ctx)
// Actual work would go here.
id = getCtx(ctx).step.Id
err := fmt.Errorf("whoops")
return FailStep(ctx, err)
}(ctx)
})
})
// The top-level step should not have inherited the sub-step result,
// since we did not call FailStep for "parent".
require.Equal(t, STEP_RESULT_SUCCESS, res.Result)
// Find the actual failed step, ensure that it has the error.
s, err := res.findStep(id)
require.NoError(t, err)
require.Equal(t, STEP_RESULT_FAILURE, s.Result)
require.Equal(t, 1, len(s.Errors))
require.Equal(t, "whoops", s.Errors[0])
}
func TestExec(t *testing.T) {
unittest.MediumTest(t)
// Basic tests around executing subprocesses.
_ = RunTestSteps(t, false, func(ctx context.Context) error {
mockExecCtx, counter := mockExec(ctx)
// Simple command.
_, err := exec.RunSimple(mockExecCtx, "true")
require.NoError(t, err)
require.Equal(t, 1, *counter)
// Verify that we get an error if the command fails.
_, err = exec.RunCwd(mockExecCtx, ".", "false")
require.Contains(t, err.Error(), "Command exited with exit status 1: ")
require.Equal(t, 2, *counter)
// Ensure that we collect stdout.
out, err := exec.RunCwd(ctx, ".", "python", "-c", "print 'hello world'")
require.NoError(t, err)
require.True(t, strings.Contains(out, "hello world"))
require.Equal(t, 2, *counter) // Not using the mock for this test case.
// Ensure that we collect stdout and stderr.
out, err = exec.RunCwd(ctx, ".", "python", "-c", "import sys; print 'stdout'; print >> sys.stderr, 'stderr'")
require.NoError(t, err)
require.True(t, strings.Contains(out, "stdout"))
require.True(t, strings.Contains(out, "stderr"))
require.Equal(t, 2, *counter) // Not using the mock for this test case.
return nil
})
}
func TestFatal(t *testing.T) {
unittest.SmallTest(t)
err := errors.New("FATAL")
checkErr := func(s *StepReport) {
require.Equal(t, 1, len(s.Errors))
require.EqualError(t, err, s.Errors[0])
}
checkErrs := func(s *StepReport) {
checkErr(s)
s.Recurse(func(s *StepReport) bool {
checkErr(s)
return true
})
}
checkExc := func(s *StepReport) {
require.Equal(t, 1, len(s.Exceptions))
require.EqualError(t, err, s.Exceptions[0])
}
checkExcs := func(s *StepReport) {
checkExc(s)
s.Recurse(func(s *StepReport) bool {
checkExc(s)
return true
})
}
// When Fatal is called in a non-infra step, all parent steps get an error.
s := RunTestSteps(t, true, func(ctx context.Context) error {
return Do(ctx, nil, func(ctx context.Context) error {
return Do(ctx, Props("non-infra step"), func(ctx context.Context) error {
Fatal(ctx, err)
return nil
})
})
})
checkErrs(s)
// When Fatal is called in an infra step, all parent steps get an exception.
s = RunTestSteps(t, true, func(ctx context.Context) error {
return Do(ctx, nil, func(ctx context.Context) error {
return Do(ctx, Props("infra step").Infra(), func(ctx context.Context) error {
Fatal(ctx, err)
return nil
})
})
})
checkExcs(s)
// Check the case where we call Fatal() after a failed subprocess but
// still want to perform deferred cleanup.
ranCleanup := false
s = RunTestSteps(t, true, func(ctx context.Context) error {
defer func() {
util.LogErr(Do(ctx, Props("cleanup").Infra(), func(ctx context.Context) error {
ranCleanup = true
return nil
}))
}()
ctx, _ = mockExec(ctx)
if _, err := exec.RunSimple(ctx, "false"); err != nil {
Fatal(ctx, err)
return err
}
return nil
})
require.Equal(t, 1, len(s.Errors))
require.Contains(t, s.Errors[0], "Command exited with exit status 1: ")
require.True(t, ranCleanup)
// Check the case where we call Fatal() after an infra step failed whose
// parent is not an infra step.
s = RunTestSteps(t, true, func(ctx context.Context) error {
return Do(ctx, Props("non-infra step"), func(ctx context.Context) error {
if err := Do(ctx, Props("infra step").Infra(), func(ctx context.Context) error {
return errors.New("Infra Failure")
}); err != nil {
Fatal(ctx, err)
}
return nil
})
})
require.Equal(t, 1, len(s.Exceptions))
require.Equal(t, "Infra Failure", s.Exceptions[0])
}
func TestEnv(t *testing.T) {
unittest.MediumTest(t)
// Verify that each step inherits the environment of its parent.
s := RunTestSteps(t, false, func(ctx context.Context) error {
return Do(ctx, Props("a").Env([]string{"a=a"}), func(ctx context.Context) error {
return Do(ctx, Props("b").Env([]string{"b=b"}), func(ctx context.Context) error {
_, err := exec.RunCommand(ctx, &exec.Command{
Name: "python",
Args: []string{"-c", "print 'hello world'"},
Env: []string{"c=c"},
})
return err
})
})
})
var leaf *StepReport
s.Recurse(func(s *StepReport) bool {
if len(s.Steps) == 0 {
leaf = s
return false
}
return true
})
require.NotNil(t, leaf)
expect := MergeEnv(os.Environ(), BASE_ENV)
expect = append(expect, "a=a", "b=b", "c=c")
assertdeep.Equal(t, expect, leaf.StepProperties.Environ)
var data *ExecData
for _, d := range leaf.Data {
ed, ok := d.(*ExecData)
if ok {
data = ed
break
}
}
require.NotNil(t, data)
assertdeep.Equal(t, data.Env, expect)
}
func TestEnvMerge(t *testing.T) {
unittest.SmallTest(t)
tc := []struct {
a []string
b []string
expect []string
}{
// Unrelated variables both show up.
{
expect: []string{"a=a", "b=b"},
a: []string{"a=a"},
b: []string{"b=b"},
},
// The second env takes precedence over the first.
{
expect: []string{"k=v2"},
a: []string{"k=v1"},
b: []string{"k=v2"},
},
// PATH gets special treatment.
// If only one is specified, it gets preserved.
{
expect: []string{"PATH=p2"},
a: []string{},
b: []string{"PATH=p2"},
},
{
expect: []string{"PATH=p1"},
a: []string{"PATH=p1"},
b: []string{},
},
// The second env takes precedence over the first.
{
expect: []string{"PATH=p2"},
a: []string{"PATH=p1"},
b: []string{"PATH=p2"},
},
// ... even if the second env defines it to be empty.
{
expect: []string{"PATH="},
a: []string{"PATH=p1"},
b: []string{"PATH="},
},
// If provided, PATH_PLACEHOLDER gets replaced by PATH from the first.
{
expect: []string{"PATH=p1:p2"},
a: []string{"PATH=p1"},
b: []string{fmt.Sprintf("PATH=%s:p2", PATH_PLACEHOLDER)},
},
{
expect: []string{"PATH=p2:p1"},
a: []string{"PATH=p1"},
b: []string{fmt.Sprintf("PATH=p2:%s", PATH_PLACEHOLDER)},
},
// There's no good reason to do this, but it would work.
{
expect: []string{"PATH=p1:p1"},
a: []string{"PATH=p1"},
b: []string{fmt.Sprintf("PATH=%s:%s", PATH_PLACEHOLDER, PATH_PLACEHOLDER)},
},
}
for _, c := range tc {
require.Equal(t, c.expect, MergeEnv(c.a, c.b))
}
}
func TestEnvInheritance(t *testing.T) {
unittest.SmallTest(t)
// Set up exec mock and expectations.
runCount := 0
expect := MergeEnv(os.Environ(), BASE_ENV)
expect = append(expect, "a=a", "b=b", "c=c", "d=d")
mockRun := &exec.CommandCollector{}
mockRun.SetDelegateRun(func(ctx context.Context, cmd *exec.Command) error {
runCount++
require.Equal(t, expect, cmd.Env)
return nil
})
// Verify that environments are inherited properly.
require.Equal(t, 0, runCount)
s := RunTestSteps(t, false, func(ctx context.Context) error {
ctx = WithExecRunFn(ctx, mockRun.Run)
return Do(ctx, Props("a").Env([]string{"a=a", "b=a"}), func(ctx context.Context) error {
ctx = WithEnv(ctx, []string{"b=b", "c=b"})
return Do(ctx, Props("c").Env([]string{"c=c", "d=c"}), func(ctx context.Context) error {
_, err := exec.RunCommand(ctx, &exec.Command{
Name: "true",
Env: []string{"d=d"},
})
return err
})
})
})
require.Equal(t, 1, runCount)
var leaf *StepReport
s.Recurse(func(s *StepReport) bool {
if len(s.Steps) == 0 {
leaf = s
return false
}
return true
})
require.NotNil(t, leaf)
assertdeep.Equal(t, expect, leaf.StepProperties.Environ)
var data *ExecData
for _, d := range leaf.Data {
ed, ok := d.(*ExecData)
if ok {
data = ed
break
}
}
require.NotNil(t, data)
assertdeep.Equal(t, data.Env, expect)
// Verify that multiple invocations of WithEnv get merged.
require.Equal(t, 1, runCount)
s = RunTestSteps(t, false, func(ctx context.Context) error {
ctx = WithExecRunFn(ctx, mockRun.Run)
ctx = WithEnv(ctx, []string{"a=a", "b=a"})
ctx = WithEnv(ctx, []string{"b=b", "c=b"})
ctx = WithEnv(ctx, []string{"c=c", "d=c"})
_, err := exec.RunCommand(ctx, &exec.Command{
Name: "true",
Env: []string{"d=d"},
})
return err
})
require.Equal(t, 2, runCount)
leaf = nil
s.Recurse(func(s *StepReport) bool {
if len(s.Steps) == 0 {
leaf = s
return false
}
return true
})
require.NotNil(t, leaf)
assertdeep.Equal(t, expect, leaf.StepProperties.Environ)
data = nil
for _, d := range leaf.Data {
ed, ok := d.(*ExecData)
if ok {
data = ed
break
}
}
require.NotNil(t, data)
assertdeep.Equal(t, data.Env, expect)
}