| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154 |
- package safeapply
- import (
- "errors"
- "os"
- "path/filepath"
- "testing"
- "time"
- )
- func TestRestartRecoveryAndConfirmation(t *testing.T) {
- dir := t.TempDir()
- state := filepath.Join(dir, "state")
- original := filepath.Join(dir, "config")
- created := filepath.Join(dir, "new")
- if err := os.WriteFile(original, []byte("before"), 0640); err != nil {
- t.Fatal(err)
- }
- now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
- applyCalls := 0
- manager := New(state, []string{original, created}, func() error { applyCalls++; return nil })
- manager.now = func() time.Time { return now }
- token, deadline, err := manager.Begin(time.Minute)
- if err != nil {
- t.Fatal(err)
- }
- if _, _, err = manager.Begin(time.Minute); !errors.Is(err, ErrPending) {
- t.Fatal(err)
- }
- if err = manager.Confirm("wrong"); !errors.Is(err, ErrToken) {
- t.Fatal(err)
- }
- os.WriteFile(original, []byte("changed"), 0600)
- os.WriteFile(created, []byte("new"), 0600)
- restarted := New(state, []string{original, created}, func() error { applyCalls++; return nil })
- restarted.now = func() time.Time { return deadline }
- if err = restarted.Confirm(token); !errors.Is(err, ErrExpired) {
- t.Fatal(err)
- }
- if ok, err := restarted.RecoverExpired(); err != nil || !ok {
- t.Fatalf("recover: %v %v", ok, err)
- }
- data, err := os.ReadFile(original)
- if err != nil || string(data) != "before" {
- t.Fatalf("restored %q %v", data, err)
- }
- if _, err = os.Stat(created); !os.IsNotExist(err) {
- t.Fatalf("created file remains: %v", err)
- }
- info, err := os.Stat(original)
- if err != nil || info.Mode().Perm() != 0640 {
- t.Fatalf("mode: %v %v", info, err)
- }
- if applyCalls != 1 {
- t.Fatal(applyCalls)
- }
- if status, err := restarted.Status(); err != nil || status.Pending {
- t.Fatalf("status: %+v %v", status, err)
- }
- }
- func TestRecoveryFailureRemainsRetryable(t *testing.T) {
- dir := t.TempDir()
- path := filepath.Join(dir, "file")
- os.WriteFile(path, []byte("old"), 0600)
- now := time.Now()
- fail := true
- m := New(filepath.Join(dir, "state"), []string{path}, func() error {
- if fail {
- return errors.New("apply failed")
- }
- return nil
- })
- m.now = func() time.Time { return now }
- _, deadline, err := m.Begin(time.Second)
- if err != nil {
- t.Fatal(err)
- }
- os.WriteFile(path, []byte("new"), 0600)
- now = deadline
- if ok, err := m.RecoverExpired(); err == nil || ok {
- t.Fatal("expected recovery failure")
- }
- if status, err := m.Status(); err != nil || !status.Pending {
- t.Fatalf("status %+v %v", status, err)
- }
- fail = false
- if ok, err := m.RecoverExpired(); err != nil || !ok {
- t.Fatalf("retry %v %v", ok, err)
- }
- }
- func TestConfirmPreventsRecovery(t *testing.T) {
- dir := t.TempDir()
- path := filepath.Join(dir, "file")
- os.WriteFile(path, []byte("old"), 0600)
- now := time.Now()
- m := New(filepath.Join(dir, "state"), []string{path}, func() error { t.Fatal("unexpected recovery"); return nil })
- m.now = func() time.Time { return now }
- token, deadline, err := m.Begin(time.Second)
- if err != nil {
- t.Fatal(err)
- }
- os.WriteFile(path, []byte("new"), 0600)
- if err = m.Confirm(token); err != nil {
- t.Fatal(err)
- }
- now = deadline
- if ok, err := m.RecoverExpired(); err != nil || ok {
- t.Fatalf("recovered %v %v", ok, err)
- }
- data, _ := os.ReadFile(path)
- if string(data) != "new" {
- t.Fatalf("got %q", data)
- }
- }
- func TestConfirmationAndRecoverySerialize(t *testing.T) {
- dir := t.TempDir()
- path := filepath.Join(dir, "config")
- if err := os.WriteFile(path, []byte("old"), 0600); err != nil {
- t.Fatal(err)
- }
- now := time.Now()
- state := filepath.Join(dir, "state")
- first := New(state, []string{path}, func() error { return nil })
- first.now = func() time.Time { return now }
- token, deadline, err := first.Begin(time.Minute)
- if err != nil {
- t.Fatal(err)
- }
- second := New(state, []string{path}, func() error { return nil })
- second.now = func() time.Time { return deadline }
- confirm := make(chan error, 1)
- recoverResult := make(chan bool, 1)
- recoverErr := make(chan error, 1)
- start := make(chan struct{})
- go func() { <-start; confirm <- first.Confirm(token) }()
- go func() { <-start; ok, err := second.RecoverExpired(); recoverResult <- ok; recoverErr <- err }()
- close(start)
- confirmErr := <-confirm
- recovered := <-recoverResult
- err = <-recoverErr
- if err != nil {
- t.Fatal(err)
- }
- if confirmErr == nil && recovered {
- t.Fatal("both confirmation and recovery succeeded")
- }
- if confirmErr != nil && !errors.Is(confirmErr, ErrNoPending) {
- t.Fatalf("confirmation: %v", confirmErr)
- }
- if confirmErr != nil && !recovered {
- t.Fatal("neither confirmation nor recovery succeeded")
- }
- }
|