Files
netbird/util/file_windows_test.go
T
Theodor Midtlien 4c19226342 [client] Use POSIX style file read/write of json for windows (#7631)
* Use POSIX-like file read/write of json for windows + tests: allow renaming an open file.
2026-09-24 10:23:50 +02:00

117 lines
3.1 KiB
Go

package util
import (
"context"
"io"
"os"
"path/filepath"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// seedReplace lays out a write as writeBytes leaves it: the destination that
// exists and the temp file that is to take its place.
func seedReplace(t *testing.T) (src, dst string) {
t.Helper()
dir := t.TempDir()
src = filepath.Join(dir, ".tmpstate.json")
dst = filepath.Join(dir, "state.json")
require.NoError(t, os.WriteFile(src, []byte(`{"SomeField": 2}`), 0o600))
require.NoError(t, os.WriteFile(dst, []byte(`{"SomeField": 1}`), 0o600))
return src, dst
}
// The reader has to share the file for delete, or the rename cannot take
// delete access on it. Regression test.
func TestRenameFile_ReplacesAFileBeingRead(t *testing.T) {
t.Run("a reader that shares delete", func(t *testing.T) {
src, dst := seedReplace(t)
f, err := openRead(dst)
require.NoError(t, err)
defer f.Close()
require.Error(t, os.Rename(src, dst),
"delete sharing alone has to be too little, or this test proves nothing")
require.NoError(t, renameFile(src, dst), "POSIX semantics have to get the replace through")
// The handle stays on the file it opened, so a read in flight finishes
// on that version instead of seeing the replacement.
held, err := io.ReadAll(f)
require.NoError(t, err)
assert.JSONEq(t, `{"SomeField": 1}`, string(held), "the version the reader opened")
landed, err := os.ReadFile(dst)
require.NoError(t, err)
assert.JSONEq(t, `{"SomeField": 2}`, string(landed), "the version the writer put there")
})
t.Run("a reader that does not", func(t *testing.T) {
src, dst := seedReplace(t)
f, err := os.Open(dst)
require.NoError(t, err)
defer f.Close()
require.Error(t, renameFile(src, dst),
"a plain read still holds the file, and the caller is owed that error")
})
t.Run("no readers at all", func(t *testing.T) {
src, dst := seedReplace(t)
require.NoError(t, renameFile(src, dst))
landed, err := os.ReadFile(dst)
require.NoError(t, err)
assert.JSONEq(t, `{"SomeField": 2}`, string(landed), "the destination holds what replaced it")
})
}
// A config rewritten while it is being read, which is the daemon reading the
// active profile against a profile switch writing it.
func TestReadJsonWriteJson_Concurrently(t *testing.T) {
path := filepath.Join(t.TempDir(), "state.json")
require.NoError(t, WriteJson(context.Background(), path, &TestConfig{SomeField: 1}))
var wg sync.WaitGroup
errs := make(chan error, 128)
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for r := 0; r < 50; r++ {
var got TestConfig
if _, err := ReadJson(path, &got); err != nil {
errs <- err
return
}
}
}()
}
for i := 0; i < 2; i++ {
wg.Add(1)
go func(writer int) {
defer wg.Done()
for r := 0; r < 50; r++ {
if err := WriteJson(context.Background(), path, &TestConfig{SomeField: writer}); err != nil {
errs <- err
return
}
}
}(i)
}
wg.Wait()
close(errs)
for err := range errs {
assert.NoError(t, err, "a read and a write of the same config must not collide")
}
}