Skip to content

Commit fbe04c3

Browse files
committed
feat: add test_flakiness target and AwaitCommandTestCase for improved testing reliability
- Introduced a new Makefile target `test_flakiness` to run tests multiple times for flakiness detection. - Added `AwaitCommandTestCase` struct to handle command execution with await and retry logic in tests. - Updated `testStreamsXreadBlockMaxID` to utilize the new `AwaitCommandTestCase` for better synchronization and error handling. - Enhanced GitHub Actions workflow to include flakiness testing step.
1 parent 2c3dd72 commit fbe04c3

File tree

4 files changed

+118
-10
lines changed

4 files changed

+118
-10
lines changed

.github/workflows/test.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,16 @@ jobs:
4545
with:
4646
version: "2023.1"
4747
install-go: false
48+
49+
test-flakiness:
50+
runs-on: ubuntu-latest
51+
steps:
52+
- name: Checkout
53+
uses: actions/checkout@v4
54+
55+
- name: Set up Go
56+
uses: actions/setup-go@v5
57+
with:
58+
go-version: 1.23.x
59+
60+
- run: RUNS=25 make test_flakiness

Makefile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ record_fixtures:
2929
update_tester_utils:
3030
go get -u github.com/codecrafters-io/tester-utils
3131

32+
TEST_TARGET ?= test
33+
RUNS ?= 100
34+
test_flakiness:
35+
@$(foreach i,$(shell seq 1 $(RUNS)), \
36+
echo "Running iteration $(i)/$(RUNS) of \"make $(TEST_TARGET)\"" ; \
37+
make $(TEST_TARGET) > /tmp/test ; \
38+
if [ "$$?" -ne 0 ]; then \
39+
echo "Test failed on iteration $(i)" ; \
40+
cat /tmp/test ; \
41+
exit 1 ; \
42+
fi ;\
43+
)
44+
3245
test_base_with_redis: build
3346
CODECRAFTERS_REPOSITORY_DIR=./internal/test_helpers/pass_all \
3447
CODECRAFTERS_TEST_CASES_JSON="[{\"slug\":\"jm1\",\"tester_log_prefix\":\"stage-1\",\"title\":\"Stage #1: Bind to a port\"},{\"slug\":\"rg2\",\"tester_log_prefix\":\"stage-2\",\"title\":\"Stage #2: Respond to PING\"},{\"slug\":\"wy1\",\"tester_log_prefix\":\"stage-3\",\"title\":\"Stage #3: Respond to multiple PINGs\"},{\"slug\":\"zu2\",\"tester_log_prefix\":\"stage-4\",\"title\":\"Stage #4: Handle concurrent clients\"},{\"slug\":\"qq0\",\"tester_log_prefix\":\"stage-5\",\"title\":\"Stage #5: Implement the ECHO command\"},{\"slug\":\"la7\",\"tester_log_prefix\":\"stage-6\",\"title\":\"Stage #6: Implement the SET \u0026 GET commands\"},{\"slug\":\"yz1\",\"tester_log_prefix\":\"stage-7\",\"title\":\"Stage #7: Expiry\"}]" \
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package test_cases
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"time"
7+
8+
resp_client "github.com/codecrafters-io/redis-tester/internal/resp/connection"
9+
resp_value "github.com/codecrafters-io/redis-tester/internal/resp/value"
10+
"github.com/codecrafters-io/redis-tester/internal/resp_assertions"
11+
"github.com/codecrafters-io/tester-utils/logger"
12+
)
13+
14+
type AwaitCommandTestCase struct {
15+
Command string
16+
Args []string
17+
Assertion resp_assertions.RESPAssertion
18+
ShouldSkipUnreadDataCheck bool
19+
Retries int
20+
ShouldRetryFunc func(resp_value.Value) bool
21+
22+
// ReceivedResponse is set after the test case is run
23+
ReceivedResponse resp_value.Value
24+
25+
AwaitChan chan bool
26+
DoneChan chan bool
27+
}
28+
29+
func (t *AwaitCommandTestCase) Run(client *resp_client.RespConnection, logger *logger.Logger) error {
30+
var value resp_value.Value
31+
var err error
32+
33+
for attempt := 0; attempt <= t.Retries; attempt++ {
34+
if attempt > 0 {
35+
logger.Infof("Retrying... (%d/%d attempts)", attempt, t.Retries)
36+
}
37+
38+
command := strings.ToUpper(t.Command)
39+
40+
if err = client.SendCommand(command, t.Args...); err != nil {
41+
return err
42+
}
43+
44+
value, err = client.ReadValue()
45+
if err != nil {
46+
return err
47+
}
48+
49+
if t.Retries == 0 {
50+
break
51+
}
52+
53+
if t.ShouldRetryFunc == nil {
54+
panic(fmt.Sprintf("Received SendCommand with retries: %d but no ShouldRetryFunc.", t.Retries))
55+
} else {
56+
if t.ShouldRetryFunc(value) {
57+
// If ShouldRetryFunc returns true, we sleep and retry.
58+
time.Sleep(500 * time.Millisecond)
59+
} else {
60+
break
61+
}
62+
}
63+
}
64+
65+
t.ReceivedResponse = value
66+
67+
<-t.AwaitChan
68+
69+
if err = t.Assertion.Run(value); err != nil {
70+
return err
71+
}
72+
73+
if !t.ShouldSkipUnreadDataCheck {
74+
client.ReadIntoBuffer() // Let's make sure there's no extra data
75+
76+
if client.UnreadBuffer.Len() > 0 {
77+
return fmt.Errorf("Found extra data: %q", client.UnreadBuffer.String())
78+
}
79+
}
80+
81+
logger.Successf("Received %s", value.FormattedString())
82+
t.DoneChan <- true
83+
return nil
84+
}

internal/test_streams_xread_block_max_id.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ func testStreamsXreadBlockMaxID(stageHarness *test_case_harness.TestCaseHarness)
4242
return err
4343
}
4444

45-
waitChan := make(chan bool, 1)
45+
xReadDone := make(chan bool, 1)
46+
xAddDone := make(chan bool, 1)
4647
randomInt = testerutils_random.RandomInt(1, 100)
4748

4849
go func() error {
@@ -53,20 +54,16 @@ func testStreamsXreadBlockMaxID(stageHarness *test_case_harness.TestCaseHarness)
5354
FieldValuePairs: [][]string{{"temperature", strconv.Itoa(randomInt)}},
5455
}},
5556
}})
56-
xreadCommandTestCase := &test_cases.SendCommandTestCase{
57+
awaitXReadTestCase := &test_cases.AwaitCommandTestCase{
5758
Command: "XREAD",
5859
Args: []string{"block", "0", "streams", randomKey, "$"},
5960
Assertion: assertion,
6061
ShouldSkipUnreadDataCheck: true,
62+
AwaitChan: xAddDone,
63+
DoneChan: xReadDone,
6164
}
6265

63-
if err := xreadCommandTestCase.Run(client1, logger); err != nil {
64-
logger.Errorf("Error: %v", err)
65-
return err
66-
}
67-
68-
waitChan <- true
69-
return nil
66+
return awaitXReadTestCase.Run(client1, logger)
7067
}()
7168

7269
time.Sleep(1000 * time.Millisecond)
@@ -89,7 +86,8 @@ func testStreamsXreadBlockMaxID(stageHarness *test_case_harness.TestCaseHarness)
8986
return err
9087
}
9188

92-
<-waitChan
89+
xAddDone <- true
90+
<-xReadDone
9391

9492
xreadCommandTestCase := &test_cases.SendCommandTestCase{
9593
Command: "XREAD",

0 commit comments

Comments
 (0)