1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package usecase
4
5import (
6 "context"
7 "sync/atomic"
8 "testing"
9 "time"
10
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/mock"
13 "github.com/stretchr/testify/require"
14
15 "bnema.dev/ermite/internal/domain"
16 "bnema.dev/ermite/internal/ports/mocks"
17)
18
19func TestMergeabilityBackoffIsBounded(t *testing.T) {
20 assert.Equal(t, time.Second, mergeBackoff(1))
21 assert.Equal(t, 2*time.Second, mergeBackoff(2))
22 assert.Equal(t, 4*time.Second, mergeBackoff(3))
23 assert.Equal(t, time.Minute, mergeBackoff(20))
24}
25
26func TestMergeabilityReadMarksTheRepoDirty(t *testing.T) {
27 repo, err := domain.ParseRepoName("proj")
28 require.NoError(t, err)
29 oid, err := domain.ParseObjectID("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
30 require.NoError(t, err)
31 store := mocks.NewMergeRequestStore(t)
32 store.EXPECT().GetMergeability(mock.Anything, repo, int64(7)).Return(domain.Mergeability{}, domain.ErrNotFound)
33 svc := NewMergeability(context.Background(), MergeabilityConfig{Store: store})
34 defer svc.Close(context.Background())
35 mr := domain.MergeRequest{Repo: repo, Number: 7, HeadID: oid}
36 assert.Equal(t, domain.MergeabilityChecking, svc.Read(context.Background(), mr, oid).State)
37 // Another MR in the same repository is the same unit of work.
38 mr.Number = 8
39 mr.HeadID = domain.ObjectID{}
40 svc.Read(context.Background(), mr, oid)
41 got, found := svc.next()
42 require.True(t, found)
43 assert.Equal(t, repo, got)
44 _, found = svc.next()
45 assert.False(t, found)
46}
47
48func TestMergeabilityOnePassPerRepoAtATimeAndAgain(t *testing.T) {
49 repo, err := domain.ParseRepoName("proj")
50 require.NoError(t, err)
51 other, err := domain.ParseRepoName("other")
52 require.NoError(t, err)
53 meta, store := mocks.NewRepoMetaStore(t), mocks.NewMergeRequestStore(t)
54 meta.EXPECT().List(mock.Anything).Return(nil, nil)
55 meta.EXPECT().Get(mock.Anything, repo).Return(domain.Repo{Name: repo}, nil)
56 meta.EXPECT().Get(mock.Anything, other).Return(domain.Repo{Name: other}, nil)
57 entered, release, otherPassed := make(chan struct{}, 4), make(chan struct{}), make(chan struct{})
58 var passes, inflight, peak atomic.Int32
59 store.EXPECT().ListOpen(mock.Anything, repo).RunAndReturn(func(context.Context, domain.RepoName) ([]domain.MergeRequest, error) {
60 n := inflight.Add(1)
61 if n > peak.Load() {
62 peak.Store(n)
63 }
64 pass := passes.Add(1)
65 entered <- struct{}{}
66 if pass == 1 {
67 <-release
68 }
69 inflight.Add(-1)
70 return nil, nil
71 })
72 store.EXPECT().ListOpen(mock.Anything, other).RunAndReturn(func(context.Context, domain.RepoName) ([]domain.MergeRequest, error) {
73 close(otherPassed)
74 return nil, nil
75 })
76 svc := NewMergeability(context.Background(), MergeabilityConfig{Store: store, Meta: meta, Workers: 2})
77 svc.Start()
78 svc.OnOpened(context.Background(), domain.MergeRequest{Repo: repo})
79 <-entered
80 for range 50 {
81 svc.OnOpened(context.Background(), domain.MergeRequest{Repo: repo})
82 }
83 svc.OnOpened(context.Background(), domain.MergeRequest{Repo: other})
84 <-otherPassed // the other worker has serviced all work eligible meanwhile
85 assert.EqualValues(t, 1, passes.Load())
86 close(release)
87 <-entered
88 svc.Close(context.Background())
89 assert.EqualValues(t, 2, passes.Load(), "all triggers during a pass coalesce into exactly one more pass")
90 assert.EqualValues(t, 1, peak.Load())
91}