detached a95a473

test(config): synchronize watcher timer resets

commit
a95a47340511917665eb463b269f92259c9090b2
author
brice · · 2026-07-18 14:13 UTC
parent
dbc6bb7
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +67 -109
M internal/adapters/config/config_test.go +56 -98
@@ -7,12 +7,11 @@ import (
77 "path/filepath"
88 "reflect"
99 "strings"
10- "sync"
1110 "testing"
1211 "time"
1312
1413 "github.com/bnema/vev/internal/domain"
15- "github.com/bnema/vev/internal/ports"
14+ portsmocks "github.com/bnema/vev/internal/ports/mocks"
1615 "github.com/stretchr/testify/require"
1716 )
1817
@@ -587,7 +586,7 @@ func TestWatchReloadsChangedFileWithFakeClock(t *testing.T) {
587586 t.Fatal(err)
588587 }
589588
590- clk := newFakeClock(time.Unix(0, 0))
589+ clk := newWatchClockMock(t)
591590 ctx, cancel := context.WithCancel(context.Background())
592591 defer cancel()
593592
@@ -598,16 +597,16 @@ func TestWatchReloadsChangedFileWithFakeClock(t *testing.T) {
598597 changes := make(chan change, 2)
599598 done := make(chan error, 1)
600599 go func() {
601- done <- Watch(ctx, clk, path, func(cfg domain.Config, warnings []domain.Warning) {
600+ done <- Watch(ctx, clk.clock, path, func(cfg domain.Config, warnings []domain.Warning) {
602601 changes <- change{cfg: cfg, warnings: warnings}
603602 })
604603 }()
605604
606- clk.waitForTimers(t, 1)
605+ clk.waitForTimer()
607606 if err := os.WriteFile(path, []byte("theme = light\nbad line\n"), 0o600); err != nil {
608607 t.Fatal(err)
609608 }
610- clk.advance(2 * time.Second)
609+ clk.fire()
611610
612611 var got change
613612 select {
@@ -636,7 +635,7 @@ func TestWatchUsesDefaultsWhenReloadLoadFails(t *testing.T) {
636635 t.Fatal(err)
637636 }
638637
639- clk := newFakeClock(time.Unix(0, 0))
638+ clk := newWatchClockMock(t)
640639 ctx, cancel := context.WithCancel(context.Background())
641640 defer cancel()
642641
@@ -647,17 +646,17 @@ func TestWatchUsesDefaultsWhenReloadLoadFails(t *testing.T) {
647646 changes := make(chan change, 2)
648647 done := make(chan error, 1)
649648 go func() {
650- done <- Watch(ctx, clk, path, func(cfg domain.Config, warnings []domain.Warning) {
649+ done <- Watch(ctx, clk.clock, path, func(cfg domain.Config, warnings []domain.Warning) {
651650 changes <- change{cfg: cfg, warnings: warnings}
652651 })
653652 }()
654653
655- clk.waitForTimers(t, 1)
654+ clk.waitForTimer()
656655 tooLong := strings.Repeat("x", 70*1024)
657656 if err := os.WriteFile(path, []byte(tooLong), 0o600); err != nil {
658657 t.Fatal(err)
659658 }
660- clk.advance(2 * time.Second)
659+ clk.fire()
661660
662661 got := <-changes
663662 require.Equal(t, domain.Defaults(), got.cfg)
@@ -673,23 +672,23 @@ func TestWatchDoesNotRepeatPersistentStatError(t *testing.T) {
673672 t.Parallel()
674673
675674 path := filepath.Join(t.TempDir(), strings.Repeat("x", 5000))
676- clk := newFakeClock(time.Unix(0, 0))
675+ clk := newWatchClockMock(t)
677676 ctx, cancel := context.WithCancel(context.Background())
678677 defer cancel()
679678
680679 changes := make(chan []domain.Warning, 4)
681680 done := make(chan error, 1)
682681 go func() {
683- done <- Watch(ctx, clk, path, func(_ domain.Config, warnings []domain.Warning) {
682+ done <- Watch(ctx, clk.clock, path, func(_ domain.Config, warnings []domain.Warning) {
684683 changes <- warnings
685684 })
686685 }()
687686
688687 first := <-changes
689688 require.Len(t, first, 1)
690- clk.waitForTimers(t, 1)
691- clk.advance(2 * time.Second)
692- clk.advance(2 * time.Second)
689+ clk.waitForTimer()
690+ clk.fire()
691+ clk.fire()
693692
694693 select {
695694 case extra := <-changes:
@@ -703,96 +702,55 @@ func TestWatchDoesNotRepeatPersistentStatError(t *testing.T) {
703702 }
704703 }
705704
706-var _ ports.Clock = (*fakeClock)(nil)
707-var _ ports.Timer = (*fakeTimer)(nil)
708-
709-type fakeClock struct {
710- mu sync.Mutex
711- now time.Time
712- timers []*fakeTimer
713- notify chan struct{}
714-}
715-
716-func newFakeClock(now time.Time) *fakeClock {
717- return &fakeClock{now: now, notify: make(chan struct{}, 16)}
718-}
719-
720-func (c *fakeClock) Now() time.Time {
721- c.mu.Lock()
722- defer c.mu.Unlock()
723- return c.now
724-}
725-
726-func (c *fakeClock) NewTimer(d time.Duration) ports.Timer {
727- c.mu.Lock()
728- defer c.mu.Unlock()
729-
730- t := &fakeTimer{clock: c, ch: make(chan time.Time, 1), next: c.now.Add(d), active: true}
731- c.timers = append(c.timers, t)
732- c.notify <- struct{}{}
733- return t
705+type watchClockMock struct {
706+ t *testing.T
707+ clock *portsmocks.MockClock
708+ ticks chan time.Time
709+ ready chan struct{}
710+ resets chan struct{}
734711 }
735712
736-func (c *fakeClock) waitForTimers(t *testing.T, n int) {
713+func newWatchClockMock(t *testing.T) *watchClockMock {
737714 t.Helper()
738- deadline := time.After(2 * time.Second)
739- for c.timerCount() < n {
740- select {
741- case <-c.notify:
742- case <-deadline:
743- t.Fatalf("timed out waiting for %d timers, got %d", n, c.timerCount())
744- }
715+ timer := portsmocks.NewMockTimer(t)
716+ clock := portsmocks.NewMockClock(t)
717+ m := &watchClockMock{
718+ t: t,
719+ clock: clock,
720+ ticks: make(chan time.Time),
721+ ready: make(chan struct{}),
722+ resets: make(chan struct{}),
745723 }
724+ timer.EXPECT().C().Return(m.ticks)
725+ timer.EXPECT().Reset(pollInterval).Run(func(time.Duration) {
726+ m.resets <- struct{}{}
727+ }).Return(false)
728+ timer.EXPECT().Stop().Return(true)
729+ clock.EXPECT().NewTimer(pollInterval).Run(func(time.Duration) {
730+ close(m.ready)
731+ }).Return(timer).Once()
732+ return m
746733 }
747734
748-func (c *fakeClock) timerCount() int {
749- c.mu.Lock()
750- defer c.mu.Unlock()
751- return len(c.timers)
752-}
753-
754-func (c *fakeClock) advance(d time.Duration) {
755- c.mu.Lock()
756- defer c.mu.Unlock()
757-
758- c.now = c.now.Add(d)
759- for _, timer := range c.timers {
760- timer.mu.Lock()
761- if timer.active && !timer.next.After(c.now) {
762- timer.ch <- c.now
763- timer.active = false
764- }
765- timer.mu.Unlock()
735+func (m *watchClockMock) waitForTimer() {
736+ m.t.Helper()
737+ select {
738+ case <-m.ready:
739+ case <-time.After(time.Second):
740+ m.t.Fatal("timed out waiting for watcher timer creation")
766741 }
767742 }
768743
769-type fakeTimer struct {
770- clock *fakeClock
771- mu sync.Mutex
772- ch chan time.Time
773- next time.Time
774- active bool
775-}
776-
777-func (t *fakeTimer) C() <-chan time.Time { return t.ch }
778-
779-func (t *fakeTimer) Reset(d time.Duration) bool {
780- t.clock.mu.Lock()
781- defer t.clock.mu.Unlock()
782- t.mu.Lock()
783- defer t.mu.Unlock()
784-
785- wasActive := t.active
786- t.active = true
787- t.next = t.clock.now.Add(d)
788- return wasActive
789-}
790-
791-func (t *fakeTimer) Stop() bool {
792- t.mu.Lock()
793- defer t.mu.Unlock()
794-
795- wasActive := t.active
796- t.active = false
797- return wasActive
744+func (m *watchClockMock) fire() {
745+ m.t.Helper()
746+ select {
747+ case m.ticks <- time.Now():
748+ case <-time.After(time.Second):
749+ m.t.Fatal("timed out firing watcher timer")
750+ }
751+ select {
752+ case <-m.resets:
753+ case <-time.After(time.Second):
754+ m.t.Fatal("timed out waiting for watcher timer reset")
755+ }
798756 }
@@ -7,12 +7,11 @@ import (
7 "path/filepath"7 "path/filepath"
8 "reflect"8 "reflect"
9 "strings"9 "strings"
10- "sync"
11 "testing"10 "testing"
12 "time"11 "time"
13 12
14 "github.com/bnema/vev/internal/domain"13 "github.com/bnema/vev/internal/domain"
15- "github.com/bnema/vev/internal/ports"14+ portsmocks "github.com/bnema/vev/internal/ports/mocks"
16 "github.com/stretchr/testify/require"15 "github.com/stretchr/testify/require"
17 )16 )
18 17
@@ -587,7 +586,7 @@ func TestWatchReloadsChangedFileWithFakeClock(t *testing.T) {
587 t.Fatal(err)586 t.Fatal(err)
588 }587 }
589 588
590- clk := newFakeClock(time.Unix(0, 0))589+ clk := newWatchClockMock(t)
591 ctx, cancel := context.WithCancel(context.Background())590 ctx, cancel := context.WithCancel(context.Background())
592 defer cancel()591 defer cancel()
593 592
@@ -598,16 +597,16 @@ func TestWatchReloadsChangedFileWithFakeClock(t *testing.T) {
598 changes := make(chan change, 2)597 changes := make(chan change, 2)
599 done := make(chan error, 1)598 done := make(chan error, 1)
600 go func() {599 go func() {
601- done <- Watch(ctx, clk, path, func(cfg domain.Config, warnings []domain.Warning) {600+ done <- Watch(ctx, clk.clock, path, func(cfg domain.Config, warnings []domain.Warning) {
602 changes <- change{cfg: cfg, warnings: warnings}601 changes <- change{cfg: cfg, warnings: warnings}
603 })602 })
604 }()603 }()
605 604
606- clk.waitForTimers(t, 1)605+ clk.waitForTimer()
607 if err := os.WriteFile(path, []byte("theme = light\nbad line\n"), 0o600); err != nil {606 if err := os.WriteFile(path, []byte("theme = light\nbad line\n"), 0o600); err != nil {
608 t.Fatal(err)607 t.Fatal(err)
609 }608 }
610- clk.advance(2 * time.Second)609+ clk.fire()
611 610
612 var got change611 var got change
613 select {612 select {
@@ -636,7 +635,7 @@ func TestWatchUsesDefaultsWhenReloadLoadFails(t *testing.T) {
636 t.Fatal(err)635 t.Fatal(err)
637 }636 }
638 637
639- clk := newFakeClock(time.Unix(0, 0))638+ clk := newWatchClockMock(t)
640 ctx, cancel := context.WithCancel(context.Background())639 ctx, cancel := context.WithCancel(context.Background())
641 defer cancel()640 defer cancel()
642 641
@@ -647,17 +646,17 @@ func TestWatchUsesDefaultsWhenReloadLoadFails(t *testing.T) {
647 changes := make(chan change, 2)646 changes := make(chan change, 2)
648 done := make(chan error, 1)647 done := make(chan error, 1)
649 go func() {648 go func() {
650- done <- Watch(ctx, clk, path, func(cfg domain.Config, warnings []domain.Warning) {649+ done <- Watch(ctx, clk.clock, path, func(cfg domain.Config, warnings []domain.Warning) {
651 changes <- change{cfg: cfg, warnings: warnings}650 changes <- change{cfg: cfg, warnings: warnings}
652 })651 })
653 }()652 }()
654 653
655- clk.waitForTimers(t, 1)654+ clk.waitForTimer()
656 tooLong := strings.Repeat("x", 70*1024)655 tooLong := strings.Repeat("x", 70*1024)
657 if err := os.WriteFile(path, []byte(tooLong), 0o600); err != nil {656 if err := os.WriteFile(path, []byte(tooLong), 0o600); err != nil {
658 t.Fatal(err)657 t.Fatal(err)
659 }658 }
660- clk.advance(2 * time.Second)659+ clk.fire()
661 660
662 got := <-changes661 got := <-changes
663 require.Equal(t, domain.Defaults(), got.cfg)662 require.Equal(t, domain.Defaults(), got.cfg)
@@ -673,23 +672,23 @@ func TestWatchDoesNotRepeatPersistentStatError(t *testing.T) {
673 t.Parallel()672 t.Parallel()
674 673
675 path := filepath.Join(t.TempDir(), strings.Repeat("x", 5000))674 path := filepath.Join(t.TempDir(), strings.Repeat("x", 5000))
676- clk := newFakeClock(time.Unix(0, 0))675+ clk := newWatchClockMock(t)
677 ctx, cancel := context.WithCancel(context.Background())676 ctx, cancel := context.WithCancel(context.Background())
678 defer cancel()677 defer cancel()
679 678
680 changes := make(chan []domain.Warning, 4)679 changes := make(chan []domain.Warning, 4)
681 done := make(chan error, 1)680 done := make(chan error, 1)
682 go func() {681 go func() {
683- done <- Watch(ctx, clk, path, func(_ domain.Config, warnings []domain.Warning) {682+ done <- Watch(ctx, clk.clock, path, func(_ domain.Config, warnings []domain.Warning) {
684 changes <- warnings683 changes <- warnings
685 })684 })
686 }()685 }()
687 686
688 first := <-changes687 first := <-changes
689 require.Len(t, first, 1)688 require.Len(t, first, 1)
690- clk.waitForTimers(t, 1)689+ clk.waitForTimer()
691- clk.advance(2 * time.Second)690+ clk.fire()
692- clk.advance(2 * time.Second)691+ clk.fire()
693 692
694 select {693 select {
695 case extra := <-changes:694 case extra := <-changes:
@@ -703,96 +702,55 @@ func TestWatchDoesNotRepeatPersistentStatError(t *testing.T) {
703 }702 }
704 }703 }
705 704
706-var _ ports.Clock = (*fakeClock)(nil)705+type watchClockMock struct {
707-var _ ports.Timer = (*fakeTimer)(nil)706+ t *testing.T
708-707+ clock *portsmocks.MockClock
709-type fakeClock struct {708+ ticks chan time.Time
710- mu sync.Mutex709+ ready chan struct{}
711- now time.Time710+ resets chan struct{}
712- timers []*fakeTimer
713- notify chan struct{}
714-}
715-
716-func newFakeClock(now time.Time) *fakeClock {
717- return &fakeClock{now: now, notify: make(chan struct{}, 16)}
718-}
719-
720-func (c *fakeClock) Now() time.Time {
721- c.mu.Lock()
722- defer c.mu.Unlock()
723- return c.now
724-}
725-
726-func (c *fakeClock) NewTimer(d time.Duration) ports.Timer {
727- c.mu.Lock()
728- defer c.mu.Unlock()
729-
730- t := &fakeTimer{clock: c, ch: make(chan time.Time, 1), next: c.now.Add(d), active: true}
731- c.timers = append(c.timers, t)
732- c.notify <- struct{}{}
733- return t
734 }711 }
735 712
736-func (c *fakeClock) waitForTimers(t *testing.T, n int) {713+func newWatchClockMock(t *testing.T) *watchClockMock {
737 t.Helper()714 t.Helper()
738- deadline := time.After(2 * time.Second)715+ timer := portsmocks.NewMockTimer(t)
739- for c.timerCount() < n {716+ clock := portsmocks.NewMockClock(t)
740- select {717+ m := &watchClockMock{
741- case <-c.notify:718+ t: t,
742- case <-deadline:719+ clock: clock,
743- t.Fatalf("timed out waiting for %d timers, got %d", n, c.timerCount())720+ ticks: make(chan time.Time),
744- }721+ ready: make(chan struct{}),
722+ resets: make(chan struct{}),
745 }723 }
724+ timer.EXPECT().C().Return(m.ticks)
725+ timer.EXPECT().Reset(pollInterval).Run(func(time.Duration) {
726+ m.resets <- struct{}{}
727+ }).Return(false)
728+ timer.EXPECT().Stop().Return(true)
729+ clock.EXPECT().NewTimer(pollInterval).Run(func(time.Duration) {
730+ close(m.ready)
731+ }).Return(timer).Once()
732+ return m
746 }733 }
747 734
748-func (c *fakeClock) timerCount() int {735+func (m *watchClockMock) waitForTimer() {
749- c.mu.Lock()736+ m.t.Helper()
750- defer c.mu.Unlock()737+ select {
751- return len(c.timers)738+ case <-m.ready:
752-}739+ case <-time.After(time.Second):
753-740+ m.t.Fatal("timed out waiting for watcher timer creation")
754-func (c *fakeClock) advance(d time.Duration) {
755- c.mu.Lock()
756- defer c.mu.Unlock()
757-
758- c.now = c.now.Add(d)
759- for _, timer := range c.timers {
760- timer.mu.Lock()
761- if timer.active && !timer.next.After(c.now) {
762- timer.ch <- c.now
763- timer.active = false
764- }
765- timer.mu.Unlock()
766 }741 }
767 }742 }
768 743
769-type fakeTimer struct {744+func (m *watchClockMock) fire() {
770- clock *fakeClock745+ m.t.Helper()
771- mu sync.Mutex746+ select {
772- ch chan time.Time747+ case m.ticks <- time.Now():
773- next time.Time748+ case <-time.After(time.Second):
774- active bool749+ m.t.Fatal("timed out firing watcher timer")
775-}750+ }
776-751+ select {
777-func (t *fakeTimer) C() <-chan time.Time { return t.ch }752+ case <-m.resets:
778-753+ case <-time.After(time.Second):
779-func (t *fakeTimer) Reset(d time.Duration) bool {754+ m.t.Fatal("timed out waiting for watcher timer reset")
780- t.clock.mu.Lock()755+ }
781- defer t.clock.mu.Unlock()
782- t.mu.Lock()
783- defer t.mu.Unlock()
784-
785- wasActive := t.active
786- t.active = true
787- t.next = t.clock.now.Add(d)
788- return wasActive
789-}
790-
791-func (t *fakeTimer) Stop() bool {
792- t.mu.Lock()
793- defer t.mu.Unlock()
794-
795- wasActive := t.active
796- t.active = false
797- return wasActive
798 }756 }
M internal/adapters/config/palette_anchor_test.go +7 -7
@@ -6,7 +6,6 @@ import (
66 "path/filepath"
77 "strings"
88 "testing"
9- "time"
109
1110 "github.com/bnema/vev/internal/domain"
1211 "github.com/stretchr/testify/require"
@@ -50,21 +49,22 @@ func TestLoadMissingPaletteAnchorDefaultsToCenter(t *testing.T) {
5049 func TestWatchPaletteAnchorRestoresCenteredDefaultAfterFileRemoval(t *testing.T) {
5150 path := filepath.Join(t.TempDir(), "config")
5251 require.NoError(t, os.WriteFile(path, []byte("palette.anchor = top-right\n"), 0o600))
53- clk := newFakeClock(time.Unix(0, 0))
52+ clk := newWatchClockMock(t)
5453 ctx, cancel := context.WithCancel(context.Background())
5554 defer cancel()
5655 changes := make(chan domain.Config, 2)
5756 done := make(chan error, 1)
58- go func() { done <- Watch(ctx, clk, path, func(cfg domain.Config, _ []domain.Warning) { changes <- cfg }) }()
59- clk.waitForTimers(t, 1)
57+ go func() {
58+ done <- Watch(ctx, clk.clock, path, func(cfg domain.Config, _ []domain.Warning) { changes <- cfg })
59+ }()
60+ clk.waitForTimer()
6061
6162 require.NoError(t, os.WriteFile(path, []byte("palette.anchor = bottom-left\n"), 0o600))
62- clk.advance(2 * time.Second)
63+ clk.fire()
6364 require.Equal(t, domain.AnchorBottomLeft, (<-changes).Palette.Anchor)
6465
6566 require.NoError(t, os.Remove(path))
66- clk.waitForTimers(t, 1)
67- clk.advance(2 * time.Second)
67+ clk.fire()
6868 got := <-changes
6969 require.Equal(t, domain.PaletteConfig{Anchor: domain.AnchorCenter, AnchorSet: true}, got.Palette)
7070 cancel()
@@ -6,7 +6,6 @@ import (
6 "path/filepath"6 "path/filepath"
7 "strings"7 "strings"
8 "testing"8 "testing"
9- "time"
10 9
11 "github.com/bnema/vev/internal/domain"10 "github.com/bnema/vev/internal/domain"
12 "github.com/stretchr/testify/require"11 "github.com/stretchr/testify/require"
@@ -50,21 +49,22 @@ func TestLoadMissingPaletteAnchorDefaultsToCenter(t *testing.T) {
50 func TestWatchPaletteAnchorRestoresCenteredDefaultAfterFileRemoval(t *testing.T) {49 func TestWatchPaletteAnchorRestoresCenteredDefaultAfterFileRemoval(t *testing.T) {
51 path := filepath.Join(t.TempDir(), "config")50 path := filepath.Join(t.TempDir(), "config")
52 require.NoError(t, os.WriteFile(path, []byte("palette.anchor = top-right\n"), 0o600))51 require.NoError(t, os.WriteFile(path, []byte("palette.anchor = top-right\n"), 0o600))
53- clk := newFakeClock(time.Unix(0, 0))52+ clk := newWatchClockMock(t)
54 ctx, cancel := context.WithCancel(context.Background())53 ctx, cancel := context.WithCancel(context.Background())
55 defer cancel()54 defer cancel()
56 changes := make(chan domain.Config, 2)55 changes := make(chan domain.Config, 2)
57 done := make(chan error, 1)56 done := make(chan error, 1)
58- go func() { done <- Watch(ctx, clk, path, func(cfg domain.Config, _ []domain.Warning) { changes <- cfg }) }()57+ go func() {
59- clk.waitForTimers(t, 1)58+ done <- Watch(ctx, clk.clock, path, func(cfg domain.Config, _ []domain.Warning) { changes <- cfg })
59+ }()
60+ clk.waitForTimer()
60 61
61 require.NoError(t, os.WriteFile(path, []byte("palette.anchor = bottom-left\n"), 0o600))62 require.NoError(t, os.WriteFile(path, []byte("palette.anchor = bottom-left\n"), 0o600))
62- clk.advance(2 * time.Second)63+ clk.fire()
63 require.Equal(t, domain.AnchorBottomLeft, (<-changes).Palette.Anchor)64 require.Equal(t, domain.AnchorBottomLeft, (<-changes).Palette.Anchor)
64 65
65 require.NoError(t, os.Remove(path))66 require.NoError(t, os.Remove(path))
66- clk.waitForTimers(t, 1)67+ clk.fire()
67- clk.advance(2 * time.Second)
68 got := <-changes68 got := <-changes
69 require.Equal(t, domain.PaletteConfig{Anchor: domain.AnchorCenter, AnchorSet: true}, got.Palette)69 require.Equal(t, domain.PaletteConfig{Anchor: domain.AnchorCenter, AnchorSet: true}, got.Palette)
70 cancel()70 cancel()
M internal/adapters/config/theme_accent_test.go +4 -4
@@ -118,7 +118,7 @@ func TestWatchThemeAccentInvalidReloadFallsBackToAuto(t *testing.T) {
118118 require.Empty(t, warnings)
119119 require.Equal(t, domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 2}, initial.ThemeAccent)
120120
121- clk := newFakeClock(time.Unix(0, 0))
121+ clk := newWatchClockMock(t)
122122 ctx, cancel := context.WithCancel(context.Background())
123123 defer cancel()
124124 type change struct {
@@ -128,14 +128,14 @@ func TestWatchThemeAccentInvalidReloadFallsBackToAuto(t *testing.T) {
128128 changes := make(chan change, 1)
129129 done := make(chan error, 1)
130130 go func() {
131- done <- Watch(ctx, clk, path, func(cfg domain.Config, warnings []domain.Warning) {
131+ done <- Watch(ctx, clk.clock, path, func(cfg domain.Config, warnings []domain.Warning) {
132132 changes <- change{cfg: cfg, warnings: warnings}
133133 })
134134 }()
135135
136- clk.waitForTimers(t, 1)
136+ clk.waitForTimer()
137137 require.NoError(t, os.WriteFile(path, []byte("theme.accent = invalid\n"), 0o600))
138- clk.advance(2 * time.Second)
138+ clk.fire()
139139
140140 select {
141141 case got := <-changes:
@@ -118,7 +118,7 @@ func TestWatchThemeAccentInvalidReloadFallsBackToAuto(t *testing.T) {
118 require.Empty(t, warnings)118 require.Empty(t, warnings)
119 require.Equal(t, domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 2}, initial.ThemeAccent)119 require.Equal(t, domain.ThemeAccent{Mode: domain.ThemeAccentSlot, Slot: 2}, initial.ThemeAccent)
120 120
121- clk := newFakeClock(time.Unix(0, 0))121+ clk := newWatchClockMock(t)
122 ctx, cancel := context.WithCancel(context.Background())122 ctx, cancel := context.WithCancel(context.Background())
123 defer cancel()123 defer cancel()
124 type change struct {124 type change struct {
@@ -128,14 +128,14 @@ func TestWatchThemeAccentInvalidReloadFallsBackToAuto(t *testing.T) {
128 changes := make(chan change, 1)128 changes := make(chan change, 1)
129 done := make(chan error, 1)129 done := make(chan error, 1)
130 go func() {130 go func() {
131- done <- Watch(ctx, clk, path, func(cfg domain.Config, warnings []domain.Warning) {131+ done <- Watch(ctx, clk.clock, path, func(cfg domain.Config, warnings []domain.Warning) {
132 changes <- change{cfg: cfg, warnings: warnings}132 changes <- change{cfg: cfg, warnings: warnings}
133 })133 })
134 }()134 }()
135 135
136- clk.waitForTimers(t, 1)136+ clk.waitForTimer()
137 require.NoError(t, os.WriteFile(path, []byte("theme.accent = invalid\n"), 0o600))137 require.NoError(t, os.WriteFile(path, []byte("theme.accent = invalid\n"), 0o600))
138- clk.advance(2 * time.Second)138+ clk.fire()
139 139
140 select {140 select {
141 case got := <-changes:141 case got := <-changes:

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close