diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-17 13:53:01 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-17 13:53:01 +0200 |
| commit | a80d470cbad7335fd5e534f32d935e5a49aadb24 (patch) | |
| tree | c7d92ba026ebb90dfce4ffb8cb547a1db49812b2 /internal/apply/swap_test.go | |
| parent | 78d8313791f05defc9e0a9f2bad8e9710f741a60 (diff) | |
| download | krino-a80d470cbad7335fd5e534f32d935e5a49aadb24.tar.gz krino-a80d470cbad7335fd5e534f32d935e5a49aadb24.zip | |
a file trashed to make room is logged the moment it is trashed
The displace was carried out first and logged only when the whole step
finished - for a copy or a cross-device move, the entire data transfer
later. A process killed in that window left the user's file in the
Trash with nothing recording it: krino log said "nothing applied" and
undo offered nothing.
It is its own action with its own line (spec ยง9), so it is now written
the moment trash.Put returns, through a hook ChainLogged calls before
the step that needed the name begins. A displace that cannot be logged
fails the step rather than compounding an unrecorded destructive act
with a second one.
Verified by killing krino -9 mid-copy with 600 MB in flight:
before: krino log "nothing applied", 0 displace lines
after: krino log "1 displaced", 1 displace line
Diffstat (limited to 'internal/apply/swap_test.go')
| -rw-r--r-- | internal/apply/swap_test.go | 90 |
1 files changed, 88 insertions, 2 deletions
diff --git a/internal/apply/swap_test.go b/internal/apply/swap_test.go index d16d3f9..297db72 100644 --- a/internal/apply/swap_test.go +++ b/internal/apply/swap_test.go @@ -4,6 +4,7 @@ package apply import ( "context" + "errors" "os" "path/filepath" "strings" @@ -156,7 +157,7 @@ func TestChainLoggedReportsEachStepBeforeTheNext(t *testing.T) { } } return nil - }) + }, nil) if err != nil || len(calls) != 2 || res[1].Status != "ok" { t.Fatalf("calls %v, err %v, results %+v", calls, err, res) } @@ -206,7 +207,7 @@ func TestChainLoggedStopsBetweenStepsWhenInterrupted(t *testing.T) { cancel() } return nil - }) + }, nil) if err != nil { t.Fatal(err) } @@ -217,3 +218,88 @@ func TestChainLoggedStopsBetweenStepsWhenInterrupted(t *testing.T) { t.Errorf("the finished step was undone or never ran: %v", err) } } + +// TestDisplaceIsReportedBeforeTheStepThatNeededIt: trashing the file in the +// way is a destructive act of its own, and it is durable the moment +// trash.Put returns. Reporting it only when the whole step finishes leaves +// a window - the entire data transfer of a copy or a cross-device move - +// in which the user's file is in the Trash with nothing recording that it +// went there. Killed in that window, krino log said "nothing applied". +func TestDisplaceIsReportedBeforeTheStepThatNeededIt(t *testing.T) { + root := t.TempDir() + // The Trash must be on the same filesystem as the file being trashed. + t.Setenv("XDG_DATA_HOME", filepath.Join(root, "share")) + src := filepath.Join(root, "a.pdf") + dst := filepath.Join(root, "W", "a.pdf") + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(dst, []byte("the file already there"), 0o644); err != nil { + t.Fatal(err) + } + c := planned(t, root, "a.pdf", "incoming", + plan.Step{Kind: plan.Copy, Src: src, Dst: dst, Displaces: dst}, + ) + + var order []string + res, err := ChainLogged(context.Background(), c, + func(i int, sr StepResult) error { + order = append(order, "step") + return nil + }, + func(i int, step plan.Step, entry string) error { + order = append(order, "displace") + if entry == "" { + t.Error("the displace was reported with no trash entry") + } + // The file is in the Trash already; the copy has not begun. + if b, err := os.ReadFile(dst); err == nil && string(b) == "incoming" { + t.Error("the displace was reported only after the copy had run") + } + return nil + }) + if err != nil { + t.Fatal(err) + } + if len(order) != 2 || order[0] != "displace" || order[1] != "step" { + t.Errorf("order = %v; want the displace reported first", order) + } + if res[0].Status != "ok" { + t.Errorf("step = %+v", res[0]) + } +} + +// TestDisplaceThatCannotBeReportedFailsTheStep: if the displace cannot be +// written to the log, the step must not go on to use the name - the user's +// file is already in the Trash and nothing would record it. +func TestDisplaceThatCannotBeReportedFailsTheStep(t *testing.T) { + root := t.TempDir() + t.Setenv("XDG_DATA_HOME", filepath.Join(root, "share")) + src := filepath.Join(root, "a.pdf") + dst := filepath.Join(root, "W", "a.pdf") + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(dst, []byte("the file already there"), 0o644); err != nil { + t.Fatal(err) + } + c := planned(t, root, "a.pdf", "incoming", + plan.Step{Kind: plan.Copy, Src: src, Dst: dst, Displaces: dst}, + ) + res, err := ChainLogged(context.Background(), c, nil, + func(i int, step plan.Step, entry string) error { + return errors.New("log is closed") + }) + if err != nil { + t.Fatal(err) + } + if res[0].Status != "failed" || !strings.Contains(res[0].Detail, "could not be logged") { + t.Errorf("step = %+v; want a failure naming the unlogged displace", res[0]) + } + if res[0].DisplacedEntry == "" { + t.Error("the failure does not carry the trash entry, so nothing can say where the file went") + } + if b, rerr := os.ReadFile(dst); rerr == nil && string(b) == "incoming" { + t.Error("the copy ran even though the displace could not be logged") + } +} |
