package fileops import ( "bytes" "fmt" "os" "os/exec" "path/filepath" "sync" "testing" "time" ) func TestWithLockSerializesAcrossProcesses(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "settings.conf") started := filepath.Join(dir, "started") entered := filepath.Join(dir, "entered") var command *exec.Cmd var output bytes.Buffer err := WithLock(path, func() error { command = exec.Command(os.Args[0], "-test.run=^TestWithLockHelperProcess$") command.Env = append(os.Environ(), "MPV_MANAGER_LOCK_HELPER=1", "MPV_MANAGER_LOCK_PATH="+path, "MPV_MANAGER_LOCK_STARTED="+started, "MPV_MANAGER_LOCK_ENTERED="+entered, ) command.Stdout = &output command.Stderr = &output if err := command.Start(); err != nil { return err } deadline := time.Now().Add(5 * time.Second) for { if _, err := os.Stat(started); err == nil { break } if time.Now().After(deadline) { return fmt.Errorf("helper did not start; output: %s", output.String()) } time.Sleep(5 * time.Millisecond) } time.Sleep(50 * time.Millisecond) if _, err := os.Stat(entered); err == nil { return fmt.Errorf("helper entered while parent held the advisory lock") } return nil }) if err != nil { t.Fatal(err) } if err := command.Wait(); err != nil { t.Fatalf("helper failed: %v\n%s", err, output.String()) } if _, err := os.Stat(entered); err != nil { t.Fatalf("helper did not enter after release: %v", err) } } func TestWithLockHelperProcess(t *testing.T) { if os.Getenv("MPV_MANAGER_LOCK_HELPER") != "1" { return } if err := os.WriteFile(os.Getenv("MPV_MANAGER_LOCK_STARTED"), []byte("started"), 0o600); err != nil { t.Fatal(err) } if err := WithLock(os.Getenv("MPV_MANAGER_LOCK_PATH"), func() error { return os.WriteFile(os.Getenv("MPV_MANAGER_LOCK_ENTERED"), []byte("entered"), 0o600) }); err != nil { t.Fatal(err) } } func TestWithLockSerializesEquivalentPaths(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "settings.conf") equivalent := filepath.Join(dir, "nested", "..", "settings.conf") entered := make(chan struct{}) release := make(chan struct{}) firstDone := make(chan struct{}) go func() { _ = WithLock(path, func() error { close(entered) <-release return nil }) close(firstDone) }() <-entered attempting := make(chan struct{}) secondEntered := make(chan struct{}) go func() { close(attempting) _ = WithLock(equivalent, func() error { close(secondEntered) return nil }) }() <-attempting select { case <-secondEntered: t.Fatal("equivalent path entered while the first lock was held") case <-time.After(20 * time.Millisecond): } close(release) <-firstDone <-secondEntered } func TestAtomicWriteConcurrentTempFilesDoNotCollide(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "settings.conf") const writers = 24 var wg sync.WaitGroup errs := make(chan error, writers) for i := 0; i < writers; i++ { wg.Add(1) go func(i int) { defer wg.Done() if err := AtomicWrite(path, []byte(fmt.Sprintf("writer-%d", i)), 0o600); err != nil { errs <- err } }(i) } wg.Wait() close(errs) for err := range errs { t.Fatal(err) } data, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if len(data) == 0 { t.Fatal("atomic write left an empty target") } matches, err := filepath.Glob(filepath.Join(dir, ".settings.conf.tmp-*")) if err != nil { t.Fatal(err) } if len(matches) != 0 { t.Fatalf("temporary files were not cleaned up: %v", matches) } }