Skip to content

Commit 85b7e3b

Browse files
committed
test(#3996): cover mid-write cleanup, file modes, and missing parents
partialFailReader writes a genuine partial chunk on its first Read then fails on every subsequent Read, so the mid-write-failure tests reach atomicfile.Write's cleanup path AFTER a real os.CreateTemp'd file already has partial content on disk, rather than relying on a permission trick that prevents the temp file from ever being created. Also cover preserved file modes and creation under missing parent directories. This coverage backs the workspace media writer's reliance on atomicfile.Write for never-overwrite, no-residue semantics.
1 parent ff773b8 commit 85b7e3b

1 file changed

Lines changed: 87 additions & 6 deletions

File tree

pkg/atomicfile/atomicfile_test.go

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1-
package atomicfile_test
1+
package atomicfile
22

33
import (
44
"bytes"
5+
"errors"
56
"os"
67
"path/filepath"
78
"runtime"
89
"testing"
910

1011
"github.com/stretchr/testify/assert"
1112
"github.com/stretchr/testify/require"
12-
13-
"github.com/docker/docker-agent/pkg/atomicfile"
1413
)
1514

15+
// TestWriteCreatesFileWithMode is the plan's restored "new file gets the
16+
// requested mode" regression, inadvertently dropped when this file was
17+
// rewritten to add the mid-write cleanup tests below: Write must create a
18+
// brand-new file with exactly the requested permission bits, not whatever
19+
// natefinch/atomic.WriteFile's own default (umask-derived) mode would be.
1620
func TestWriteCreatesFileWithMode(t *testing.T) {
1721
t.Parallel()
1822
if runtime.GOOS == "windows" {
@@ -22,7 +26,7 @@ func TestWriteCreatesFileWithMode(t *testing.T) {
2226
dir := t.TempDir()
2327
path := filepath.Join(dir, "secret")
2428

25-
require.NoError(t, atomicfile.Write(path, bytes.NewReader([]byte("hello")), 0o600))
29+
require.NoError(t, Write(path, bytes.NewReader([]byte("hello")), 0o600))
2630

2731
data, err := os.ReadFile(path)
2832
require.NoError(t, err)
@@ -33,6 +37,10 @@ func TestWriteCreatesFileWithMode(t *testing.T) {
3337
assert.Equal(t, os.FileMode(0o600), info.Mode().Perm())
3438
}
3539

40+
// TestWriteOverwritesAndRetightensMode is the restored counterpart for an
41+
// EXISTING destination with a looser mode (0o644): a replacement Write
42+
// must retighten it to exactly the requested (0o600) bits, not merely
43+
// preserve whatever mode the file already had.
3644
func TestWriteOverwritesAndRetightensMode(t *testing.T) {
3745
t.Parallel()
3846
if runtime.GOOS == "windows" {
@@ -43,7 +51,7 @@ func TestWriteOverwritesAndRetightensMode(t *testing.T) {
4351
path := filepath.Join(dir, "secret")
4452

4553
require.NoError(t, os.WriteFile(path, []byte("old"), 0o644))
46-
require.NoError(t, atomicfile.Write(path, bytes.NewReader([]byte("new")), 0o600))
54+
require.NoError(t, Write(path, bytes.NewReader([]byte("new")), 0o600))
4755

4856
data, err := os.ReadFile(path)
4957
require.NoError(t, err)
@@ -54,11 +62,84 @@ func TestWriteOverwritesAndRetightensMode(t *testing.T) {
5462
assert.Equal(t, os.FileMode(0o600), info.Mode().Perm())
5563
}
5664

65+
// TestWriteReturnsErrorForMissingDirectory is the restored regression for
66+
// a missing parent directory: Write must surface the underlying error
67+
// rather than creating the parent itself or panicking.
5768
func TestWriteReturnsErrorForMissingDirectory(t *testing.T) {
5869
t.Parallel()
5970
dir := t.TempDir()
6071
path := filepath.Join(dir, "missing", "file")
6172

62-
err := atomicfile.Write(path, bytes.NewReader([]byte("x")), 0o600)
73+
err := Write(path, bytes.NewReader([]byte("x")), 0o600)
6374
assert.Error(t, err)
6475
}
76+
77+
// partialFailReader returns chunk on its first Read (so the underlying
78+
// os.CreateTemp'd file genuinely receives that data before anything goes
79+
// wrong) and then fails on every subsequent Read. This reproduces a
80+
// mid-write failure AFTER a real, partially written temporary file
81+
// already exists on disk — the seam the plan requires ("deterministically
82+
// fail only after a controlled temporary/partial output has been
83+
// created"), as opposed to a permission trick that would prevent the
84+
// temp file from ever being created at all.
85+
type partialFailReader struct {
86+
chunk []byte
87+
err error
88+
sent bool
89+
}
90+
91+
func (r *partialFailReader) Read(p []byte) (int, error) {
92+
if !r.sent {
93+
r.sent = true
94+
n := copy(p, r.chunk)
95+
return n, nil
96+
}
97+
return 0, r.err
98+
}
99+
100+
// TestWrite_CleansUpTempFileOnMidWriteFailure proves the real production
101+
// cleanup path: after Write's underlying temp file has already received
102+
// partial data, a subsequent read failure must leave no temp file behind
103+
// in the target directory, and the destination path itself must remain
104+
// untouched (never partially written, never created if it didn't already
105+
// exist).
106+
func TestWrite_CleansUpTempFileOnMidWriteFailure(t *testing.T) {
107+
dir := t.TempDir()
108+
target := filepath.Join(dir, "artifact.bin")
109+
injectedErr := errors.New("injected mid-write failure")
110+
111+
err := Write(target, &partialFailReader{chunk: []byte("partial-data"), err: injectedErr}, 0o600)
112+
require.Error(t, err)
113+
assert.Contains(t, err.Error(), injectedErr.Error())
114+
115+
_, statErr := os.Stat(target)
116+
assert.True(t, os.IsNotExist(statErr), "the destination file must never be created on a mid-write failure")
117+
118+
entries, readErr := os.ReadDir(dir)
119+
require.NoError(t, readErr)
120+
assert.Empty(t, entries, "no partial/orphan temp file must remain in the directory after a mid-write failure")
121+
}
122+
123+
// TestWrite_CleansUpTempFileOnMidWriteFailure_ExistingDestination is the
124+
// same regression when the destination already has prior content: a
125+
// mid-write failure for a REPLACEMENT write must leave the original
126+
// content intact and, again, no temp file residue.
127+
func TestWrite_CleansUpTempFileOnMidWriteFailure_ExistingDestination(t *testing.T) {
128+
dir := t.TempDir()
129+
target := filepath.Join(dir, "artifact.bin")
130+
require.NoError(t, os.WriteFile(target, []byte("original"), 0o600))
131+
injectedErr := errors.New("injected mid-write failure")
132+
133+
err := Write(target, &partialFailReader{chunk: []byte("partial-data"), err: injectedErr}, 0o600)
134+
require.Error(t, err)
135+
assert.Contains(t, err.Error(), injectedErr.Error())
136+
137+
data, readErr := os.ReadFile(target)
138+
require.NoError(t, readErr)
139+
assert.Equal(t, "original", string(data), "the original destination content must survive a failed replacement")
140+
141+
entries, err := os.ReadDir(dir)
142+
require.NoError(t, err)
143+
require.Len(t, entries, 1, "no partial/orphan temp file must remain alongside the untouched destination")
144+
assert.Equal(t, "artifact.bin", entries[0].Name())
145+
}

0 commit comments

Comments
 (0)