| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "path/filepath" |
| 7 | "sync" |
| 8 | "sync/atomic" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "reasonix/internal/identitylock" |
| 13 | ) |
| 14 | |
| 15 | func TestManualCreationWaitsForSupersededBuildActualExit(t *testing.T) { |
| 16 | a := newManualSessionTestApp(t) |
| 17 | entered := make(chan struct{}, 2) |
| 18 | first, second := make(chan struct{}), make(chan struct{}) |
| 19 | var firstOnce, secondOnce sync.Once |
| 20 | defer firstOnce.Do(func() { close(first) }) |
| 21 | defer secondOnce.Do(func() { close(second) }) |
| 22 | var calls atomic.Int32 |
| 23 | a.tabBuildStartHook = func(string) { |
| 24 | n := calls.Add(1) |
| 25 | entered <- struct{}{} |
| 26 | if n == 1 { |
| 27 | <-first |
| 28 | } else { |
| 29 | <-second |
| 30 | } |
| 31 | } |
| 32 | v, err := a.BeginManualSessionCreation(ManualSessionCreationRequest{OperationID: "superseded-creation-owner", Scope: "global"}) |
| 33 | if err != nil { |
| 34 | t.Fatal(err) |
| 35 | } |
| 36 | <-entered |
| 37 | a.mu.RLock() |
| 38 | var tab *WorkspaceTab |
| 39 | for _, candidate := range a.tabs { |
| 40 | if candidate.SessionID == v.Ref.SessionID { |
| 41 | tab = candidate |
| 42 | } |
| 43 | } |
| 44 | old := tab.buildExecution |
| 45 | oldNotification := tab.buildDone |
| 46 | a.mu.RUnlock() |
| 47 | a.startTabControllerBuild(tab) |
| 48 | <-entered |
| 49 | select { |
| 50 | case <-oldNotification: |
| 51 | default: |
| 52 | t.Fatal("test did not supersede the notification") |
| 53 | } |
| 54 | m := a.creationManager() |
| 55 | a.shuttingDown.Store(true) |
| 56 | ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) |
| 57 | defer cancel() |
| 58 | if err = m.CancelAndWait(ctx); !errors.Is(err, context.DeadlineExceeded) { |
| 59 | t.Fatalf("unjoined build stopped: %v", err) |
| 60 | } |
| 61 | firstOnce.Do(func() { close(first) }) |
| 62 | <-old.done |
| 63 | path := filepath.Join(filepath.Dir(a.sessionUIStore().Path()), "manual-creation-locks", v.Ref.SessionID+".lock") |
| 64 | unlock, err := identitylock.TryAcquire(path) |
| 65 | if err == nil { |
| 66 | unlock() |
| 67 | t.Fatal("released before the second build exited") |
| 68 | } |
| 69 | if !errors.Is(err, identitylock.ErrHeld) { |
| 70 | t.Fatal(err) |
| 71 | } |
| 72 | secondOnce.Do(func() { close(second) }) |
| 73 | if err = m.CancelAndWait(t.Context()); err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | got, _ := a.GetManualSessionCreation(v.OperationID) |
| 77 | if got.Phase != "starting" { |
| 78 | t.Fatalf("cancelled attempt persisted a terminal result: %+v", got) |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | func TestManualCreationResumesPreparedStorage(t *testing.T) { |
| 83 | a := newManualSessionTestApp(t) |
| 84 | v, _ := seedManualCreation(t, a, "prepared-creation-recovery", "starting") |
| 85 | tab, err := a.reserveManualSessionTab(t.Context(), v, func(string) {}) |
| 86 | if err != nil { |
| 87 | t.Fatal(err) |
| 88 | } |
| 89 | m := a.creationManager() |
| 90 | m.Ensure(v.OperationID, "recovery", "") |
| 91 | a.manualCreationTasks.Wait() |
| 92 | got, err := a.GetManualSessionCreation(v.OperationID) |
| 93 | if err != nil || got.Phase != "ready" || got.Ref != v.Ref { |
| 94 | t.Fatalf("resume: %+v %v", got, err) |
| 95 | } |
| 96 | a.mu.RLock() |
| 97 | defer a.mu.RUnlock() |
| 98 | if len(a.tabs) != 1 || a.tabs[tab.ID] != tab { |
| 99 | t.Fatal("prepared runtime identity was replaced") |
| 100 | } |
| 101 | } |
| 102 |