1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain_test
4
5import (
6 "strings"
7 "testing"
8
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"
11
12 "bnema.dev/ermite/internal/domain"
13)
14
15// Groups compare like .NET's OrdinalIgnoreCase: upper-cased character by
16// character, a non-ASCII character whose upper case is ASCII keeping itself.
17func TestConcurrencyKeyFoldsLikeOrdinalIgnoreCase(t *testing.T) {
18 t.Parallel()
19 k := domain.ConcurrencyKey
20 // The three sigmas are one letter.
21 assert.Equal(t, k("Σ"), k("σ"))
22 assert.Equal(t, k("σ"), k("ς"))
23 assert.Equal(t, k("ΟΔΟΣ"), k("οδος"))
24 // ASCII and ordinary letters.
25 assert.Equal(t, k("Deploy"), k("DEPLOY"))
26 assert.Equal(t, k("É"), k("é"))
27 assert.NotEqual(t, k("a"), k("b"))
28 // The Turkish i's: i == I, but ı (dotless) and İ (dotted capital) are not I.
29 assert.Equal(t, k("i"), k("I"))
30 assert.NotEqual(t, k("ı"), k("I"))
31 assert.NotEqual(t, k("İ"), k("I"))
32 assert.NotEqual(t, k("ı"), k("i"))
33 assert.NotEqual(t, k("İ"), k("i"))
34 assert.NotEqual(t, k("ı"), k("İ"))
35 // ſ (long s) is not S.
36 assert.NotEqual(t, k("ſ"), k("S"))
37 assert.NotEqual(t, k("ſ"), k("s"))
38 // One character stays one character.
39 assert.Len(t, []rune(k("ß")), 1)
40 assert.NotEqual(t, k("ß"), k("ss"))
41}
42
43func TestConcurrencyGroupLengthCountsUTF16Units(t *testing.T) {
44 t.Parallel()
45 assert.Equal(t, 400, domain.ConcurrencyGroupLength(strings.Repeat("é", 400)), "a BMP character is one unit")
46 assert.Equal(t, 2, domain.ConcurrencyGroupLength("😀"), "a supplementary character is two")
47 assert.Equal(t, 400, domain.ConcurrencyGroupLength(strings.Repeat("😀", 200)))
48}
49
50// queue: max lines up to GitHub's 100 waiting entries in order and rejects one
51// more; a waiting entry that leaves keeps the order of the others; the head
52// takes the free running place.
53func TestConcurrencyQueueMax(t *testing.T) {
54 t.Parallel()
55 assert.Equal(t, 100, domain.CIMaxConcurrencyQueue)
56 rec := domain.CIConcurrencyRecord{Running: &domain.CIConcurrencyEntry{RunID: 1, Attempt: 1}}
57 for i := range domain.CIMaxConcurrencyQueue {
58 replaced, rejected := rec.Enqueue(domain.CIConcurrencyEntry{RunID: int64(10 + i), Attempt: 1, Max: true}, domain.CIMaxConcurrencyQueue)
59 require.False(t, rejected, "entry %d", i)
60 require.Empty(t, replaced)
61 }
62 require.Len(t, rec.Pending, domain.CIMaxConcurrencyQueue)
63 replaced, rejected := rec.Enqueue(domain.CIConcurrencyEntry{RunID: 999, Attempt: 1, Max: true}, domain.CIMaxConcurrencyQueue)
64 assert.True(t, rejected, "the 101st waiting entry is rejected")
65 assert.Empty(t, replaced)
66 assert.Len(t, rec.Pending, domain.CIMaxConcurrencyQueue, "the line is left as it was")
67 assert.False(t, rec.Names(domain.CIConcurrencyEntry{RunID: 999, Attempt: 1}))
68
69 assert.True(t, rec.RemovePending(domain.CIConcurrencyEntry{RunID: 11, Attempt: 1}))
70 assert.False(t, rec.RemovePending(domain.CIConcurrencyEntry{RunID: 11, Attempt: 1}))
71 assert.EqualValues(t, []int64{10, 12, 13}, []int64{rec.Pending[0].RunID, rec.Pending[1].RunID, rec.Pending[2].RunID})
72
73 assert.Nil(t, rec.Promote(), "the running place is taken")
74 rec.Running = nil
75 head := rec.Promote()
76 require.NotNil(t, head)
77 assert.EqualValues(t, 10, head.RunID, "first in, first out")
78 assert.EqualValues(t, 12, rec.Pending[0].RunID)
79 assert.Equal(t, 1, rec.PendingIndex(domain.CIConcurrencyEntry{RunID: 13, Attempt: 1}))
80}
81
82// A queue: single entry replaces the whole line, queue: max entries included,
83// and is then the only waiting one; a max entry joins behind it.
84func TestConcurrencyQueueSingleClearsTheLine(t *testing.T) {
85 t.Parallel()
86 rec := domain.CIConcurrencyRecord{Running: &domain.CIConcurrencyEntry{RunID: 1, Attempt: 1}}
87 for i := range 3 {
88 rec.Enqueue(domain.CIConcurrencyEntry{RunID: int64(10 + i), Attempt: 1, Max: true}, 3)
89 }
90 single := domain.CIConcurrencyEntry{RunID: 20, Attempt: 1}
91 replaced, rejected := rec.Enqueue(single, 3)
92 assert.False(t, rejected)
93 require.Len(t, replaced, 3)
94 assert.EqualValues(t, 10, replaced[0].RunID)
95 assert.Equal(t, []domain.CIConcurrencyEntry{single}, rec.Pending)
96 maxEntry := domain.CIConcurrencyEntry{RunID: 21, Attempt: 1, Max: true}
97 replaced, _ = rec.Enqueue(maxEntry, 3)
98 assert.Empty(t, replaced)
99 assert.Equal(t, []domain.CIConcurrencyEntry{single, maxEntry}, rec.Pending)
100 assert.True(t, domain.CIConcurrencyRejected.Owed())
101 assert.True(t, domain.CIConcurrencyReplaced.Owed())
102 assert.False(t, domain.CIConcurrencyPending.Owed())
103 assert.Equal(t, "Canceling since the concurrency group 'g' already has 100 waiting requests", domain.ConcurrencyQueueFullMessage("g"))
104}