diff options
Diffstat (limited to 'internal/apply')
| -rw-r--r-- | internal/apply/apply.go | 12 | ||||
| -rw-r--r-- | internal/apply/swap_test.go | 34 |
2 files changed, 43 insertions, 3 deletions
diff --git a/internal/apply/apply.go b/internal/apply/apply.go index 684641e..a93aad4 100644 --- a/internal/apply/apply.go +++ b/internal/apply/apply.go @@ -7,6 +7,7 @@ package apply import ( + "context" "errors" "fmt" "os" @@ -44,7 +45,7 @@ type StepResult struct { // marking the rest skipped. It never touches a file whose size or mtime no // longer matches what the plan recorded. It is ChainLogged with no done. func Chain(c plan.Chain) []StepResult { - results, _ := ChainLogged(c, nil) + results, _ := ChainLogged(context.Background(), c, nil) return results } @@ -58,11 +59,18 @@ func Chain(c plan.Chain) []StepResult { // planned destination was taken since planning, stops the chain too: every // later step was planned against the name the file did not get, and must // not act on whatever is at that path (review M3). -func ChainLogged(c plan.Chain, done func(i int, sr StepResult) error) ([]StepResult, error) { +// +// Once ctx is cancelled (an interrupt), the step already under way finishes +// and every later step is skipped as "interrupted": an interrupt stops after +// the current step, not after the file's whole chain (spec ยง11). +func ChainLogged(ctx context.Context, c plan.Chain, done func(i int, sr StepResult) error) ([]StepResult, error) { results := make([]StepResult, len(c.Steps)) stopWhy := "" for i, step := range c.Steps { + if stopWhy == "" && ctx.Err() != nil { + stopWhy = "interrupted" + } switch { case stopWhy != "": results[i] = StepResult{Step: step, Status: "skipped", Detail: stopWhy} diff --git a/internal/apply/swap_test.go b/internal/apply/swap_test.go index c185686..ccac945 100644 --- a/internal/apply/swap_test.go +++ b/internal/apply/swap_test.go @@ -3,6 +3,7 @@ package apply import ( + "context" "os" "path/filepath" "strings" @@ -144,7 +145,7 @@ func TestChainLoggedReportsEachStepBeforeTheNext(t *testing.T) { plan.Step{Kind: plan.Rename, Src: moved, Dst: renamed}, ) var calls []int - res, err := ChainLogged(c, func(i int, sr StepResult) error { + res, err := ChainLogged(context.Background(), c, func(i int, sr StepResult) error { calls = append(calls, i) if i == 0 { if _, err := os.Lstat(moved); err != nil { @@ -185,3 +186,34 @@ func TestChainRefusesToDisplaceANonRegularTarget(t *testing.T) { t.Errorf("the directory was displaced: %v", err) } } + +// TestChainLoggedStopsBetweenStepsWhenInterrupted: once the context is +// cancelled, the chain finishes the step it is on and skips the rest, so an +// interrupt stops after the current step, not after the file's whole chain +// (re-review pa F7). +func TestChainLoggedStopsBetweenStepsWhenInterrupted(t *testing.T) { + root := t.TempDir() + p := filepath.Join(root, "a.pdf") + moved := filepath.Join(root, "W", "a.pdf") + c := planned(t, root, "a.pdf", "body", + plan.Step{Kind: plan.Move, Src: p, Dst: moved}, + plan.Step{Kind: plan.Rename, Src: moved, Dst: filepath.Join(root, "W", "b.pdf")}, + ) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + res, err := ChainLogged(ctx, c, func(i int, sr StepResult) error { + if i == 0 { + cancel() + } + return nil + }) + if err != nil { + t.Fatal(err) + } + if res[0].Status != "ok" || res[1].Status != "skipped" || res[1].Detail != "interrupted" { + t.Errorf("steps = %s, %s %q; want ok, then skipped as interrupted", res[0].Status, res[1].Status, res[1].Detail) + } + if _, err := os.Lstat(moved); err != nil { + t.Errorf("the finished step was undone or never ran: %v", err) + } +} |
