tast: Validate control message ordering.
Make the tast command validate the ordering of control
messages that it receives from the test executable.
BUG=chromium:778389
TEST=added unit tests
Change-Id: Ie225738896b0784d8803f742feb7fe8c0d9f918f
Reviewed-on: https://chromium-review.googlesource.com/769107
Commit-Ready: Dan Erat <derat@chromium.org>
Tested-by: Dan Erat <derat@chromium.org>
Reviewed-by: Jason Clinton <jclinton@chromium.org>
diff --git a/src/chromiumos/tast/cmd/run/results.go b/src/chromiumos/tast/cmd/run/results.go
index 0586102..8f6f695 100644
--- a/src/chromiumos/tast/cmd/run/results.go
+++ b/src/chromiumos/tast/cmd/run/results.go
@@ -63,11 +63,12 @@
ctx context.Context
cfg *Config
- numTests int // total number of tests that are expected to run
- results []testResult // information about completed tests
- res *testResult // information about the currently-running test
- stage *timing.Stage // current test's timing stage
- crf copyAndRemoveFunc // function used to copy and remove files from DUT
+ runStart, runEnd time.Time // test-binary-reported times at which run started and ended
+ numTests int // total number of tests that are expected to run
+ results []testResult // information about completed tests
+ res *testResult // information about the currently-running test
+ stage *timing.Stage // current test's timing stage
+ crf copyAndRemoveFunc // function used to copy and remove files from DUT
}
func (r *resultsHandler) close() {
@@ -88,6 +89,10 @@
// handleRunStart handles RunStart control messages from test executables.
func (r *resultsHandler) handleRunStart(msg *control.RunStart) error {
+ if !r.runStart.IsZero() {
+ return errors.New("multiple RunStart messages")
+ }
+ r.runStart = msg.Time
r.numTests = msg.NumTests
r.setProgress("Starting testing")
return nil
@@ -107,6 +112,17 @@
// handleRunEnd handles RunEnd control messages from test executables.
func (r *resultsHandler) handleRunEnd(msg *control.RunEnd) error {
+ if r.runStart.IsZero() {
+ return errors.New("no RunStart message before RunEnd")
+ }
+ if r.res != nil {
+ return fmt.Errorf("got RunEnd message while %s still running", r.res.Name)
+ }
+ if !r.runEnd.IsZero() {
+ return errors.New("multiple RunEnd messages")
+ }
+ r.runEnd = msg.Time
+
if len(msg.LogDir) != 0 {
r.setProgress("Copying system logs")
if err := r.crf(msg.LogDir, filepath.Join(r.cfg.ResDir, systemLogsDir)); err != nil {
@@ -135,8 +151,11 @@
msg.Test.Name = msg.Name
}
+ if r.runStart.IsZero() {
+ return errors.New("no RunStart message before TestStart")
+ }
if r.res != nil {
- return fmt.Errorf("notified about start of %s while %s still running",
+ return fmt.Errorf("got TestStart message for %s while %s still running",
msg.Test.Name, r.res.Name)
}
if tl, ok := timing.FromContext(r.ctx); ok {
@@ -176,7 +195,7 @@
// handleTestError handles TestError control messages from test executables.
func (r *resultsHandler) handleTestError(msg *control.TestError) error {
if r.res == nil {
- return errors.New("notified about test error while no test was running")
+ return errors.New("got TestError message while no test was running")
}
te := msg.Error
@@ -195,7 +214,7 @@
// handleTestEnd handles TestEnd control messages from test executables.
func (r *resultsHandler) handleTestEnd(msg *control.TestEnd) error {
if r.res == nil || msg.Name != r.res.Name {
- return fmt.Errorf("notified about completion of not-started test %s", msg.Name)
+ return fmt.Errorf("got TestEnd message for not-started test %s", msg.Name)
}
if r.stage != nil {
r.stage.End()
@@ -413,7 +432,12 @@
return err
}
- // TODO(derat): Check that RunStart and RunEnd messages were received and that the
- // number of TestStart/TestEnd pairs matched the number specified in RunStart.
+ if rh.runEnd.IsZero() {
+ return errors.New("no RunEnd message")
+ }
+ if len(rh.results) != rh.numTests {
+ return fmt.Errorf("got results for %v test(s); expected %v", len(rh.results), rh.numTests)
+ }
+
return nil
}
diff --git a/src/chromiumos/tast/cmd/run/results_test.go b/src/chromiumos/tast/cmd/run/results_test.go
index 0bdd185..e816fc3 100644
--- a/src/chromiumos/tast/cmd/run/results_test.go
+++ b/src/chromiumos/tast/cmd/run/results_test.go
@@ -123,6 +123,81 @@
// TODO(derat): Check more output, including run errors.
}
+func TestValidateMessages(t *gotesting.T) {
+ tempDir := testutil.TempDir(t, "results_test.")
+ defer os.RemoveAll(tempDir)
+
+ for _, tc := range []struct {
+ desc string
+ msgs []interface{}
+ }{
+ {"no RunStart", []interface{}{
+ &control.RunEnd{time.Unix(1, 0), "", ""},
+ }},
+ {"multiple RunStart", []interface{}{
+ &control.RunStart{time.Unix(1, 0), 0},
+ &control.RunStart{time.Unix(2, 0), 0},
+ &control.RunEnd{time.Unix(3, 0), "", ""},
+ }},
+ {"no RunEnd", []interface{}{
+ &control.RunStart{time.Unix(1, 0), 0},
+ }},
+ {"multiple RunEnd", []interface{}{
+ &control.RunStart{time.Unix(1, 0), 0},
+ &control.RunEnd{time.Unix(2, 0), "", ""},
+ &control.RunEnd{time.Unix(3, 0), "", ""},
+ }},
+ {"num tests mismatch", []interface{}{
+ &control.RunStart{time.Unix(1, 0), 1},
+ &control.RunEnd{time.Unix(2, 0), "", ""},
+ }},
+ {"unfinished test", []interface{}{
+ &control.RunStart{time.Unix(1, 0), 1},
+ &control.TestStart{time.Unix(2, 0), "test1", testing.Test{Name: "test1"}},
+ &control.TestEnd{time.Unix(3, 0), "test1"},
+ &control.TestStart{time.Unix(4, 0), "test2", testing.Test{Name: "test2"}},
+ &control.RunEnd{time.Unix(5, 0), "", ""},
+ }},
+ {"TestStart before RunStart", []interface{}{
+ &control.TestStart{time.Unix(1, 0), "test1", testing.Test{Name: "test1"}},
+ &control.RunStart{time.Unix(2, 0), 1},
+ &control.TestEnd{time.Unix(3, 0), "test1"},
+ &control.RunEnd{time.Unix(4, 0), "", ""},
+ }},
+ {"TestError without TestStart", []interface{}{
+ &control.RunStart{time.Unix(1, 0), 0},
+ &control.TestError{time.Unix(2, 0), testing.Error{}},
+ &control.RunEnd{time.Unix(3, 0), "", ""},
+ }},
+ {"wrong TestEnd", []interface{}{
+ &control.RunStart{time.Unix(1, 0), 0},
+ &control.TestStart{time.Unix(2, 0), "test1", testing.Test{Name: "test1"}},
+ &control.TestEnd{time.Unix(3, 0), "test2"},
+ &control.RunEnd{time.Unix(3, 0), "", ""},
+ }},
+ {"no TestEnd", []interface{}{
+ &control.RunStart{time.Unix(1, 0), 2},
+ &control.TestStart{time.Unix(2, 0), "test1", testing.Test{Name: "test1"}},
+ &control.TestStart{time.Unix(3, 0), "test2", testing.Test{Name: "test2"}},
+ &control.TestEnd{time.Unix(4, 0), "test2"},
+ &control.RunEnd{time.Unix(5, 0), "", ""},
+ }},
+ } {
+ b := bytes.Buffer{}
+ mw := control.NewMessageWriter(&b)
+ for _, msg := range tc.msgs {
+ mw.WriteMessage(msg)
+ }
+ cfg := Config{
+ Logger: logging.NewSimple(&bytes.Buffer{}, 0, false),
+ ResDir: filepath.Join(tempDir, tc.desc),
+ }
+ if err := readTestOutput(context.Background(), &cfg, &b, noOpCopyAndRemove); err == nil {
+ t.Errorf("readTestOutput() didn't fail for %s", tc.desc)
+ }
+ }
+}
+
func TestReadTestOutputTimeout(t *gotesting.T) {
tempDir := testutil.TempDir(t, "results_test.")
defer os.RemoveAll(tempDir)