Ctrl K
ComunidadGuías…

util/file_windows_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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")
	}
}