test(daemon): cover concurrent MRU touches
- commit
- e8cafa154672076d6a876dcafe48c15c2ae2ecaa
- author
- brice · · 2026-07-05 08:17 UTC
- parent
- 467a998
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +50 -0
M internal/usecase/daemon/session_test.go +50 -0
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "io" | |
| 6 | 6 | "log/slog" | |
| 7 | 7 | "sync" | |
| 8 | + | "sync/atomic" | |
| 8 | 9 | "testing" | |
| 9 | 10 | "time" | |
| 10 | 11 | ||
| @@ -659,6 +660,55 @@ func TestDaemonLoadsPersistedSessionsAsStopped(t *testing.T) { | |||
| 659 | 660 | require.Equal(t, stoppedSession{name: "work", cwd: "/tmp/work", createdAt: 7}, stopped) | |
| 660 | 661 | } | |
| 661 | 662 | ||
| 663 | + | func TestTouchMRUConcurrentUpdatesRemainMonotonic(t *testing.T) { | |
| 664 | + | d := &Daemon{} | |
| 665 | + | sess := &session{} | |
| 666 | + | const goroutines = 16 | |
| 667 | + | const iterations = 1000 | |
| 668 | + | start := make(chan struct{}) | |
| 669 | + | done := make(chan struct{}) | |
| 670 | + | var decreased atomic.Bool | |
| 671 | + | var wg sync.WaitGroup | |
| 672 | + | var observer sync.WaitGroup | |
| 673 | + | ||
| 674 | + | observer.Add(1) | |
| 675 | + | go func() { | |
| 676 | + | defer observer.Done() | |
| 677 | + | previous := sess.mruAt.Load() | |
| 678 | + | for { | |
| 679 | + | select { | |
| 680 | + | case <-done: | |
| 681 | + | return | |
| 682 | + | default: | |
| 683 | + | } | |
| 684 | + | current := sess.mruAt.Load() | |
| 685 | + | if current < previous { | |
| 686 | + | decreased.Store(true) | |
| 687 | + | return | |
| 688 | + | } | |
| 689 | + | previous = current | |
| 690 | + | } | |
| 691 | + | }() | |
| 692 | + | ||
| 693 | + | for range goroutines { | |
| 694 | + | wg.Add(1) | |
| 695 | + | go func() { | |
| 696 | + | defer wg.Done() | |
| 697 | + | <-start | |
| 698 | + | for range iterations { | |
| 699 | + | d.touchMRU(sess) | |
| 700 | + | } | |
| 701 | + | }() | |
| 702 | + | } | |
| 703 | + | close(start) | |
| 704 | + | wg.Wait() | |
| 705 | + | close(done) | |
| 706 | + | observer.Wait() | |
| 707 | + | ||
| 708 | + | require.False(t, decreased.Load()) | |
| 709 | + | require.Equal(t, d.mruSeq.Load(), sess.mruAt.Load()) | |
| 710 | + | } | |
| 711 | + | ||
| 662 | 712 | func TestCreateRenameKillPersistenceLifecycle(t *testing.T) { | |
| 663 | 713 | sz := domain.Size{Cols: 80, Rows: 24} | |
| 664 | 714 | p, release := newBlockingPTY(t) | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "io" | 5 | "io" | ||
| 6 | "log/slog" | 6 | "log/slog" | ||
| 7 | "sync" | 7 | "sync" | ||
| 8 | + | "sync/atomic" | |||
| 8 | "testing" | 9 | "testing" | ||
| 9 | "time" | 10 | "time" | ||
| 10 | 11 | ||||
| @@ -659,6 +660,55 @@ func TestDaemonLoadsPersistedSessionsAsStopped(t *testing.T) { | |||||
| 659 | require.Equal(t, stoppedSession{name: "work", cwd: "/tmp/work", createdAt: 7}, stopped) | 660 | require.Equal(t, stoppedSession{name: "work", cwd: "/tmp/work", createdAt: 7}, stopped) | ||
| 660 | } | 661 | } | ||
| 661 | 662 | ||||
| 663 | + | func TestTouchMRUConcurrentUpdatesRemainMonotonic(t *testing.T) { | |||
| 664 | + | d := &Daemon{} | |||
| 665 | + | sess := &session{} | |||
| 666 | + | const goroutines = 16 | |||
| 667 | + | const iterations = 1000 | |||
| 668 | + | start := make(chan struct{}) | |||
| 669 | + | done := make(chan struct{}) | |||
| 670 | + | var decreased atomic.Bool | |||
| 671 | + | var wg sync.WaitGroup | |||
| 672 | + | var observer sync.WaitGroup | |||
| 673 | + | ||||
| 674 | + | observer.Add(1) | |||
| 675 | + | go func() { | |||
| 676 | + | defer observer.Done() | |||
| 677 | + | previous := sess.mruAt.Load() | |||
| 678 | + | for { | |||
| 679 | + | select { | |||
| 680 | + | case <-done: | |||
| 681 | + | return | |||
| 682 | + | default: | |||
| 683 | + | } | |||
| 684 | + | current := sess.mruAt.Load() | |||
| 685 | + | if current < previous { | |||
| 686 | + | decreased.Store(true) | |||
| 687 | + | return | |||
| 688 | + | } | |||
| 689 | + | previous = current | |||
| 690 | + | } | |||
| 691 | + | }() | |||
| 692 | + | ||||
| 693 | + | for range goroutines { | |||
| 694 | + | wg.Add(1) | |||
| 695 | + | go func() { | |||
| 696 | + | defer wg.Done() | |||
| 697 | + | <-start | |||
| 698 | + | for range iterations { | |||
| 699 | + | d.touchMRU(sess) | |||
| 700 | + | } | |||
| 701 | + | }() | |||
| 702 | + | } | |||
| 703 | + | close(start) | |||
| 704 | + | wg.Wait() | |||
| 705 | + | close(done) | |||
| 706 | + | observer.Wait() | |||
| 707 | + | ||||
| 708 | + | require.False(t, decreased.Load()) | |||
| 709 | + | require.Equal(t, d.mruSeq.Load(), sess.mruAt.Load()) | |||
| 710 | + | } | |||
| 711 | + | ||||
| 662 | func TestCreateRenameKillPersistenceLifecycle(t *testing.T) { | 712 | func TestCreateRenameKillPersistenceLifecycle(t *testing.T) { | ||
| 663 | sz := domain.Size{Cols: 80, Rows: 24} | 713 | sz := domain.Size{Cols: 80, Rows: 24} | ||
| 664 | p, release := newBlockingPTY(t) | 714 | p, release := newBlockingPTY(t) | ||