author | Alberto Bertogli
<albertito@blitiri.com.ar> 2024-03-07 00:26:24 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2024-03-07 23:07:37 UTC |
parent | 06aea2f7862a1e04e24216b574462cf3a5d490d2 |
internal/safeio/safeio.go | +16 | -1 |
internal/safeio/safeio_test.go | +132 | -3 |
diff --git a/internal/safeio/safeio.go b/internal/safeio/safeio.go index 22abf73..31ad2a4 100644 --- a/internal/safeio/safeio.go +++ b/internal/safeio/safeio.go @@ -8,6 +8,21 @@ import ( "syscall" ) +// osFile is an interface to the methods of os.File that we need, so we can +// simulate failures in tests. +type osFile interface { + Name() string + Chmod(os.FileMode) error + Chown(int, int) error + Write([]byte) (int, error) + Close() error +} + +var createTemp func(dir, pattern string) (osFile, error) = func( + dir, pattern string) (osFile, error) { + return os.CreateTemp(dir, pattern) +} + // FileOp represents an operation on a file (passed by its name). type FileOp func(fname string) error @@ -27,7 +42,7 @@ func WriteFile(filename string, data []byte, perm os.FileMode, ops ...FileOp) er // would have no expectation of Rename being atomic. // We make the file names start with "." so there's no confusion with the // originals. - tmpf, err := os.CreateTemp(path.Dir(filename), "."+path.Base(filename)) + tmpf, err := createTemp(path.Dir(filename), "."+path.Base(filename)) if err != nil { return err } diff --git a/internal/safeio/safeio_test.go b/internal/safeio/safeio_test.go index c460922..021619e 100644 --- a/internal/safeio/safeio_test.go +++ b/internal/safeio/safeio_test.go @@ -112,6 +112,135 @@ func TestWriteFileWithFailingOp(t *testing.T) { } } -// TODO: We should test the possible failure scenarios for WriteFile, but it -// gets tricky without being able to do failure injection (or turning the code -// into a mess). +type testFile struct { + t *testing.T + + name string + + expectChmod os.FileMode + chmodErr error + + expectChownUid, expectChownGid int + chownErr error + + expectWrite []byte + writeN int + writeErr error + + closeErr error +} + +func (f *testFile) Name() string { + return f.name +} + +func (f *testFile) Chmod(perm os.FileMode) error { + if f.expectChmod != perm { + f.t.Errorf("unexpected Chmod(%v), expected Chmod(%v)", + perm, f.expectChmod) + } + return f.chmodErr +} + +func (f *testFile) Chown(uid, gid int) error { + if f.expectChownUid != uid || f.expectChownGid != gid { + f.t.Errorf("unexpected Chown(%v, %v), expected Chown(%v, %v)", + uid, gid, f.expectChownUid, f.expectChownGid) + } + return f.chownErr +} + +func (f *testFile) Write(b []byte) (int, error) { + if !bytes.Equal(b, f.expectWrite) { + f.t.Errorf("unexpected Write(%q), expected Write(%q)", + b, f.expectWrite) + } + return f.writeN, f.writeErr +} + +func (f *testFile) Close() error { + return f.closeErr +} + +var _ osFile = &testFile{} + +func TestErrors(t *testing.T) { + dir := testlib.MustTempDir(t) + defer testlib.RemoveIfOk(t, dir) + + oldCreateTemp := createTemp + defer func() { createTemp = oldCreateTemp }() + + // createTemp failure. + ctError := errors.New("createTemp error") + createTemp = func(dir, pattern string) (osFile, error) { + return nil, ctError + } + err := WriteFile("fname", []byte("new content"), 0660) + if err != ctError { + t.Errorf("expected %v, got %v", ctError, err) + } + + // Have a real backing file for some of the operations, like getting the + // owner. + fname := dir + "/file1" + + // Test file to simulate failures on. + tf := &testFile{name: fname, t: t} + createTemp = func(dir, pattern string) (osFile, error) { + return tf, nil + } + + // Test Chmod error. + testlib.Rewrite(t, fname, "old content") + tf.expectChmod = 0660 + tf.chmodErr = errors.New("chmod error") + err = WriteFile(fname, []byte("new content"), 0660) + if err != tf.chmodErr { + t.Errorf("expected %v, got %v", tf.chmodErr, err) + } + checkNotExists(t, fname) + + // Test Chown error. + testlib.Rewrite(t, fname, "old content") + tf.chmodErr = nil + tf.expectChownUid, tf.expectChownGid = getOwner(fname) + if tf.expectChownUid < 0 { + t.Fatalf("error getting owner of %v", fname) + } + tf.chownErr = errors.New("chown error") + err = WriteFile(fname, []byte("new content"), 0660) + if err != tf.chownErr { + t.Errorf("expected %v, got %v", tf.chownErr, err) + } + checkNotExists(t, fname) + + // Test Write error. + testlib.Rewrite(t, fname, "old content") + tf.chownErr = nil + tf.expectWrite = []byte("new content") + tf.writeErr = errors.New("write error") + err = WriteFile(fname, []byte("new content"), 0660) + if err != tf.writeErr { + t.Errorf("expected %v, got %v", tf.writeErr, err) + } + checkNotExists(t, fname) + + // Test Close error. + testlib.Rewrite(t, fname, "old content") + tf.writeErr = nil + tf.writeN = len(tf.expectWrite) + tf.closeErr = errors.New("close error") + err = WriteFile(fname, []byte("new content"), 0660) + if err != tf.closeErr { + t.Errorf("expected %v, got %v", tf.closeErr, err) + } + checkNotExists(t, fname) +} + +func checkNotExists(t *testing.T, fname string) { + t.Helper() + if _, err := os.Stat(fname); err == nil { + t.Fatalf("file %v exists", fname) + } +}