Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .claude/skills/tce-examples/reference/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,25 @@ Two environments and one Codex gate. Client identity for all of them comes from
| Dependencies | self-bootstrapped into `work/`, **cached** | tracked manifests in `fidelity/`, reinstalled every run |
| Client repos | none needed | clones required (`bootstrap.sh`) |
| C# / PHP | local stubs (`dotnet/stubs.cs`) | the real `Doc.csproj` / real PHPUnit |
| Clients | 13 (no C) | 13 (no RedisVL) |
| Clients | 17 of the 18 in `clients.tsv` (no RedisVL) | 17 of the 18 (no RedisVL) |
| Speed | seconds once warm | minutes — full toolchain install per client |

**C (hiredis) is the one client portable mode cannot bootstrap.** Every other portable
runner installs its dependency into `work/` (`pip install redis`, `npm i redis`,
`gem install redis`); hiredis is a system C library, so `run_hiredis` compiles against
an existing install, searching `/usr/local`, `/opt/homebrew`, then `/usr` for
`include/hiredis/hiredis.h` and baking the library path in with `-rpath`. Where hiredis
is absent it reports `SKIP (hiredis headers not found ...)` rather than a compile-error
FAIL, so a green run on a box without it still says nothing about the C examples —
check for the SKIP rather than assuming coverage.

**A client whose `portable` column is `-` is filtered out of the run list entirely**
by `clients_for_mode()` — it gets no row at all, not a `SKIP`. The "no portable runner"
FAIL branch only fires if you name that client explicitly on the command line. So
"absent from the results table" and "passed" look identical when scanning output; count
the rows against `clients.tsv` if coverage matters. (RedisVL is the only such client
now.)

**Iterate in portable, confirm in fidelity.** Portable is the fast loop because `work/`
persists between runs. Fidelity is the pre-merge check: it runs the example the way the
client repo will, which is the only way to catch a failure caused by the real test base
Expand Down
2 changes: 1 addition & 1 deletion build/example-test-harness/clients.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ lettuce-sync Lettuce-Sync lettuce_sync lettuce_sync lettuce-sync lettuce-sync {S
lettuce-async Java-Async lettuce_async lettuce_async lettuce-async lettuce-async {Set}Example.java src/test/java/io/redis/examples/async lettuce-async src/test/java/io/redis/examples/async lettuce-async
lettuce-reactive Java-Reactive lettuce_reactive lettuce_reactive lettuce-reactive lettuce-reactive {Set}Example.java src/test/java/io/redis/examples/reactive lettuce-reactive src/test/java/io/redis/examples/reactive lettuce-reactive
go-redis Go go_redis go-redis go-redis|go go-redis {set}_test.go doctests go-redis . go
hiredis C hi_redis - hiredis|c hiredis {set}.c examples hiredis . -
hiredis C hi_redis - hiredis|c hiredis {set}.c examples hiredis . hiredis
nredisstack C#-Sync (NRedisStack) nredisstack_sync nredisstack_sync NRedisStack|nredisstack|dotnet-sync nredisstack {Set}Example.cs tests/Doc NRedisStack tests/Doc dotnet
nredisstack-async C#-Async (NRedisStack) nredisstack_async nredisstack_async dotnet-async nredisstack {Set}Example.cs tests/Doc/Async NRedisStack tests/Doc/Async dotnet
seredis C#-Sync (SE.Redis) seredis_sync nredisstack_sync seredis seredis {Set}Example.cs tests/Doc NRedisStack tests/Doc dotnet
Expand Down
59 changes: 59 additions & 0 deletions build/example-test-harness/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ clients_for_mode() {
awk -F'\t' '!/^#/ && NF>1 && $11!="-" {print $1}' "$TSV"
fi
}
# The inverse: clients this mode cannot test at all. They never enter the drive loop, so
# without an explicit row they produce NO output — indistinguishable from a pass when
# scanning the summary. That is exactly how the C examples went untested unnoticed.
clients_excluded_for_mode() {
if [ "$MODE" = fidelity ]; then
awk -F'\t' '!/^#/ && NF>1 && $9=="-" {print $1}' "$TSV"
else
awk -F'\t' '!/^#/ && NF>1 && $11=="-" {print $1}' "$TSV"
fi
}

CLIENTS_ALL=()
while IFS= read -r c; do [ -n "$c" ] && CLIENTS_ALL+=("$c"); done < <(clients_for_mode)
Expand Down Expand Up @@ -140,6 +150,17 @@ toolchain_skip_reason() { # $1 = set, $2 = canonical client key, $3 = repo-relat
return
fi
;;
hiredis)
# Unlike every other portable client, hiredis cannot be bootstrapped into work/:
# it is a system C library (headers + shared object), not a pip/npm/gem package.
# Skip loudly where it is absent rather than FAILing with a compile error that
# says nothing about the example.
if ! hiredis_prefix >/dev/null; then
printf 'hiredis headers not found (looked in %s) — install hiredis to test the C examples' \
"$(printf '%s, ' "${HIREDIS_PREFIXES[@]}" | sed 's/, $//')"
return
fi
;;
ruby)
# redis-rb gained native hexpire/httl in 6.0.0, and 6.x requires Ruby >= 3.2. On an
# older Ruby (macOS still ships 2.6) the Gemfile resolves 5.4.1, where hexpire falls
Expand Down Expand Up @@ -314,6 +335,30 @@ run_ruby() {
gem list -i redis >/dev/null 2>&1 || gem install --silent redis >/dev/null 2>&1
ruby "$1" >"$LOG" 2>&1; rc=$?
}
# Prefixes searched for a hiredis install, in order. Homebrew uses /usr/local on Intel
# macOS and /opt/homebrew on Apple silicon; Linux distro packages land in /usr.
HIREDIS_PREFIXES=(/usr/local /opt/homebrew /usr)
hiredis_prefix() { # prints the first prefix containing hiredis/hiredis.h; non-zero if none
local p
for p in "${HIREDIS_PREFIXES[@]}"; do
[ -f "$p/include/hiredis/hiredis.h" ] && { printf '%s' "$p"; return 0; }
done
return 1
}
run_hiredis() {
local d="$WORK/hiredis" p; mkdir -p "$d"
p="$(hiredis_prefix)"
# Source FIRST, then -lhiredis: GNU ld resolves left to right, and Debian/Ubuntu default
# to --as-needed, which drops a library listed before any undefined symbol references it.
# macOS/ld64 tolerates either order, so getting this wrong fails only on Linux. Matches
# the order bootstrap.sh already uses for the fidelity C build.
# -rpath bakes the library path into the binary, so it runs without callers having to
# set DYLD_LIBRARY_PATH (macOS) or LD_LIBRARY_PATH (Linux). On compile failure the
# compiler diagnostics stay in $LOG; on success the program's own output replaces them.
cc "$1" -I"$p/include" -L"$p/lib" -Wl,-rpath,"$p/lib" -lhiredis -o "$d/example" >"$LOG" 2>&1 \
&& "$d/example" >"$LOG" 2>&1
Comment thread
cursor[bot] marked this conversation as resolved.
rc=$?
}
run_node() {
local d="$WORK/node"; mkdir -p "$d"
[ -d "$d/node_modules/redis" ] || { printf '{"type":"module"}\n' >"$d/package.json"; (cd "$d" && npm i -s redis >/dev/null 2>&1); }
Expand Down Expand Up @@ -585,6 +630,20 @@ for c in "${CLIENTS[@]}"; do
else SUMMARY+=("$c FAIL (results/${SET}_${c}.log)"); log ">> $c: FAIL"; fi
done

# Report the clients this mode cannot test, so "absent from the table" never masquerades as
# "passed". Only on a full sweep — when clients were named explicitly, the existing "no
# portable runner" FAIL already covers it.
if [ $# -eq 0 ]; then
while IFS= read -r c; do
[ -n "$c" ] || continue
if [ "$MODE" = fidelity ]; then
SUMMARY+=("$c SKIP (no fidelity dir in clients.tsv; portable only)")
else
SUMMARY+=("$c SKIP (no portable runner in clients.tsv; try --fidelity)")
fi
done < <(clients_excluded_for_mode)
fi

log ""; log "=== RESULTS: $SET ==="
for e in "${SUMMARY[@]}"; do printf ' %-18s %s\n' "${e%% *}" "${e#* }"; done

Expand Down
32 changes: 30 additions & 2 deletions local_examples/cmds_string/hiredis/cmds_string.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ int main(int argc, char **argv) {
// STEP_END

// REMOVE_START
redisReply *cleanup = redisCommand(c, "DEL key1 key2 nonexisting");
redisReply *cleanup = redisCommand(c, "DEL key1 key2 mykey nonexisting");
freeReplyObject(cleanup);
// REMOVE_END

Expand Down Expand Up @@ -67,7 +67,35 @@ int main(int argc, char **argv) {
// REMOVE_END

freeReplyObject(reply);
redisReply *cleanup2 = redisCommand(c, "DEL key1 key2 nonexisting");

// STEP_START incr
reply = redisCommand(c, "SET mykey 10");
printf("%s\n", reply->str);
// >>> OK
freeReplyObject(reply);

reply = redisCommand(c, "INCR mykey");
printf("%lld\n", reply->integer);
// >>> 11
// REMOVE_START
if (reply->integer != 11) {
printf("ASSERTION FAILED: Expected 11, got %lld\n", reply->integer);
}
// REMOVE_END
freeReplyObject(reply);

reply = redisCommand(c, "GET mykey");
printf("%s\n", reply->str);
// >>> 11
// REMOVE_START
if (strcmp(reply->str, "11") != 0) {
printf("ASSERTION FAILED: Expected '11', got '%s'\n", reply->str);
}
// REMOVE_END
freeReplyObject(reply);
// STEP_END

redisReply *cleanup2 = redisCommand(c, "DEL key1 key2 mykey nonexisting");
freeReplyObject(cleanup2);

// STEP_START disconnect
Expand Down
20 changes: 19 additions & 1 deletion local_examples/cmds_string/ioredis/cmds-string.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const redis = new Redis();
// HIDE_END

// REMOVE_START
await redis.del('key1', 'key2', 'nonexisting');
await redis.del('key1', 'key2', 'mykey', 'nonexisting');
// REMOVE_END

// STEP_START mget
Expand All @@ -24,6 +24,24 @@ assert.deepEqual(mgetResult, ['Hello', 'World', null]);
await redis.del('key1', 'key2', 'nonexisting');
// REMOVE_END

// STEP_START incr
const incrResult1 = await redis.set('mykey', '10');
console.log(incrResult1); // >>> OK

const incrResult2 = await redis.incr('mykey');
console.log(incrResult2); // >>> 11

const incrResult3 = await redis.get('mykey');
console.log(incrResult3); // >>> 11
// STEP_END

// REMOVE_START
assert.equal(incrResult1, 'OK');
assert.equal(incrResult2, 11);
assert.equal(incrResult3, '11');
await redis.del('mykey');
// REMOVE_END

// HIDE_START
redis.disconnect();
// HIDE_END
31 changes: 29 additions & 2 deletions local_examples/cmds_string/lettuce-async/CmdsStringExample.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public void run() {
RedisAsyncCommands<String, String> asyncCommands = connection.async();

// REMOVE_START
asyncCommands.del("key1", "key2", "nonexisting").toCompletableFuture().join();
asyncCommands.del("key1", "key2", "mykey", "nonexisting").toCompletableFuture().join();
// REMOVE_END

// STEP_START mget
Expand All @@ -44,8 +44,35 @@ public void run() {
// STEP_END

mgetExample.join();

// STEP_START incr
CompletableFuture<Void> incrExample = asyncCommands.set("mykey", "10")
.thenCompose(incrResult1 -> {
System.out.println(incrResult1); // >>> OK
// REMOVE_START
assertThat(incrResult1).isEqualTo("OK");
// REMOVE_END
return asyncCommands.incr("mykey");
})
.thenCompose(incrResult2 -> {
System.out.println(incrResult2); // >>> 11
// REMOVE_START
assertThat(incrResult2).isEqualTo(11L);
// REMOVE_END
return asyncCommands.get("mykey");
})
.thenAccept(incrResult3 -> {
System.out.println(incrResult3); // >>> 11
// REMOVE_START
assertThat(incrResult3).isEqualTo("11");
// REMOVE_END
})
.toCompletableFuture();
// STEP_END

incrExample.join();
// REMOVE_START
asyncCommands.del("key1", "key2", "nonexisting").toCompletableFuture().join();
asyncCommands.del("key1", "key2", "mykey", "nonexisting").toCompletableFuture().join();
// REMOVE_END
} finally {
redisClient.shutdown();
Expand Down
31 changes: 29 additions & 2 deletions local_examples/cmds_string/lettuce-reactive/CmdsStringExample.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public void run() {
RedisReactiveCommands<String, String> reactiveCommands = connection.reactive();

// REMOVE_START
reactiveCommands.del("key1", "key2", "nonexisting").block();
reactiveCommands.del("key1", "key2", "mykey", "nonexisting").block();
// REMOVE_END

// STEP_START mget
Expand All @@ -43,8 +43,35 @@ public void run() {
// STEP_END

mgetExample.block();

// STEP_START incr
Mono<Void> incrExample = reactiveCommands.set("mykey", "10")
.flatMap(incrResult1 -> {
System.out.println(incrResult1); // >>> OK
// REMOVE_START
assertThat(incrResult1).isEqualTo("OK");
// REMOVE_END
return reactiveCommands.incr("mykey");
})
.flatMap(incrResult2 -> {
System.out.println(incrResult2); // >>> 11
// REMOVE_START
assertThat(incrResult2).isEqualTo(11L);
// REMOVE_END
return reactiveCommands.get("mykey");
})
.doOnNext(incrResult3 -> {
System.out.println(incrResult3); // >>> 11
// REMOVE_START
assertThat(incrResult3).isEqualTo("11");
// REMOVE_END
})
.then();
// STEP_END

incrExample.block();
// REMOVE_START
reactiveCommands.del("key1", "key2", "nonexisting").block();
reactiveCommands.del("key1", "key2", "mykey", "nonexisting").block();
// REMOVE_END
} finally {
redisClient.shutdown();
Expand Down
20 changes: 19 additions & 1 deletion local_examples/cmds_string/node-redis/cmds-string.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ await client.connect().catch(console.error);
// HIDE_END

// REMOVE_START
await client.del(['key1', 'key2', 'nonexisting']);
await client.del(['key1', 'key2', 'mykey', 'nonexisting']);
// REMOVE_END

// STEP_START mget
Expand All @@ -24,6 +24,24 @@ assert.deepEqual(mgetResult, ['Hello', 'World', null]);
await client.del(['key1', 'key2', 'nonexisting']);
// REMOVE_END

// STEP_START incr
const incrResult1 = await client.set('mykey', '10');
console.log(incrResult1); // >>> OK

const incrResult2 = await client.incr('mykey');
console.log(incrResult2); // >>> 11

const incrResult3 = await client.get('mykey');
console.log(incrResult3); // >>> 11
// STEP_END

// REMOVE_START
assert.equal(incrResult1, 'OK');
assert.equal(incrResult2, 11);
assert.equal(incrResult3, '11');
await client.del('mykey');
// REMOVE_END

// HIDE_START
await client.close();
// HIDE_END
20 changes: 19 additions & 1 deletion local_examples/cmds_string/predis/CmdsStringTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public function testCmdsString() {
]);

// REMOVE_START
$r->del('key1', 'key2', 'nonexisting');
$r->del('key1', 'key2', 'mykey', 'nonexisting');
// REMOVE_END

// STEP_START mget
Expand All @@ -33,5 +33,23 @@ public function testCmdsString() {
$this->assertEquals(['Hello', 'World', null], $mgetResult);
$r->del('key1', 'key2', 'nonexisting');
// REMOVE_END

// STEP_START incr
$incrResult1 = $r->set('mykey', '10');
echo $incrResult1 . PHP_EOL; // >>> OK

$incrResult2 = $r->incr('mykey');
echo $incrResult2 . PHP_EOL; // >>> 11

$incrResult3 = $r->get('mykey');
echo $incrResult3 . PHP_EOL; // >>> 11
// STEP_END

// REMOVE_START
$this->assertEquals('OK', (string) $incrResult1);
$this->assertEquals(11, $incrResult2);
$this->assertEquals('11', $incrResult3);
$r->del('mykey');
// REMOVE_END
}
}
23 changes: 22 additions & 1 deletion local_examples/cmds_string/redis-py/cmds_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# HIDE_END

# REMOVE_START
r.delete("key1", "key2", "nonexisting")
r.delete("key1", "key2", "mykey", "nonexisting")
# REMOVE_END

# STEP_START mget
Expand All @@ -22,3 +22,24 @@
assert mget_result == ["Hello", "World", None]
r.delete("key1", "key2", "nonexisting")
# REMOVE_END

# STEP_START incr
incr_result1 = r.set("mykey", "10")
print(incr_result1)
# >>> True

incr_result2 = r.incr("mykey")
print(incr_result2)
# >>> 11

incr_result3 = r.get("mykey")
print(incr_result3)
# >>> 11
# STEP_END

# REMOVE_START
assert incr_result1 is True
assert incr_result2 == 11
assert incr_result3 == "11"
r.delete("mykey")
# REMOVE_END
Loading
Loading