Skip to content

Commit e47e47d

Browse files
authored
Fix empty diff being returned when single *cached* step had to be executed (#567)
This PR fixes a bug in the step-wise caching logic that @eseliger and I ran into last week. ### Problem When no cached result for the *complete* task was found, but a cached result for a single step and that step was the only left to execute then an empty diff was returned. ### How to reproduce 1. Execute a batch spec with the following steps ```yaml steps: - run: echo "this is step 1" >> README.txt container: alpine:3 - run: echo "this is step 2" >> README.md container: alpine:3 ``` the complete results and the results for each step are now cached. 2. Update the batch spec and re-execute: ```yaml steps: - run: echo "this is step 1" >> README.txt container: alpine:3 ``` This will produce an empty diff because the `Coordinator` did not find a cached result for the complete task/batch spec, but for the first step. `runSteps` didn't handle this case though, when only a single step had to be executed but that was also cached. ### The fix Take a look at my comments in the diffs here in GitHub to see what the fix is. Spoiler: it's just a condition and an early exit to handle the case of "cached step result == the only step to execute". In order to get to this fix though I wrote a lot of tests, found one other bug (`Files` was not cached), and cleaned up the 300 line long `runSteps` function. `runSteps` is now much easier to understand and doesn't contain _all_ levels of abstraction. The outer `runSteps` concerns itself with which steps need to be executed and how to handle their results and `executeSingleStep` does the lower-level work of rendering templates, starting containers, setting up the logger, etc.
1 parent ed44e37 commit e47e47d

File tree

7 files changed

+612
-212
lines changed

7 files changed

+612
-212
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ All notable changes to `src-cli` are documented in this file.
1717

1818
### Fixed
1919

20+
- The per-step caching of batch spec execution results was broken when re-execution could use the cached results of a step and that step was the only one left to execute. That resulted in empty diffs being uploaded. This is now fixed. [#567](https://github.com/sourcegraph/src-cli/pull/567)
21+
2022
### Removed
2123

2224
## 3.30.0

internal/batches/executor/executor_test.go

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import (
1414
"testing"
1515
"time"
1616

17+
"github.com/google/go-cmp/cmp"
1718
"github.com/sourcegraph/go-diff/diff"
1819
"github.com/sourcegraph/src-cli/internal/api"
1920
"github.com/sourcegraph/src-cli/internal/batches"
21+
"github.com/sourcegraph/src-cli/internal/batches/git"
2022
"github.com/sourcegraph/src-cli/internal/batches/mock"
2123
"github.com/sourcegraph/src-cli/internal/batches/workspace"
2224
)
@@ -466,3 +468,239 @@ func featuresAllEnabled() batches.FeatureFlags {
466468
AllowOptionalPublished: true,
467469
}
468470
}
471+
472+
func TestExecutor_CachedStepResults(t *testing.T) {
473+
t.Run("single step cached", func(t *testing.T) {
474+
archive := mock.RepoArchive{
475+
Repo: testRepo1, Files: map[string]string{
476+
"README.md": "# Welcome to the README\n",
477+
},
478+
}
479+
480+
cachedDiff := []byte(`diff --git README.md README.md
481+
index 02a19af..c9644dd 100644
482+
--- README.md
483+
+++ README.md
484+
@@ -1 +1,2 @@
485+
# Welcome to the README
486+
+foobar
487+
`)
488+
489+
task := &Task{
490+
BatchChangeAttributes: &BatchChangeAttributes{},
491+
Steps: []batches.Step{
492+
{Run: `echo -e "foobar\n" >> README.md`},
493+
},
494+
CachedResultFound: true,
495+
CachedResult: stepExecutionResult{
496+
StepIndex: 0,
497+
Diff: cachedDiff,
498+
Outputs: map[string]interface{}{},
499+
PreviousStepResult: StepResult{},
500+
},
501+
Repository: testRepo1,
502+
}
503+
504+
results, err := testExecuteTasks(t, []*Task{task}, archive)
505+
if err != nil {
506+
t.Fatalf("execution failed: %s", err)
507+
}
508+
509+
if have, want := len(results), 1; have != want {
510+
t.Fatalf("wrong number of execution results. want=%d, have=%d", want, have)
511+
}
512+
513+
// We want the diff to be the same as the cached one, since we only had to
514+
// execute a single step
515+
executionResult := results[0].result
516+
if diff := cmp.Diff(executionResult.Diff, string(cachedDiff)); diff != "" {
517+
t.Fatalf("wrong diff: %s", diff)
518+
}
519+
520+
if have, want := len(results[0].stepResults), 1; have != want {
521+
t.Fatalf("wrong length of step results. have=%d, want=%d", have, want)
522+
}
523+
524+
stepResult := results[0].stepResults[0]
525+
if diff := cmp.Diff(stepResult, task.CachedResult); diff != "" {
526+
t.Fatalf("wrong stepResult: %s", diff)
527+
}
528+
})
529+
530+
t.Run("one of multiple steps cached", func(t *testing.T) {
531+
archive := mock.RepoArchive{
532+
Repo: testRepo1,
533+
Files: map[string]string{
534+
"README.md": `# automation-testing
535+
This repository is used to test opening and closing pull request with Automation
536+
537+
(c) Copyright Sourcegraph 2013-2020.
538+
(c) Copyright Sourcegraph 2013-2020.
539+
(c) Copyright Sourcegraph 2013-2020.`,
540+
},
541+
}
542+
543+
cachedDiff := []byte(`diff --git README.md README.md
544+
index 1914491..cd2ccbf 100644
545+
--- README.md
546+
+++ README.md
547+
@@ -3,4 +3,5 @@ This repository is used to test opening and closing pull request with Automation
548+
549+
(c) Copyright Sourcegraph 2013-2020.
550+
(c) Copyright Sourcegraph 2013-2020.
551+
-(c) Copyright Sourcegraph 2013-2020.
552+
\ No newline at end of file
553+
+(c) Copyright Sourcegraph 2013-2020.this is step 2
554+
+this is step 3
555+
diff --git README.txt README.txt
556+
new file mode 100644
557+
index 0000000..888e1ec
558+
--- /dev/null
559+
+++ README.txt
560+
@@ -0,0 +1 @@
561+
+this is step 1
562+
`)
563+
564+
wantFinalDiff := `diff --git README.md README.md
565+
index 1914491..d6782d3 100644
566+
--- README.md
567+
+++ README.md
568+
@@ -3,4 +3,7 @@ This repository is used to test opening and closing pull request with Automation
569+
570+
(c) Copyright Sourcegraph 2013-2020.
571+
(c) Copyright Sourcegraph 2013-2020.
572+
-(c) Copyright Sourcegraph 2013-2020.
573+
\ No newline at end of file
574+
+(c) Copyright Sourcegraph 2013-2020.this is step 2
575+
+this is step 3
576+
+this is step 4
577+
+previous_step.modified_files=[README.md]
578+
diff --git README.txt README.txt
579+
new file mode 100644
580+
index 0000000..888e1ec
581+
--- /dev/null
582+
+++ README.txt
583+
@@ -0,0 +1 @@
584+
+this is step 1
585+
diff --git my-output.txt my-output.txt
586+
new file mode 100644
587+
index 0000000..257ae8e
588+
--- /dev/null
589+
+++ my-output.txt
590+
@@ -0,0 +1 @@
591+
+this is step 5
592+
`
593+
594+
task := &Task{
595+
Repository: testRepo1,
596+
BatchChangeAttributes: &BatchChangeAttributes{},
597+
Steps: []batches.Step{
598+
{Run: `echo "this is step 1" >> README.txt`},
599+
{Run: `echo "this is step 2" >> README.md`},
600+
{Run: `echo "this is step 3" >> README.md`, Outputs: batches.Outputs{
601+
"myOutput": batches.Output{
602+
Value: "my-output.txt",
603+
},
604+
}},
605+
{Run: `echo "this is step 4" >> README.md
606+
echo "previous_step.modified_files=${{ previous_step.modified_files }}" >> README.md
607+
`},
608+
{Run: `echo "this is step 5" >> ${{ outputs.myOutput }}`},
609+
},
610+
CachedResultFound: true,
611+
CachedResult: stepExecutionResult{
612+
StepIndex: 2,
613+
Diff: cachedDiff,
614+
Outputs: map[string]interface{}{
615+
"myOutput": "my-output.txt",
616+
},
617+
PreviousStepResult: StepResult{
618+
Files: &git.Changes{
619+
Modified: []string{"README.md"},
620+
Added: []string{"README.txt"},
621+
},
622+
Stdout: nil,
623+
Stderr: nil,
624+
},
625+
},
626+
}
627+
628+
results, err := testExecuteTasks(t, []*Task{task}, archive)
629+
if err != nil {
630+
t.Fatalf("execution failed: %s", err)
631+
}
632+
633+
if have, want := len(results), 1; have != want {
634+
t.Fatalf("wrong number of execution results. want=%d, have=%d", want, have)
635+
}
636+
637+
executionResult := results[0].result
638+
if diff := cmp.Diff(executionResult.Diff, wantFinalDiff); diff != "" {
639+
t.Fatalf("wrong diff: %s", diff)
640+
}
641+
642+
if diff := cmp.Diff(executionResult.Outputs, task.CachedResult.Outputs); diff != "" {
643+
t.Fatalf("wrong execution result outputs: %s", diff)
644+
}
645+
646+
// Only two steps should've been executed
647+
if have, want := len(results[0].stepResults), 2; have != want {
648+
t.Fatalf("wrong length of step results. have=%d, want=%d", have, want)
649+
}
650+
651+
lastStepResult := results[0].stepResults[1]
652+
if have, want := lastStepResult.StepIndex, 4; have != want {
653+
t.Fatalf("wrong stepIndex. have=%d, want=%d", have, want)
654+
}
655+
656+
if diff := cmp.Diff(lastStepResult.Outputs, task.CachedResult.Outputs); diff != "" {
657+
t.Fatalf("wrong step result outputs: %s", diff)
658+
}
659+
})
660+
}
661+
662+
func testExecuteTasks(t *testing.T, tasks []*Task, archives ...mock.RepoArchive) ([]taskResult, error) {
663+
if runtime.GOOS == "windows" {
664+
t.Skip("Test doesn't work on Windows because dummydocker is written in bash")
665+
}
666+
667+
testTempDir, err := ioutil.TempDir("", "executor-integration-test-*")
668+
if err != nil {
669+
t.Fatal(err)
670+
}
671+
t.Cleanup(func() { os.Remove(testTempDir) })
672+
673+
// Setup dummydocker
674+
addToPath(t, "testdata/dummydocker")
675+
676+
// Setup mock test server & client
677+
mux := mock.NewZipArchivesMux(t, nil, archives...)
678+
ts := httptest.NewServer(mux)
679+
t.Cleanup(ts.Close)
680+
681+
var clientBuffer bytes.Buffer
682+
client := api.NewClient(api.ClientOpts{Endpoint: ts.URL, Out: &clientBuffer})
683+
684+
// Prepare tasks
685+
for _, t := range tasks {
686+
for i := range t.Steps {
687+
t.Steps[i].SetImage(&mock.Image{
688+
RawDigest: t.Steps[i].Container,
689+
})
690+
}
691+
}
692+
693+
// Setup executor
694+
executor := newExecutor(newExecutorOpts{
695+
Creator: workspace.NewCreator(context.Background(), "bind", testTempDir, testTempDir, []batches.Step{}),
696+
Fetcher: batches.NewRepoFetcher(client, testTempDir, false),
697+
Logger: mock.LogNoOpManager{},
698+
699+
TempDir: testTempDir,
700+
Parallelism: runtime.GOMAXPROCS(0),
701+
Timeout: 30 * time.Second,
702+
})
703+
704+
executor.Start(context.Background(), tasks, NewTaskStatusCollection(tasks))
705+
return executor.Wait(context.Background())
706+
}

0 commit comments

Comments
 (0)