Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2c14ef8
DOC-6823 make sorted-set zrank/zadd_lex examples runnable in the inte…
andy-stark-redis Jul 8, 2026
2f99455
DOC-6823 make string setnx_xx example runnable in the interactive CLI
andy-stark-redis Jul 8, 2026
329abae
DOC-6823 add reusable multi-client example test harness
andy-stark-redis Jul 8, 2026
c858733
DOC-6823 make hash hmget/hincrby examples runnable in the interactive…
andy-stark-redis Jul 8, 2026
6d7f19d
DOC-6823 make timeseries dependent examples runnable in the interacti…
andy-stark-redis Jul 8, 2026
0898fad
DOC-6823 add timeseries recreates to the go-redis client
andy-stark-redis Jul 8, 2026
2423230
DOC-6823 add timeseries recreates to jedis, node, and C# clients
andy-stark-redis Jul 8, 2026
12302f9
DOC-6823 make sets smismember example runnable in the interactive CLI
andy-stark-redis Jul 8, 2026
9bf49b2
DOC-6823 add smismember recreates across the sets client tabs
andy-stark-redis Jul 8, 2026
4f3925e
DOC-6823 make geosearch runnable in the interactive CLI via needs_prereq
andy-stark-redis Jul 8, 2026
6735cf9
DOC-6823 run reactive StringExample steps sequentially, not under Mon…
andy-stark-redis Jul 9, 2026
4c2284f
DOC-6823 use Double.NEGATIVE_INFINITY, not Double.MIN_VALUE, for -inf…
andy-stark-redis Jul 9, 2026
17b158d
DOC-6823 make document-database term queries runnable via a merged cr…
andy-stark-redis Jul 9, 2026
b2fcf89
DOC-6823 make geoindex GEO/GEOSHAPE examples self-contained and runnable
andy-stark-redis Jul 9, 2026
38e3594
DOC-6823 disable Try-it on document-database (payload over redis.io/c…
andy-stark-redis Jul 9, 2026
8f8402f
DOC-6823 add build/check_tryit_payloads.py — audit Try-it button payl…
andy-stark-redis Jul 9, 2026
c58916e
DOC-6823 trim document-database descriptions under the Try-it limit; …
andy-stark-redis Jul 9, 2026
f37804f
DOC-6823 correct Try-it limit to 4094 request-line; re-disable docume…
andy-stark-redis Jul 9, 2026
3b59414
DOC-6823 fix pre-existing upstream defects Bugbot flagged in the new …
andy-stark-redis Jul 9, 2026
41cf3fd
DOC-6823 make check_tryit_payloads serialize like JSON.stringify (com…
andy-stark-redis Jul 9, 2026
1e0445c
DOC-6823 fix stale 8192 references in check_tryit_payloads docstring/…
andy-stark-redis Jul 9, 2026
f79c791
DOC-6823 drop the lingering 8192 reference from the checker docstring
andy-stark-redis Jul 9, 2026
983f1d3
DOC-6823 fix swapped brand/model for bicycle:9 in the jedis search ov…
andy-stark-redis Jul 9, 2026
bdc4452
DOC-6823 lowercase C# search paths, fix geoindex py import, dispose h…
andy-stark-redis Jul 9, 2026
c75fe58
DOC-6823 bump harness jedis to 7.5.3 so RedisClient examples compile
andy-stark-redis Jul 9, 2026
b6cb882
DOC-6823 fail the harness loudly on unreachable Redis / failed flush
andy-stark-redis Jul 9, 2026
903c0d0
DOC-6823 complete the go search_quickstart index schema (price, condi…
andy-stark-redis Jul 9, 2026
e815e44
DOC-6823 order-independent node wildcard assert + harness exits non-z…
andy-stark-redis Jul 9, 2026
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
179 changes: 179 additions & 0 deletions build/check_tryit_payloads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""
Check interactive "Try it" payload sizes across the docs.

The external "Try it" button on a `clients-example` block opens
`redis.io/cli?commands=<base64>&autorun=true`, where <base64> is the URL-safe
base64 of a JSON array of the block's CLI commands. The server rejects the
request with "Request Line is too large (N > 4094)" once the **HTTP request
line** exceeds ~4094 bytes. The request line is `GET /cli?commands=<base64>&
autorun=true HTTP/1.1`, i.e. the base64 payload plus ~40 bytes of scaffolding,
so the base64 must be ≲ 4054 bytes. Two subtleties this checker accounts for:

* The cap is on the request line / URL, not the raw typed command text.
Sizes are computed to match the browser exactly: base64 of
JSON.stringify(cmds) (compact separators, real UTF-8) — see _payload_b64_len.
* A block with `needs_prereq="true"` has its set's `prereq="true"` block
prepended by the button, so it inherits the prereq's payload size.

A block is only subject to the limit if its external button is actually shown:
`try_it != "false"` and `runnable != "false"`. Blocks over the limit should get
`try_it="false"` (the on-page terminal still runs them — it does not go through
the URL). See the DOC-6823 work on content/develop/get-started/document-database.md.

Usage:
python build/check_tryit_payloads.py [--content-dir content] [--limit 4094]
[--overhead 40] [--warn-frac 0.85] [--quiet]

Exit status: 1 if any interactive block's request line exceeds the limit, else 0
(so it can gate CI). Stdlib only; run from the repo root.
"""

import argparse
import base64
import glob
import json
import os
import re
import sys

# Match a shortcode attribute value. The negative lookbehind on a word char
# stops `prereq="..."` from also matching inside `needs_prereq="..."`.
def _attr(tag, name):
m = re.search(r'(?<![\w])' + re.escape(name) + r'="([^"]*)"', tag)
return m.group(1) if m else None


_OPEN_TAG = re.compile(r'\{\{<\s*clients-example\s+.*?>\}\}', re.S)
_CLOSE = '{{< /clients-example'


def _parse_blocks(text):
"""Yield (open_tag, body_between_tags) for each clients-example block."""
parts = re.split(r'(' + _OPEN_TAG.pattern + r')', text, flags=re.S)
i = 1
while i < len(parts):
tag = parts[i]
body = parts[i + 1] if i + 1 < len(parts) else ""
end = body.find(_CLOSE)
if end >= 0:
body = body[:end]
yield tag, body
i += 2


def _cli_cmds(body):
"""Extract the redis-cli command lines (the `> ` / `redis> ` prefixed lines)."""
cmds = []
for line in body.split("\n"):
s = line.strip()
if s.startswith("redis> "):
cmds.append(s[len("redis> "):])
elif s.startswith("> "):
cmds.append(s[2:])
return cmds


def _payload_b64_len(cmds):
"""Bytes of the URL-safe base64 payload the button would send (no padding).

Mirrors the browser's btoa(unescape(encodeURIComponent(JSON.stringify(cmds)))):
JSON.stringify emits compact JSON (no spaces after ',' / ':') and keeps real
Unicode, so use compact separators and ensure_ascii=False + UTF-8 bytes.
Default json.dumps (spaces, \\uXXXX escapes) would overstate the size.
"""
js = json.dumps(cmds, separators=(",", ":"), ensure_ascii=False)
b64 = base64.b64encode(js.encode("utf-8")).decode().rstrip("=")
return len(b64)
Comment thread
cursor[bot] marked this conversation as resolved.


def scan(content_dir):
"""Return (blocks, set_prereq) for all clients-example blocks under content_dir."""
blocks = []
set_prereq = {} # (file, set) -> prereq command list
for f in sorted(glob.glob(os.path.join(content_dir, "**", "*.md"), recursive=True)):
try:
text = open(f, encoding="utf-8").read()
except (OSError, UnicodeDecodeError):
continue
if "clients-example" not in text:
continue
for tag, body in _parse_blocks(text):
b = {
"file": f,
"set": _attr(tag, "set"),
"step": _attr(tag, "step"),
"prereq": _attr(tag, "prereq") == "true",
"needs_prereq": _attr(tag, "needs_prereq") == "true",
"runnable": _attr(tag, "runnable"),
"try_it": _attr(tag, "try_it"),
"cmds": _cli_cmds(body),
}
blocks.append(b)
if b["prereq"]:
set_prereq[(b["file"], b["set"])] = b["cmds"]
return blocks, set_prereq


def payload_for(b, set_prereq):
"""The full command list the button would send for block b."""
cmds = []
if b["needs_prereq"]:
cmds += set_prereq.get((b["file"], b["set"]), [])
cmds += b["cmds"]
return cmds


def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--content-dir", default="content")
ap.add_argument("--limit", type=int, default=4094,
help="max HTTP request-line bytes for redis.io/cli "
"(observed server cap: 4094)")
ap.add_argument("--overhead", type=int, default=40,
help="bytes added to the base64 payload to form the request "
"line: 'GET /cli?commands=' + '&autorun=true HTTP/1.1' "
"(~40; observed ~24, so 40 is conservative)")
ap.add_argument("--warn-frac", type=float, default=0.85,
help="warn when payload exceeds this fraction of the limit")
ap.add_argument("--quiet", action="store_true",
help="only print offenders, not the summary")
args = ap.parse_args()

blocks, set_prereq = scan(args.content_dir)

over, near = [], []
for b in blocks:
# Only blocks whose external button is actually shown are subject to the limit.
if b["try_it"] == "false" or b["runnable"] == "false" or not b["cmds"]:
continue
b64 = _payload_b64_len(payload_for(b, set_prereq))
size = b64 + args.overhead # estimated HTTP request-line length
if size > args.limit:
over.append((size, b64, b))
elif size > args.limit * args.warn_frac:
near.append((size, b64, b))

for size, b64, b in sorted(near, key=lambda x: x[0], reverse=True):
print(f"WARN req-line ~{size} B ({100*size/args.limit:.0f}% of "
f"{args.limit}, b64={b64}) {b['set']}/{b['step']} ({b['file']})")
for size, b64, b in sorted(over, key=lambda x: x[0], reverse=True):
print(f"OVER req-line ~{size} B (> {args.limit}, b64={b64}) "
f"{b['set']}/{b['step']} ({b['file']}) -- set try_it=\"false\" "
f"or trim to b64 <= {args.limit - args.overhead}")

if not args.quiet:
interactive = sum(1 for b in blocks
if b["try_it"] != "false" and b["runnable"] != "false"
and b["cmds"])
print(f"\nScanned {len(blocks)} blocks ({interactive} interactive) under "
f"{args.content_dir}/. Request-line limit={args.limit} B "
f"(base64 payload + {args.overhead} B scaffolding). "
f"{len(over)} over, {len(near)} near.")

return 1 if over else 0


if __name__ == "__main__":
sys.exit(main())
3 changes: 3 additions & 0 deletions build/example-test-harness/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# transient: cached per-language projects/deps and run logs
work/
results/
52 changes: 52 additions & 0 deletions build/example-test-harness/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# TCE example test harness

Runs a docs example set's per-client source files (from `local_examples/`) against a
**live throwaway Redis on `localhost:6379`**, using each library's real in-file
assertions. Built for the DOC-6823 work (making `try_it="false"` examples self-contained),
but reusable for any example set.

## Usage

```bash
./run.sh <example_set> [client ...]
# all clients:
./run.sh ss_tutorial
# one/some:
./run.sh set_tutorial rust-sync dotnet
```

`example_set` ∈ { `ss_tutorial`, `set_tutorial` } today. Results print as a matrix;
per-run logs land in `results/<set>_<client>.log`.

⚠️ Several examples call `FLUSHALL`/`FLUSHDB`, and the harness flushes before each run.
Point it only at a scratch Redis.

## Clients & how each is run

| Client | Toolchain | Notes |
|---|---|---|
| python | venv + `redis` | run script directly (`assert`) |
| node | `npm i redis`, ESM | run as `.mjs` |
| go | module + `go-redis` | `go test`; needs a sibling `package example_commands` stub |
| jedis | Maven + `jedis:5.2.0` | surefire include `**/*Example.java` (classes aren't `*Test`) |
| lettuce-async / -reactive | Maven + `lettuce-core:6.5.5.RELEASE` | same surefire include |
| ruby | `redis` gem | run script (`raise`/local `assert_equal`) |
| rust-sync | Cargo + `redis = "1.3"` | file is `#[cfg(test)]` → dropped in `src/lib.rs`, `cargo test` |
| rust-async | Cargo + `redis` (tokio-comp) + `tokio` | `#[tokio::test]` |
| php | Composer + `predis/predis` | `bootstrap.php` stubs `PredisTestCase` asserts; reflection finds the `test*` method |
| dotnet | `dotnet test` + xunit + `StackExchange.Redis` | `dotnet/stubs.cs` stands in for NRedisStack's `AbstractNRedisStackTest`/`EndpointsFixture`/`[SkippableFact]`/`DocsTests` collection |

## Gotchas learned

- **Version pins matter.** `redis-rs` is now **1.x** (`1.3`), not `0.27` — the old pin failed
to compile `flushall` in a REMOVE block. Jedis 5.2, Lettuce 6.5.5, StackExchange.Redis 2.8.x.
- **Surefire only runs `*Test`/`*Tests` by default** — these classes are `*Example`, so the
POMs add an explicit `<include>`. First run looked green with **zero tests** without it.
- The test scaffolding lives in `REMOVE_START` blocks; for py/ruby/node/go/rust/jedis/lettuce
it's self-contained (stdlib asserts / JUnit), but **C# and PHP reference their repo's own
test base classes**, which is why they need the stubs above.

## Adding a new example set

Add one block of `case "$set:$client"` → source-path lines in `src_path()` in `run.sh`.
Nothing else changes; deps are cached under `work/`.
1 change: 1 addition & 0 deletions build/example-test-harness/dotnet/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using Xunit;
16 changes: 16 additions & 0 deletions build/example-test-harness/dotnet/harness.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StackExchange.Redis" Version="3.0.0" />
<PackageReference Include="NRedisStack" Version="1.6.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
</ItemGroup>
</Project>
33 changes: 33 additions & 0 deletions build/example-test-harness/dotnet/stubs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Minimal stand-ins for NRedisStack's own test fixtures, so a docs example
// file (which extends AbstractNRedisStackTest and uses [SkippableFact]) runs
// under plain xunit + StackExchange.Redis. The example creates its own
// ConnectionMultiplexer, so the fixture helpers can be no-ops.
using StackExchange.Redis;

namespace NRedisStack.Tests
{
public class EndpointsFixture
{
public enum Env { Standalone }
}

public class AbstractNRedisStackTest : IDisposable
{
private ConnectionMultiplexer? _muxer;
protected AbstractNRedisStackTest(EndpointsFixture fixture) { }
protected void SkipIfTargetConnectionDoesNotExist(EndpointsFixture.Env env) { }
protected IDatabase GetCleanDatabase(EndpointsFixture.Env env)
{
_muxer ??= ConnectionMultiplexer.Connect("localhost:6379");
return _muxer.GetDatabase();
}
Comment thread
cursor[bot] marked this conversation as resolved.
public void Dispose() { _muxer?.Dispose(); }
}

// The examples annotate the test method [SkippableFact]; make it a plain Fact.
public class SkippableFactAttribute : FactAttribute { }
}

// Satisfy [Collection("DocsTests")] + constructor injection of EndpointsFixture.
[CollectionDefinition("DocsTests")]
public class DocsTestsCollection : ICollectionFixture<NRedisStack.Tests.EndpointsFixture> { }
15 changes: 15 additions & 0 deletions build/example-test-harness/pom-jedis.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.redis</groupId><artifactId>jedis</artifactId><version>1.0</version>
<properties><maven.compiler.release>17</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
<dependencies>
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>7.5.3</version></dependency>
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.10.2</version><scope>test</scope></dependency>
<dependency><groupId>org.assertj</groupId><artifactId>assertj-core</artifactId><version>3.25.3</version><scope>test</scope></dependency>
</dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.2.5</version>
<configuration><includes><include>**/*Example.java</include></includes></configuration></plugin>
</plugins></build>
</project>
15 changes: 15 additions & 0 deletions build/example-test-harness/pom-lettuce-async.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.redis</groupId><artifactId>lettuce-async</artifactId><version>1.0</version>
<properties><maven.compiler.release>17</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
<dependencies>
<dependency><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId><version>6.5.5.RELEASE</version></dependency>
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.10.2</version><scope>test</scope></dependency>
<dependency><groupId>org.assertj</groupId><artifactId>assertj-core</artifactId><version>3.25.3</version><scope>test</scope></dependency>
</dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.2.5</version>
<configuration><includes><include>**/*Example.java</include></includes></configuration></plugin>
</plugins></build>
</project>
15 changes: 15 additions & 0 deletions build/example-test-harness/pom-lettuce-reactive.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.redis</groupId><artifactId>lettuce-reactive</artifactId><version>1.0</version>
<properties><maven.compiler.release>17</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
<dependencies>
<dependency><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId><version>6.5.5.RELEASE</version></dependency>
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.10.2</version><scope>test</scope></dependency>
<dependency><groupId>org.assertj</groupId><artifactId>assertj-core</artifactId><version>3.25.3</version><scope>test</scope></dependency>
</dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.2.5</version>
<configuration><includes><include>**/*Example.java</include></includes></configuration></plugin>
</plugins></build>
</project>
Loading
Loading