mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-24 23:59:08 +02:00
* Use POSIX-like file read/write of json for windows + tests: allow renaming an open file.
117 lines
3.1 KiB
Go
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")
|
|
}
|
|
}
|