| package log_parser |
| |
| import ( |
| "bufio" |
| "bytes" |
| "context" |
| "fmt" |
| "path/filepath" |
| "regexp" |
| "testing" |
| "time" |
| |
| "github.com/stretchr/testify/require" |
| "go.skia.org/infra/go/testutils" |
| "go.skia.org/infra/go/testutils/unittest" |
| "go.skia.org/infra/task_driver/go/td" |
| ) |
| |
| func TestRun(t *testing.T) { |
| unittest.MediumTest(t) |
| |
| // Create a dummy output format which just indicates results of steps. |
| steps := []struct { |
| Name string |
| Result td.StepResult |
| }{ |
| { |
| Name: "a", |
| Result: td.StepResultSuccess, |
| }, |
| { |
| Name: "b", |
| Result: td.StepResultFailure, |
| }, |
| { |
| Name: "c", |
| Result: td.StepResultSuccess, |
| }, |
| } |
| res := td.RunTestSteps(t, false, func(ctx context.Context) error { |
| return Run(ctx, ".", []string{"echo", "a b c"}, bufio.ScanWords, func(sm *StepManager, line string) error { |
| for _, stepData := range steps { |
| if stepData.Name == line { |
| s := sm.StartStep(td.Props(stepData.Name)) |
| if stepData.Result != td.StepResultSuccess { |
| s.Fail() |
| } |
| s.End() |
| return nil |
| } |
| } |
| return fmt.Errorf("No matching step for %q", line) |
| }) |
| }) |
| |
| // Verify that we got the expected structure. |
| require.Equal(t, 1, len(res.Steps)) |
| cmdStep := res.Steps[0] |
| require.Equal(t, "echo a b c", cmdStep.Name) |
| require.Equal(t, len(steps), len(cmdStep.Steps)) |
| for idx, actualStep := range cmdStep.Steps { |
| require.Equal(t, steps[idx].Name, actualStep.Name) |
| require.Equal(t, steps[idx].Result, actualStep.Result) |
| } |
| } |
| |
| // everyLineIsAStep is a function which may be passed to Run. It emits a new |
| // step for each line in the stdout stream. |
| func everyLineIsAStep(sm *StepManager, line string) error { |
| s := sm.CurrentStep() |
| if s != nil { |
| s.End() |
| } |
| sm.StartStep(td.Props(line)) |
| return nil |
| } |
| |
| func TestTimeout(t *testing.T) { |
| unittest.MediumTest(t) |
| // TODO(borenet): This test will not work on Windows. |
| |
| // Write a script to generate steps. |
| tmp, cleanup := testutils.TempDir(t) |
| defer cleanup() |
| slowSecondStep := filepath.Join(tmp, "script.sh") |
| testutils.WriteFile(t, slowSecondStep, `#!/bin/bash |
| echo "Step1" |
| echo "Step2" |
| sleep 10 |
| echo "Step3" |
| `) |
| |
| // The following Task Driver runs the above script, which takes longer |
| // than 10 seconds, with a timeout of 100 milliseconds. |
| res := td.RunTestSteps(t, false, func(ctx context.Context) error { |
| // Set up the timeout. |
| timeout := 100 * time.Millisecond |
| ctx, cancel := context.WithTimeout(ctx, timeout) |
| defer cancel() |
| start := time.Now() |
| defer func() { |
| elapsed := time.Now().Sub(start) |
| require.True(t, elapsed < 2*time.Second, fmt.Sprintf("Timeout is %s; command exited after %s", timeout, elapsed)) |
| }() |
| |
| // Run the script. It should hit the timeout during the second |
| // step. |
| return Run(ctx, ".", []string{slowSecondStep}, bufio.ScanLines, everyLineIsAStep) |
| }) |
| |
| // There should be a single root-level step, which is the execution of |
| // the script itself. |
| require.Equal(t, 1, len(res.Steps)) |
| require.Equal(t, td.StepResultFailure, res.Steps[0].Result) |
| |
| // We saw two log lines before the timeout, so we should have two steps. |
| // The second should be a failure because of the timeout. |
| require.Equal(t, 2, len(res.Steps[0].Steps)) |
| require.Equal(t, td.StepResultSuccess, res.Steps[0].Steps[0].Result) |
| require.Equal(t, td.StepResultFailure, res.Steps[0].Steps[1].Result) |
| |
| // The active steps should have received errors in their logs. |
| assertLogMatchesContent(t, res.Steps[0], logNameStderr, context.DeadlineExceeded.Error()+"\n") |
| assertLogMatchesContent(t, res.Steps[0].Steps[1], logNameStderr, context.DeadlineExceeded.Error()+"\n") |
| } |
| |
| // numberedStepsRe matches lines like "Step 1: Hello World" to produce steps. |
| var numberedStepsRe = regexp.MustCompile(`^Step \d: (.+)$`) |
| |
| // numberedStepsTokenHandler is a TokenHandler which uses numberedStepsRe. |
| var numberedStepsTokenHandler = RegexpTokenHandler(numberedStepsRe) |
| |
| // runPythonScript writes the given Python script to a temporary file and runs |
| // a Task Driver which uses log_parser.Run with the given TokenHandler. |
| func runPythonScript(t *testing.T, fn TokenHandler, script string) *td.StepReport { |
| // Write a script to generate steps. |
| tmp, cleanup := testutils.TempDir(t) |
| defer cleanup() |
| scriptPath := filepath.Join(tmp, "script.py") |
| testutils.WriteFile(t, scriptPath, script) |
| |
| // Run the Task Driver. |
| return td.RunTestSteps(t, false, func(ctx context.Context) error { |
| return Run(ctx, ".", []string{"python", "-u", scriptPath}, bufio.ScanLines, fn) |
| }) |
| } |
| |
| // assertLogMatchesContent verifies that the given log buffer contains the given |
| // lines. |
| func assertLogMatchesContent(t *testing.T, s *td.StepReport, logName, expect string) { |
| var b *bytes.Buffer |
| for _, data := range s.Data { |
| logData, ok := data.(*td.LogData) |
| if ok && logData.Name == logName { |
| log, ok := s.Logs[logData.Id] |
| if ok { |
| b = log |
| break |
| } |
| } |
| } |
| require.NotNil(t, b, "Failed to find log %q for step %q", logName, s.Name) |
| require.Equal(t, expect, b.String()) |
| } |
| |
| func TestLogs(t *testing.T) { |
| unittest.MediumTest(t) |
| |
| // This script writes log output which implies two sub-steps. We expect |
| // the numberedStepsTokenHandler to actually emit these two steps, and |
| // we expect that all log output which follows "Step X" lines to be |
| // attributed to step X. |
| res := runPythonScript(t, numberedStepsTokenHandler, ` |
| from __future__ import print_function |
| import sys |
| import time |
| |
| print('Step 1: Do a thing') |
| print('log for step 1') |
| print('... more') |
| print('Step 2: Do another thing') |
| print('inside step 2') |
| print('err in step 2', file=sys.stderr) |
| print('more err in step 2', file=sys.stderr) |
| `) |
| |
| // Run should produce one root-level step, which is the execution of the |
| // command itself. This step should have all of the raw output of the |
| // command. |
| require.Equal(t, 1, len(res.Steps)) |
| base := res.Steps[0] |
| require.Equal(t, td.StepResultSuccess, base.Result) |
| assertLogMatchesContent(t, base, logNameStdout, `Step 1: Do a thing |
| log for step 1 |
| ... more |
| Step 2: Do another thing |
| inside step 2 |
| `) |
| assertLogMatchesContent(t, base, logNameStderr, `err in step 2 |
| more err in step 2 |
| `) |
| |
| // Ensure that we ended up with both of the expected sub-steps, each |
| // with the correct log output. |
| // NOTE: We'd like to verify that the stderr lines went to step2 and not |
| // step1 below, but due to the racy nature of the two streams described |
| // in the docstring for Run(), we cannot guarantee that stderr lines are |
| // attached to the correct sub-step. |
| require.Equal(t, 2, len(base.Steps)) |
| step1 := base.Steps[0] |
| require.Equal(t, "Do a thing", step1.Name) |
| require.Equal(t, td.StepResultSuccess, step1.Result) |
| assertLogMatchesContent(t, step1, logNameStdout, `Step 1: Do a thing |
| log for step 1 |
| ... more |
| `) |
| |
| step2 := base.Steps[1] |
| require.Equal(t, "Do another thing", step2.Name) |
| require.Equal(t, td.StepResultSuccess, step2.Result) |
| assertLogMatchesContent(t, step2, logNameStdout, `Step 2: Do another thing |
| inside step 2 |
| `) |
| } |