test(config): synchronize watcher timer resets
- commit
- a95a47340511917665eb463b269f92259c9090b2
- author
- brice · · 2026-07-18 14:13 UTC
- parent
- dbc6bb7
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 ( | |||
| 7 | 7 | "path/filepath" | |
| 8 | 8 | "reflect" | |
| 9 | 9 | "strings" | |
| 10 | - | "sync" | |
| 11 | 10 | "testing" | |
| 12 | 11 | "time" | |
| 13 | 12 | ||
| 14 | 13 | "github.com/bnema/vev/internal/domain" | |
| 15 | - | "github.com/bnema/vev/internal/ports" | |
| 14 | + | portsmocks "github.com/bnema/vev/internal/ports/mocks" | |
| 16 | 15 | "github.com/stretchr/testify/require" | |
| 17 | 16 | ) | |
| 18 | 17 | ||
| @@ -587,7 +586,7 @@ func TestWatchReloadsChangedFileWithFakeClock(t *testing.T) { | |||
| 587 | 586 | t.Fatal(err) | |
| 588 | 587 | } | |
| 589 | 588 | ||
| 590 | - | clk := newFakeClock(time.Unix(0, 0)) | |
| 589 | + | clk := newWatchClockMock(t) | |
| 591 | 590 | ctx, cancel := context.WithCancel(context.Background()) | |
| 592 | 591 | defer cancel() | |
| 593 | 592 | ||
| @@ -598,16 +597,16 @@ func TestWatchReloadsChangedFileWithFakeClock(t *testing.T) { | |||
| 598 | 597 | changes := make(chan change, 2) | |
| 599 | 598 | done := make(chan error, 1) | |
| 600 | 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 | 601 | changes <- change{cfg: cfg, warnings: warnings} | |
| 603 | 602 | }) | |
| 604 | 603 | }() | |
| 605 | 604 | ||
| 606 | - | clk.waitForTimers(t, 1) | |
| 605 | + | clk.waitForTimer() | |
| 607 | 606 | if err := os.WriteFile(path, []byte("theme = light\nbad line\n"), 0o600); err != nil { | |
| 608 | 607 | t.Fatal(err) | |
| 609 | 608 | } | |
| 610 | - | clk.advance(2 * time.Second) | |
| 609 | + | clk.fire() | |
| 611 | 610 | ||
| 612 | 611 | var got change | |
| 613 | 612 | select { | |
| @@ -636,7 +635,7 @@ func TestWatchUsesDefaultsWhenReloadLoadFails(t *testing.T) { | |||
| 636 | 635 | t.Fatal(err) | |
| 637 | 636 | } | |
| 638 | 637 | ||
| 639 | - | clk := newFakeClock(time.Unix(0, 0)) | |
| 638 | + | clk := newWatchClockMock(t) | |
| 640 | 639 | ctx, cancel := context.WithCancel(context.Background()) | |
| 641 | 640 | defer cancel() | |
| 642 | 641 | ||
| @@ -647,17 +646,17 @@ func TestWatchUsesDefaultsWhenReloadLoadFails(t *testing.T) { | |||
| 647 | 646 | changes := make(chan change, 2) | |
| 648 | 647 | done := make(chan error, 1) | |
| 649 | 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 | 650 | changes <- change{cfg: cfg, warnings: warnings} | |
| 652 | 651 | }) | |
| 653 | 652 | }() | |
| 654 | 653 | ||
| 655 | - | clk.waitForTimers(t, 1) | |
| 654 | + | clk.waitForTimer() | |
| 656 | 655 | tooLong := strings.Repeat("x", 70*1024) | |
| 657 | 656 | if err := os.WriteFile(path, []byte(tooLong), 0o600); err != nil { | |
| 658 | 657 | t.Fatal(err) | |
| 659 | 658 | } | |
| 660 | - | clk.advance(2 * time.Second) | |
| 659 | + | clk.fire() | |
| 661 | 660 | ||
| 662 | 661 | got := <-changes | |
| 663 | 662 | require.Equal(t, domain.Defaults(), got.cfg) | |
| @@ -673,23 +672,23 @@ func TestWatchDoesNotRepeatPersistentStatError(t *testing.T) { | |||
| 673 | 672 | t.Parallel() | |
| 674 | 673 | ||
| 675 | 674 | path := filepath.Join(t.TempDir(), strings.Repeat("x", 5000)) | |
| 676 | - | clk := newFakeClock(time.Unix(0, 0)) | |
| 675 | + | clk := newWatchClockMock(t) | |
| 677 | 676 | ctx, cancel := context.WithCancel(context.Background()) | |
| 678 | 677 | defer cancel() | |
| 679 | 678 | ||
| 680 | 679 | changes := make(chan []domain.Warning, 4) | |
| 681 | 680 | done := make(chan error, 1) | |
| 682 | 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 | 683 | changes <- warnings | |
| 685 | 684 | }) | |
| 686 | 685 | }() | |
| 687 | 686 | ||
| 688 | 687 | first := <-changes | |
| 689 | 688 | 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() | |
| 693 | 692 | ||
| 694 | 693 | select { | |
| 695 | 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) | |
| 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{} | |
| 734 | 711 | } | |
| 735 | 712 | ||
| 736 | - | func (c *fakeClock) waitForTimers(t *testing.T, n int) { | |
| 713 | + | func newWatchClockMock(t *testing.T) *watchClockMock { | |
| 737 | 714 | 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{}), | |
| 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 { | |
| 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") | |
| 766 | 741 | } | |
| 767 | 742 | } | |
| 768 | 743 | ||
| 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 | + | } | |
| 798 | 756 | } | |
| @@ -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 change | 611 | 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 := <-changes | 661 | 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 <- warnings | 683 | changes <- warnings | ||
| 685 | }) | 684 | }) | ||
| 686 | }() | 685 | }() | ||
| 687 | 686 | ||||
| 688 | first := <-changes | 687 | 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.Mutex | 709 | + | ready chan struct{} |
| 711 | - | now time.Time | 710 | + | 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 *fakeClock | 745 | + | m.t.Helper() |
| 771 | - | mu sync.Mutex | 746 | + | select { |
| 772 | - | ch chan time.Time | 747 | + | case m.ticks <- time.Now(): |
| 773 | - | next time.Time | 748 | + | case <-time.After(time.Second): |
| 774 | - | active bool | 749 | + | 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 ( | |||
| 6 | 6 | "path/filepath" | |
| 7 | 7 | "strings" | |
| 8 | 8 | "testing" | |
| 9 | - | "time" | |
| 10 | 9 | ||
| 11 | 10 | "github.com/bnema/vev/internal/domain" | |
| 12 | 11 | "github.com/stretchr/testify/require" | |
| @@ -50,21 +49,22 @@ func TestLoadMissingPaletteAnchorDefaultsToCenter(t *testing.T) { | |||
| 50 | 49 | func TestWatchPaletteAnchorRestoresCenteredDefaultAfterFileRemoval(t *testing.T) { | |
| 51 | 50 | path := filepath.Join(t.TempDir(), "config") | |
| 52 | 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 | 53 | ctx, cancel := context.WithCancel(context.Background()) | |
| 55 | 54 | defer cancel() | |
| 56 | 55 | changes := make(chan domain.Config, 2) | |
| 57 | 56 | 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() | |
| 60 | 61 | ||
| 61 | 62 | require.NoError(t, os.WriteFile(path, []byte("palette.anchor = bottom-left\n"), 0o600)) | |
| 62 | - | clk.advance(2 * time.Second) | |
| 63 | + | clk.fire() | |
| 63 | 64 | require.Equal(t, domain.AnchorBottomLeft, (<-changes).Palette.Anchor) | |
| 64 | 65 | ||
| 65 | 66 | require.NoError(t, os.Remove(path)) | |
| 66 | - | clk.waitForTimers(t, 1) | |
| 67 | - | clk.advance(2 * time.Second) | |
| 67 | + | clk.fire() | |
| 68 | 68 | got := <-changes | |
| 69 | 69 | require.Equal(t, domain.PaletteConfig{Anchor: domain.AnchorCenter, AnchorSet: true}, got.Palette) | |
| 70 | 70 | 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 := <-changes | 68 | 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) { | |||
| 118 | 118 | require.Empty(t, warnings) | |
| 119 | 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 | 122 | ctx, cancel := context.WithCancel(context.Background()) | |
| 123 | 123 | defer cancel() | |
| 124 | 124 | type change struct { | |
| @@ -128,14 +128,14 @@ func TestWatchThemeAccentInvalidReloadFallsBackToAuto(t *testing.T) { | |||
| 128 | 128 | changes := make(chan change, 1) | |
| 129 | 129 | done := make(chan error, 1) | |
| 130 | 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 | 132 | changes <- change{cfg: cfg, warnings: warnings} | |
| 133 | 133 | }) | |
| 134 | 134 | }() | |
| 135 | 135 | ||
| 136 | - | clk.waitForTimers(t, 1) | |
| 136 | + | clk.waitForTimer() | |
| 137 | 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 | 140 | select { | |
| 141 | 141 | 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: | ||