diff --git a/.agents/skills/README.md b/.agents/skills/README.md index d04f48e548..3077c94c0d 100644 --- a/.agents/skills/README.md +++ b/.agents/skills/README.md @@ -1,143 +1,68 @@ # Agent Skills -This directory contains skills that teach AI agents how to perform specific tasks in this codebase. Each skill has a `SKILL.md` file with detailed instructions. - -## How Skills Work - -When you ask Augment Agent (using the VS Code plugin or Auggie CLI) to do something, it searches the codebase for relevant context—including these skill files. If your request matches a skill's purpose, Augment uses those instructions to do the job correctly. - -**Pro tip**: Reference a skill directly in your prompt for best results: -> "Using the generate-tce-examples skill, add an HMGET example for all supported languages." +This directory holds skills written for the Augment agent. It is **no longer the primary +location** — Claude Code skills live in [`.claude/skills/`](../../.claude/skills/) and Codex +skills in [`.codex/skills/`](../../.codex/skills/). ## Available Skills -### `extract-redis-cli-examples` - -Analyzes Redis command documentation pages to find CLI examples and determine which ones need multi-language code implementations. - -**Use when**: You want to audit a docs page and identify what examples are missing. +### `redis-use-case-ports` -### `generate-tce-examples` +Orchestrates a full Redis use-case implementation across all 9 supported client libraries +(`redis-py`, `node-redis`, `go-redis`, Jedis, Lettuce, StackExchange.Redis, Predis, `redis-rb`, +`redis-rs`) using a parallel-build → synthesise → audit workflow. -Creates tabbed code examples (TCEs) across 12 client languages (Python, Node.js (2), Go, Java (3), C#, PHP, Rust (2), and C) for Redis commands. +**Use when**: A new use case (cache-aside, session store, rate limiter, leaderboard, etc.) +needs to be ported to all 9 clients with consistent helper APIs, demo behaviour, and prose +structure — and you want parallel sub-agents rather than implementing serially. -**Use when**: You need to implement the same Redis example in multiple languages, with proper test markers and assertions. +**Assets**: `brief-template.md` (for parallel build agents), `report-template.md` (structured +agent output), `audit-checklist.md` (known bug classes — a living document), +`cross-diff-checklist.md` (consistency rules across clients), `redis-conventions.md` +(repo-specific layout and Hugo conventions), and `html-template.html` (shared demo UI). -**Assets**: Contains reference templates and `*_TEST_PATTERNS.md` files for each language in the `assets/` subdirectory. +## Moved: tabbed code examples -### `redis-use-case-ports` +`extract-redis-cli-examples` and `generate-tce-examples` have been replaced by a single phased +skill at **[`.claude/skills/tce-examples/`](../../.claude/skills/tce-examples/SKILL.md)**. -Orchestrates a full Redis use-case implementation across all 9 supported client libraries (`redis-py`, `node-redis`, `go-redis`, Jedis, Lettuce, StackExchange.Redis, Predis, `redis-rb`, `redis-rs`) using a parallel-build + synthesise + audit workflow. +It covers the same ground — auditing a page for missing client coverage, then generating +examples across every supported client — plus live testing and Codex review. The per-client +`*_TEST_PATTERNS.md` files and working samples moved with it, into +`.claude/skills/tce-examples/assets/`. -**Use when**: A new use case (cache-aside, session store, rate limiter, leaderboard, etc.) needs to be ported to all 9 clients with consistent helper APIs, demo behaviour, and prose structure — and you want to use parallel sub-agents rather than implementing serially. +What changed, beyond the location: -**Assets**: Contains `brief-template.md` (for parallel build agents), `report-template.md` (structured agent output), `audit-checklist.md` (known bug classes — a living document), `cross-diff-checklist.md` (consistency rules across clients), `redis-conventions.md` (repo-specific layout and Hugo conventions), and `html-template.html` (shared demo UI). +- **Auditing is scripted.** `scripts/audit_page.py` wraps the repo's own parsers + (`build/components/cli_parser.py`, `build/components/markdown_parser.py`) instead of + restating their rules in prose. +- **Client identity has one source.** `build/example-test-harness/clients.tsv` replaces the + five overlapping tables the old skill carried, which had drifted from `config.toml`. +- **Generation is parallel.** One sub-agent per client, spawned together, from a shared brief. +- **Testing is driven, not manual.** See below. -## Setup +## Test environment setup -The `generate-tce-examples` agent skill requires a very specific setup that includes (1) a clone of the `redis/docs` repo and -(2) a `clients` directory that contains clones of all the client repos and an `examples` directory structure that's used for testing. +The tabbed-code-example test environment used to be a zip file passed around by hand. It is +now generated: -At the top level, you'll have the following: - -``` -/path/to/ -├── clients -└── docs +```bash +build/example-test-harness/bootstrap.sh # scaffold tmp/clients/examples/, clone client repos +build/example-test-harness/bootstrap.sh --check # report gaps, change nothing ``` -The `clients` directory is used for agent skill reference and looks like this: +`bootstrap.sh` materialises the (gitignored) `tmp/clients/examples/` tree from the tracked +manifests in `build/example-test-harness/fidelity/`, clones the client repos it needs, and +reports which toolchains are missing. Then: -``` -/path/to/clients -├── NRedisStack -├── StackExchange.Redis -├── examples -├── go-redis -├── ioredis -├── jedis -├── lettuce -├── node-redis -├── predis -├── redis-py -├── redis-rb -├── redis-rs -└── redis-vl-python +```bash +build/example-test-harness/run.sh cmds_hash # portable: cached deps, no clones needed +build/example-test-harness/run.sh --fidelity cmds_hash # fidelity: real manifests, real clones +build/example-test-harness/run.sh --list cmds_hash # just resolve source paths ``` -The examples directory structure is used to test generated examples and has the following structure: - -``` -/path/to/clients/examples -├── NRedisStack (a full clone of the NRedisStack repo) -│   └── tests -│   └── Doc -│   └── nredisstack_sample_test.cs -├── go-redis -│   ├── sample_test.go -│   ├── go.mod -│   └── run.sh -├── hiredis -│   ├── sample_test.c -│   └── run.sh -├── ioredis -│   ├── sample_test.js -│   ├── package.json -│   └── run.sh -├── jedis -│   ├── pom.xml -│   ├── run.sh -│   └── src -│   └── test -│   └── java -│   └── io -│   └── redis -│   └── examples -│   └── SampleTest.java -├── lettuce-async -│   ├── pom.xml -│   ├── run.sh -│   └── src -│   └── test -│   └── java -│   └── io -│   └── redis -│   └── examples -│   └── async -│   └── SampleTest.java -├── lettuce-reactive -│   ├── pom.xml -│   ├── run.sh -│   └── src -│   └── test -│   └── java -│   └── io -│   └── redis -│   └── examples -│   └── reactive -│   └── SampleTest.java -├── node-redis -│   ├── sample_test.js -│   ├── package.json -│   └── run.sh -├── predis -│   ├── SampleTest.php -│   ├── composer.json -│   └── run.sh -├── redis-py -│   ├── sample_test.py -│   ├── requirements.txt -│   └── run.sh -├── rust-async -│   ├── Cargo.toml -│   ├── run.sh -│   └── tests -│   └── sample_test.rs -└── rust-sync -│   ├── Cargo.toml -│   ├── run.sh -│   └── tests -│   └── sample_test.rs -``` +Both modes need a scratch Redis on `localhost:6379` — they `FLUSHALL` between clients, so do +not point them at anything you care about. -A zip file containing this structure will be made upon request. Ping `David Dougherty` in Slack. +See [`.claude/skills/tce-examples/reference/testing.md`](../../.claude/skills/tce-examples/reference/testing.md) +for which mode to use when, and for the false-green traps both modes now guard against. diff --git a/.agents/skills/extract-redis-cli-examples/SKILL.md b/.agents/skills/extract-redis-cli-examples/SKILL.md deleted file mode 100644 index c3a65984b6..0000000000 --- a/.agents/skills/extract-redis-cli-examples/SKILL.md +++ /dev/null @@ -1,223 +0,0 @@ ---- -name: extract-redis-cli-examples -description: Extract Redis CLI examples from documentation pages, identify which commands are demonstrated, and determine what multi-language code examples need to be created or updated. ---- - -# Extract Redis CLI Examples - -This skill helps you extract Redis CLI examples from Redis documentation pages and prepare them for multi-language tabbed code example (TCE) implementation. - -## When to Use This Skill - -Use this skill when you need to: -- Analyze a command documentation page to find CLI examples -- Identify which Redis commands are demonstrated in examples -- Determine if multi-language examples already exist or need to be created -- Prepare a list of examples that need client library implementations - -## Source Formats - -Redis CLI examples appear in documentation in **four formats**: - -### 1. Redis CLI Shortcode (Interactive) - -```markdown -{{% redis-cli %}} -SET mykey "Hello" -GET mykey -{{% /redis-cli %}} -``` - -This creates an interactive redis-cli experience. The text between shortcodes contains executable commands. - -### 2. Highlight Shortcode - -```markdown -{{< highlight bash >}} -127.0.0.1:6379> SET mykey "Hello" -OK -127.0.0.1:6379> GET mykey -"Hello" -{{< / highlight >}} -``` - -Used for syntax-highlighted code blocks. The `[lang]` parameter (e.g., `bash`) specifies highlighting. - -### 3. Clients-Example Shortcode (Multi-Language) - -```markdown -{{< clients-example set="set_and_get" step="basic" >}} -> SET mykey "Hello" -OK -> GET mykey -"Hello" -{{< /clients-example >}} -``` - -**Important**: This format indicates multi-language examples MAY already exist. Check `data/examples.json` for the example ID to see which languages are implemented. - -### 4. Fenced Code Blocks - -```markdown -````bash -> SET mykey "Hello" -OK -> GET mykey -"Hello" -````  -``` - -Standard markdown code blocks, often with `bash`, `plaintext`, or no language specified. - -## Command Extraction Rules - -### Identifying Command Lines - -Command lines are identified by prompt prefixes: -- `>` - Standard prompt -- `redis>` - Redis prompt -- `127.0.0.1:6379>` - Full Redis prompt - -Lines WITHOUT these prefixes are typically output and should be ignored. -However, it may be the case that lines without prefixes are actual Redis commands, and represent code examples. - -### Parsing Command Names - -| Pattern | Example | Extracted Command | -|---------|---------|-------------------| -| Single-word | `> SET key value` | `SET` | -| Multi-word | `> ACL CAT` | `ACL CAT` | -| Dot notation | `> JSON.SET doc $ '{}'` | `JSON.SET` | -| With arguments | `> HSET key field value` | `HSET` | - -### Command Extraction Examples - -**Input:** -``` -> HSET bike:1 model Deimos brand Ergonom -(integer) 2 -> HGET bike:1 model -"Deimos" -> HGETALL bike:1 -1) "model" -2) "Deimos" -``` - -**Extracted commands:** `["HSET", "HGET", "HGETALL"]` - -## Extraction Workflow - -### Step 1: Scan the Document - -Look for all four source formats in the markdown file. For each occurrence, extract: -- The source format type -- The raw CLI content -- The location in the document (line number or section) -- For `clients-example`: the `set` and `step` parameter values - -### Step 2: Parse Commands - -For each CLI block: -1. Identify lines with command prompts (`>`, `redis>`, `127.0.0.1:6379>`) -2. Extract the command name (first token, or first two tokens for multi-word commands) -3. Deduplicate commands within the same example - -### Step 3: Check Existing Coverage - -For `clients-example` blocks, check if implementations exist: - -```bash -# Check data/examples.json for the example ID -cat data/examples.json | jq '.[""]' -``` - -This shows which client languages already have implementations. - -### Step 4: Generate Report - -Output a structured report with: - -```markdown -## Extraction Report: [filename] - -### Examples Found - -| # | Format | Commands | Example ID | Status | -|---|--------|----------|------------|--------| -| 1 | redis-cli | SET, GET | N/A | Needs TCE | -| 2 | clients-example | HSET, HGET | hash_tutorial | Partial (missing: Go, Rust) | -| 3 | highlight | ZADD, ZRANGE | N/A | Needs TCE | - -### Action Items - -1. **Create new TCE**: Examples 1, 3 need full multi-language implementation -2. **Add languages**: Example 2 needs Go, Rust implementations added -``` - -## Supported Client Languages - -The following languages are configured in `config.toml` (in display order): - -1. Python (redis-py) -2. Node.js (node-redis) -3. ioredis -4. Java-Sync (Jedis) -5. Lettuce-Sync -6. Java-Async (Lettuce) -7. Java-Reactive (Lettuce) -8. Go (go-redis) -9. C (hiredis) -10. C#-Sync (NRedisStack) -11. C#-Async (NRedisStack) -12. RedisVL -13. PHP (Predis) -14. Rust-Sync (redis-rs) -15. Rust-Async (redis-rs) - -## Key Reference Files - -- `for-ais-only/tcedocs/README.md` - How to add multi-language examples -- `for-ais-only/tcedocs/SPECIFICATION.md` - Complete TCE specification -- `for-ais-only/tcedocs/CLI_COMMAND_EXTRACTION_QUICK_REFERENCE.md` - Quick reference -- `config.toml` - Client configuration and display order -- `data/examples.json` - Existing example implementations -- `data/commands_core.json` - Command metadata (summaries, groups, complexity) - -## Example: Analyzing a Command Page - -When asked to extract examples from a command page like `content/commands/hset.md`: - -1. **Read the file** to find all CLI example formats -2. **Extract commands** from each example block -3. **Check `data/examples.json`** for existing implementations -4. **Report findings** with clear action items - -### Sample Output - -``` -Analyzing: content/commands/hset.md - -Found 3 CLI example blocks: - -1. Lines 45-52: redis-cli shortcode - Commands: HSET, HGET - Status: No TCE exists - Action: Create new example set "cmds_hash" with step "hset_basic" - -2. Lines 78-95: clients-example (set="hash_tutorial", step="hset_hget") - Commands: HSET, HGET, HGETALL - Status: TCE exists with Python, Node.js, Java-Sync - Action: Add missing languages (Go, C#, Rust, PHP) - -3. Lines 120-125: fenced code block - Commands: HSET - Status: No TCE exists - Action: Can merge with example #1 or create separate step -``` - -## Tips - -- **Prioritize `redis-cli` shortcodes** - These are interactive and high-value for conversion -- **Check the surrounding context** - The section heading often indicates the example's purpose -- **Group related commands** - Multiple commands in one block usually demonstrate a workflow -- **Note the complexity** - Simple SET/GET vs. complex pipeline operations need different handling diff --git a/.agents/skills/generate-tce-examples/SKILL.md b/.agents/skills/generate-tce-examples/SKILL.md deleted file mode 100644 index d9ccb66331..0000000000 --- a/.agents/skills/generate-tce-examples/SKILL.md +++ /dev/null @@ -1,788 +0,0 @@ ---- -name: generate-tce-examples -description: Generate tabbed code examples (TCEs) for Redis commands across all supported client languages ---- - -# Generate Tabbed Code Examples (TCEs) - -This skill helps you create multi-language examples for a given sequence of Redis commands for a particular command page. The objective is to create examples in each supported language in a single iteration. - -## When to Use This Skill - -Use this skill after the `extract-redis-cli-examples` skill has identified CLI examples that need TCE implementations. This skill: -- Creates new example files or adds to existing ones -- Implements examples across all 10+ client languages -- Ensures correct use of TCE markers and client-specific APIs -- Validates implementations against a checklist - -## Dual Folder Workspace - -The workspace has two primary directories at the root level: - -``` -.../ -├── clients/ # Client library repos and example files -└── docs/ # Documentation repository (current workspace) -``` - -### Client Library Repos (`clients/`) - -Contains one directory for each Redis client GitHub repo (useful for API research): - -``` -clients/ -├── NRedisStack # C# client -├── StackExchange.Redis # C# client (alternative) -├── go-redis # Go client -├── ioredis # Node.js client (alternative) -├── jedis # Java client (sync) -├── lettuce # Java client (async/reactive) -├── node-redis # Node.js client (primary) -├── predis # PHP client -├── redis-py # Python client -└── redis-rs # Rust client -``` - -### Example Test Directories (`clients/examples/`) - -New or modified examples should be placed here for testing: - -| Client | Directory | -|--------|-----------| -| C# (NRedisStack) | `clients/examples/NRedisStack/`* | -| Go (go-redis) | `clients/examples/go-redis/` | -| JavaScript (ioredis) | `clients/examples/ioredis/` | -| Java (Jedis) | `clients/examples/jedis/src/test/java/io/redis/examples/` | -| Java (Lettuce-async) | `clients/examples/lettuce-async/src/test/java/io/redis/examples/async/` | -| Java (Lettuce-reactive) | `clients/examples/lettuce-reactive/src/test/java/io/redis/examples/reactive/` | -| JavaScript (node-redis) | `clients/examples/node-redis/` | -| PHP (predis) | `clients/examples/predis/` | -| Python (redis-py) | `clients/examples/redis-py/` | -| Rust (async) | `clients/examples/rust-async/tests/` | -| Rust (sync) | `clients/examples/rust-sync/tests/` | - -\* Note: the NRedisStack client examples directory given above is actually a clone of the redis/NRedisStack repo. -Tests go in the tests/Doc directory. There may already be existing tests there, but they can be overwritten by new examples. - -### Command-API Mapping (`docs/data/command-api-mapping/`) - -Contains JSON files mapping Redis commands to client APIs. Use these to find the correct method signatures: - -```bash -cat data/command-api-mapping/HSET.json | jq '.api_calls.redis_py' -``` - -## Command Groups - -TCEs are organized by command group. The group determines: -1. Which existing file to add to (or create) -2. The file naming convention per language - -### Group List - -| Group | Description | -|-------|-------------| -| `bf` | Bloom filter commands | -| `bitmap` | Bitfield and bitmap commands | -| `cf` | Cuckoo filter commands | -| `cluster` | Redis cluster commands | -| `cms` | Count-min sketch commands | -| `connection` | Connection commands | -| `generic` | Generic commands (apply to all key types) | -| `geo` | Geospatial commands | -| `hash` | Hash commands | -| `hyperloglog` | HyperLogLog commands | -| `json` | JSON module commands | -| `list` | List commands | -| `pubsub` | Pub/sub commands | -| `scripting` | Lua scripting commands | -| `search` | Search module commands | -| `server` | Server commands | -| `set` | Set commands | -| `sorted-set` | Sorted set commands | -| `stream` | Stream commands | -| `string` | String commands | -| `suggestion` | Suggestion commands | -| `tdigest` | T-digest commands | -| `timeseries` | Time series commands | -| `topk` | Top-k commands | -| `transactions` | Transaction commands | -| `vector_set` | Vector set commands | - -### Finding a Command's Group - -Check the Hugo frontmatter in the command's markdown file: - -```bash -grep "^group:" content/commands/hset.md -# Output: group: hash -``` - -### File Naming by Group - -For a group like `generic`, these are the file naming conventions: - -| Client | File Name | -|--------|-----------| -| C# (NRedisStack) | `CmdsGenericExample.cs` | -| Go (go-redis) | `cmds_generic_test.go` | -| JavaScript (ioredis) | `cmds-generic.js` | -| Java (Jedis) | `CmdsGenericExample.java` | -| Java (Lettuce) | `CmdsGenericExample.java` | -| JavaScript (node-redis) | `cmds-generic.js` | -| PHP (predis) | `CmdGenericTest.php` | -| Python (redis-py) | `cmds_generic.py` | -| Rust | `cmds_generic.rs` | - -**Pattern**: Replace `generic` with the group name, using the appropriate case convention: -- **PascalCase**: Java, C# (e.g., `CmdsHashExample.java`) -- **snake_case**: Python, Go, Rust (e.g., `cmds_hash.py`) -- **kebab-case**: JavaScript (e.g., `cmds-hash.js`) -- **PascalCase + singular**: PHP (e.g., `CmdHashTest.php`) - - -## Locating Existing Examples - -Before creating new examples, check if implementations already exist. **This is critical to avoid overwriting existing steps.** - -### Understanding the Three-Tier File Locations - -| Location | Purpose | Lifecycle | -|----------|---------|-----------| -| `../clients/examples//` | **Testing area** - Write new/modified examples here for testing | Temporary; cleared after testing | -| `local_examples///` | **Staging area** - Tested examples pending merge to client repos. These files have MORE steps than client repo versions. | Semi-permanent; until merged | -| Client repos (e.g., `../redis-py/doctests/`) | **Source of truth** - Merged/official examples | Permanent | - -**Example lifecycle for adding `hmget` step to redis-py:** -1. `local_examples/cmds_hash/redis-py/cmds_hash.py` has pending steps (`hdel`, `hexpire`) -2. Copy to `../clients/examples/redis-py/cmds_hash.py`, add `hmget`, test it -3. Once testing passes, update `local_examples/` with the tested file -4. Eventually, `local_examples/` gets merged to `../redis-py/doctests/` - -### 1. Check `local_examples/` First (Staging Area) - -This is where pending extensions live. **Always check here first** to get the most complete version: - -```bash -find local_examples -name "cmds_hash*" -o -name "CmdsHash*" -# Example result: local_examples/cmds_hash/redis-py/cmds_hash.py -``` - -If the file exists here, it contains all pending changes. Use this as your base. - -### 2. Check Client Repo Doc Tests (Source of Truth) - -If the file does NOT exist in `local_examples/`, check the client repo: - -| Client | Location | -|--------|----------| -| NRedisStack (C#) | `../NRedisStack/tests/Doc/` | -| go-redis (Go) | `../go-redis/doctests/` | -| ioredis (JavaScript) | No doc tests yet | -| jedis (Java) | `../jedis/src/test/java/io/redis/examples/` | -| lettuce (Java) | `../lettuce/src/test/java/io/redis/examples/async/` and `.../reactive/` | -| node-redis (JavaScript) | `../node-redis/doctests/` | -| predis (PHP) | No doc tests yet | -| redis-py (Python) | `../redis-py/doctests/` | -| redis-rs (Rust) | No doc tests yet | - -### 3. Output to `clients/examples/` (Testing Area) - -New or modified examples should be written to `../clients/examples/` for testing: - -```bash -# Directory structure mirrors client repo structure -../clients/examples/redis-py/cmds_hash.py -../clients/examples/go-redis/cmds_hash_test.go -../clients/examples/jedis/src/test/java/io/redis/examples/CmdsHashExample.java -``` - -> **⚠️ WARNING**: Always copy the most complete version (from `local_examples/` or client repo) to `clients/examples/` before adding new steps. Creating a fresh file will lose existing steps! - -## Order of Operations - -### Step 1: Locate Existing Examples - -**IMPORTANT**: Before creating any new files, you MUST check for existing examples: - -1. **Check `local_examples/` first** - The staging area with pending extensions: - ```bash - find local_examples -name "*cmds_hash*" -o -name "*CmdsHash*" - # Example: local_examples/cmds_hash/redis-py/cmds_hash.py - ``` - -2. **Check client repo doc tests** - The source of truth for merged examples: - ```bash - # Example for go-redis - ls ../go-redis/doctests/cmds_hash_test.go 2>/dev/null - - # Example for redis-py - ls ../redis-py/doctests/cmds_hash.py 2>/dev/null - ``` - -**Decision Tree:** - -| Scenario | Action | -|----------|--------| -| File exists in `local_examples/` | Copy to `../clients/examples/`, add the new step, test | -| File exists in client repo but NOT in `local_examples/` | Copy from client repo to `../clients/examples/`, add the new step, test | -| File does NOT exist in either location | Create new file in `../clients/examples/` using templates in `assets/` | - -> **Note on ioredis and hiredis**: These clients typically don't have existing doc test examples in the client repos or `local_examples/`. When adding steps for these clients, you will usually need to create a new file from scratch using the templates in `assets/ioredis/` and `assets/hiredis/`. This is expected behavior. - -### Adding Examples to Existing Sets - -Sometimes you need to add examples for a subset of clients rather than all of them. Common scenarios: - -| Scenario | Action | -|----------|--------| -| New client added to supported list | Add examples for the new client only, matching existing step names/structure | -| Client was skipped previously | Backfill the missing client, matching existing implementations | -| Client implementation was broken | Fix or recreate the specific client's example | - -**Key principles:** -1. **Match existing step names exactly** - If other clients have `scan1`, `scan2`, use those same names -2. **Match the example structure** - Follow the same Redis command sequence as existing implementations -3. **Only create missing pieces** - Don't regenerate clients that already have working examples -4. **Implement ALL existing steps** - When adding a new client to an existing group (e.g., adding ioredis to `cmds_generic`), implement all steps that exist for other clients, not just a subset - -**Example: Adding ioredis to an existing cmds_hash set:** - -```bash -# Step 1: Check what step names exist in other clients -grep "STEP_START" ../clients/examples/redis-py/cmds_hash.py -# Output: STEP_START hset, STEP_START hget, STEP_START hmget - -# Step 2: Create ioredis implementation with the same steps -# (use assets/ioredis/sample_test.js as template, implement hset, hget, hmget steps) - -# Step 3: Test the new implementation -cd ../clients/examples/ioredis && ./run.sh cmds-hash.js -``` - -**Example: Adding `hmget` step to redis-py (file exists in local_examples):** - -```bash -# Step 1: Check if file exists in local_examples/ -ls local_examples/cmds_hash/redis-py/cmds_hash.py -# Found! This file has pending extensions (hdel, hexpire, etc.) - -# Step 2: Copy to clients/examples/ for testing -mkdir -p ../clients/examples/redis-py -cp local_examples/cmds_hash/redis-py/cmds_hash.py ../clients/examples/redis-py/ - -# Step 3: Add the new hmget step to the copied file -# (use str-replace-editor to add STEP_START hmget ... STEP_END block) - -# Step 4: Test the example -# (run tests in ../clients/examples/redis-py/) -``` - -**Example: Adding step when file only exists in client repo:** - -```bash -# Step 1: Check local_examples/ - not found -ls local_examples/cmds_hash/go-redis/cmds_hash_test.go 2>/dev/null - -# Step 2: Check client repo - found! -ls ../go-redis/doctests/cmds_hash_test.go - -# Step 3: Copy to clients/examples/ for testing -mkdir -p ../clients/examples/go-redis -cp ../go-redis/doctests/cmds_hash_test.go ../clients/examples/go-redis/ - -# Step 4: Add the new step to the copied file -# (use str-replace-editor to add STEP_START hmget ... STEP_END block) - -# Step 5: Test the example -``` - -This ensures you preserve ALL existing steps when adding new ones. - -### Step 2: Map Client APIs - -Use the command-API mapping files to find correct method signatures: - -```bash -# For HMGET command -cat data/command-api-mapping/HMGET.json | jq '.api_calls' -``` - -This shows the method name, parameters, and return types for each client. - -### Step 3: Write the Code - -Follow the conventions in `for-ais-only/tcedocs/SPECIFICATION.md` and the `*_TEST_PATTERNS.md` files in each asset directory. - -**Key TCE Markers:** - -| Marker | Purpose | -|--------|---------| -| `// EXAMPLE: ` | Example identifier (first line) | -| `// STEP_START ` | Begin named code section | -| `// STEP_END` | End named code section | -| `// HIDE_START/HIDE_END` | Code hidden but executed | -| `// REMOVE_START/REMOVE_END` | Code removed from docs (tests, cleanup) | - -**Step Naming Convention:** - -- For command pages with a **single example**: use the command name (e.g., `hmget`, `lpush`) -- For command pages with **multiple examples**: use numbered names (e.g., `scan1`, `scan2`, `scan3`) -- Keep step names lowercase and concise - -**Code Structure Pattern:** - -``` -[EXAMPLE marker] -[HIDE block: imports, connection setup] -[REMOVE block: pre-test cleanup] -[STEP block: actual example code with output comments] -[REMOVE block: assertions] -[REMOVE block: post-test cleanup] -[HIDE block: disconnect/close] -``` - -### Step 4: Validate - -Use this checklist before completing: - -- [ ] All 12 client examples implemented (one per directory in `assets/`) -- [ ] Client-specific method signatures used correctly (from API mapping) -- [ ] TCE markers properly placed -- [ ] Expected output comments included (`// >>> value`) -- [ ] Assertions wrapped in REMOVE blocks -- [ ] Cleanup code wrapped in REMOVE blocks -- [ ] Return values match expected patterns - -## Canonical Examples (Assets) - -The `assets/` directory contains reference implementations for each client: - -``` -.agent/skills/generate-tce-examples/assets/ -├── go-redis/ -│ ├── GO_REDIS_TEST_PATTERNS.md -│ └── sample_test.go -├── hiredis/ -│ └── ... -├── ioredis/ -│ ├── IOREDIS_TEST_PATTERNS.md -│ └── sample_test.js -├── jedis/ -│ ├── JEDIS_TEST_PATTERNS.md -│ └── SampleTest.java -├── lettuce-async/ -│ └── ... -├── lettuce-reactive/ -│ └── ... -├── node-redis/ -│ ├── NODE_REDIS_TEST_PATTERNS.md -│ └── sample_test.js -├── nredisstack/ -│ ├── NREDISSTACK_TEST_PATTERNS.md -│ └── nredisstack_sample_test.cs -├── predis/ -│ ├── PREDIS_TEST_PATTERNS.md -│ └── SampleTest.php -├── redis-py/ -│ ├── REDIS_PY_TEST_PATTERNS.md -│ └── sample_test.py -├── rust-async/ -│ └── ... -└── rust-sync/ - └── ... -``` - -**Always consult** the `*_TEST_PATTERNS.md` file for each language before writing code. - -### Using the Samples as Templates - -Each `sample_test.*` file opens with a ~18-line banner comment explaining the -markers. **Do not copy that banner into a generated example.** Comment lines that -are not inside a `HIDE` or `REMOVE` block are published verbatim, so the banner -would appear in the rendered documentation. Every real example starts directly -with `// EXAMPLE: ` on line 1 (optionally followed by `BINDER_ID` on -line 2). - -The samples also demonstrate several unrelated steps in one file to show the -range of patterns. A generated example normally covers one command page, so take -the structure and conventions from the sample, not its step list. - -## Client Language Quick Reference - -### Python (redis-py) - -```python -# EXAMPLE: cmds_hash -import redis - -# HIDE_START -r = redis.Redis(decode_responses=True) -# HIDE_END - -# REMOVE_START -r.delete("myhash") -# REMOVE_END - -# STEP_START hmget -r.hset("myhash", mapping={"field1": "value1", "field2": "value2"}) -result = r.hmget("myhash", ["field1", "field2", "nofield"]) -print(result) # >>> ['value1', 'value2', None] -# STEP_END - -# REMOVE_START -assert result == ['value1', 'value2', None] -r.delete("myhash") -# REMOVE_END -``` - -### Node.js (node-redis) - -```javascript -// EXAMPLE: cmds_hash -// HIDE_START -import assert from 'node:assert'; -import { createClient } from 'redis'; - -const client = createClient(); -await client.connect(); -// HIDE_END - -// REMOVE_START -await client.del('myhash'); -// REMOVE_END - -// STEP_START hmget -await client.hSet('myhash', { field1: 'value1', field2: 'value2' }); -const result = await client.hmGet('myhash', ['field1', 'field2', 'nofield']); -console.log(result); // >>> ['value1', 'value2', null] -// STEP_END - -// REMOVE_START -assert.deepEqual(result, ['value1', 'value2', null]); -await client.del('myhash'); -// REMOVE_END - -// HIDE_START -await client.quit(); -// HIDE_END -``` - - -### Java (Jedis) - -```java -// EXAMPLE: cmds_hash -// HIDE_START -import redis.clients.jedis.RedisClient; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -// HIDE_END - -public class CmdsHashExample { - // REMOVE_START - @Test - // REMOVE_END - public void run() { - // HIDE_START - RedisClient jedis = RedisClient.create("redis://localhost:6379"); - // HIDE_END - - // REMOVE_START - jedis.del("myhash"); - // REMOVE_END - - // STEP_START hmget - jedis.hset("myhash", "field1", "value1"); - jedis.hset("myhash", "field2", "value2"); - List result = jedis.hmget("myhash", "field1", "field2", "nofield"); - System.out.println(result); // >>> [value1, value2, null] - // STEP_END - - // REMOVE_START - assertEquals(Arrays.asList("value1", "value2", null), result); - jedis.del("myhash"); - // REMOVE_END - - // HIDE_START - jedis.close(); - // HIDE_END - } -} -``` - -### Go (go-redis) - -```go -// EXAMPLE: cmds_hash -package example_commands_test - -import ( - "context" - "fmt" - - "github.com/redis/go-redis/v9" -) - -func ExampleClient_hmget() { - ctx := context.Background() - - rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - }) - - // REMOVE_START - rdb.Del(ctx, "myhash") - // REMOVE_END - - // STEP_START hmget - rdb.HSet(ctx, "myhash", "field1", "value1", "field2", "value2") - result, err := rdb.HMGet(ctx, "myhash", "field1", "field2", "nofield").Result() - if err != nil { - panic(err) - } - fmt.Println(result) // >>> [value1 value2 ] - // STEP_END - - // REMOVE_START - rdb.Del(ctx, "myhash") - // REMOVE_END -} -``` - -### C# (NRedisStack) - -```csharp -// EXAMPLE: cmds_hash -// BINDER_ID csharp-sample - -using NRedisStack; -using NRedisStack.RedisStackCommands; -using StackExchange.Redis; -// REMOVE_START -using NRedisStack.Tests; -// REMOVE_END - -// REMOVE_START -namespace Doc; - -[Collection("DocsTests")] -// REMOVE_END -public class CmdsHashExample -// REMOVE_START -: AbstractNRedisStackTest, IDisposable -// REMOVE_END -{ - // REMOVE_START - public CmdsHashExample(EndpointsFixture fixture) : base(fixture) { } - - [Fact] - // REMOVE_END - public void Run() - { - // REMOVE_START - SkipIfTargetConnectionDoesNotExist(EndpointsFixture.Env.Standalone); - var _ = GetCleanDatabase(EndpointsFixture.Env.Standalone); - // REMOVE_END - - // STEP_START connect - var muxer = ConnectionMultiplexer.Connect("localhost:6379"); - var db = muxer.GetDatabase(); - // STEP_END - - // STEP_START hmget - db.HashSet("myhash", new HashEntry[] { - new HashEntry("field1", "value1"), - new HashEntry("field2", "value2") - }); - RedisValue[] result = db.HashGet("myhash", new RedisValue[] { "field1", "field2", "nofield" }); - Console.WriteLine(string.Join(", ", result)); // >>> value1, value2, - // STEP_END - - // REMOVE_START - Assert.Equal(new RedisValue[] { "value1", "value2", RedisValue.Null }, result); - db.KeyDelete("myhash"); - // REMOVE_END - - // HIDE_START - muxer.Close(); - } -} -// HIDE_END -``` - -### PHP (Predis) - -```php -// EXAMPLE: cmds_hash -// BINDER_ID php-sample - 'tcp', - 'host' => '127.0.0.1', - 'port' => 6379, - 'password' => '', - 'database' => 0, - ]); - - // REMOVE_START - $r->del('myhash'); - // REMOVE_END - - // STEP_START hmget - $r->hset('myhash', 'field1', 'value1'); - $r->hset('myhash', 'field2', 'value2'); - $result = $r->hmget('myhash', ['field1', 'field2', 'nofield']); - echo json_encode($result) . PHP_EOL; // >>> ["value1","value2",null] - // STEP_END - - // REMOVE_START - $this->assertEquals(['value1', 'value2', null], $result); - $r->del('myhash'); - // REMOVE_END - } -} -``` - -### Rust (Sync) - -> **IMPORTANT**: Rust test files must be placed in the `tests/` subdirectory (e.g., `rust-sync/tests/cmds_hash.rs`), NOT in the project root. - -```rust -// EXAMPLE: cmds_hash -// File: tests/cmds_hash.rs -#[cfg(test)] -mod cmds_hash_tests { - use redis::Commands; - use std::collections::HashMap; - - #[test] - fn run() { - let mut r = match redis::Client::open("redis://127.0.0.1") { - Ok(client) => match client.get_connection() { - Ok(conn) => conn, - Err(e) => { - println!("Failed to connect to Redis: {e}"); - return; - } - }, - Err(e) => { - println!("Failed to create Redis client: {e}"); - return; - } - }; - - // REMOVE_START - let _: Result = r.del("myhash"); - // REMOVE_END - - // STEP_START hmget - let _: i32 = r.hset("myhash", "field1", "value1").unwrap(); - let _: i32 = r.hset("myhash", "field2", "value2").unwrap(); - - match r.hmget::<_, _, Vec>>("myhash", &["field1", "field2", "nofield"]) { - Ok(result) => { - println!("{:?}", result); // >>> [Some("value1"), Some("value2"), None] - // REMOVE_START - assert_eq!(result, vec![Some("value1".to_string()), Some("value2".to_string()), None]); - // REMOVE_END - } - Err(e) => println!("Error: {e}"), - } - // STEP_END - - // REMOVE_START - let _: Result = r.del("myhash"); - // REMOVE_END - } -} -``` - -### Rust (Async) - -> **IMPORTANT**: Rust test files must be placed in the `tests/` subdirectory (e.g., `rust-async/tests/cmds_hash.rs`), NOT in the project root. - -```rust -// EXAMPLE: cmds_hash -// File: tests/cmds_hash.rs -#[cfg(test)] -mod cmds_hash_tests { - use redis::AsyncCommands; - - #[tokio::test] - async fn run() { - let client = match redis::Client::open("redis://127.0.0.1") { - Ok(client) => client, - Err(e) => { - println!("Failed to create Redis client: {e}"); - return; - } - }; - - let mut r = match client.get_multiplexed_async_connection().await { - Ok(conn) => conn, - Err(e) => { - println!("Failed to connect to Redis: {e}"); - return; - } - }; - - // REMOVE_START - let _: Result = r.del("myhash").await; - // REMOVE_END - - // STEP_START hmget - let _: i32 = r.hset("myhash", "field1", "value1").await.unwrap(); - let _: i32 = r.hset("myhash", "field2", "value2").await.unwrap(); - - match r.hmget::<_, _, Vec>>("myhash", &["field1", "field2", "nofield"]).await { - Ok(result) => { - println!("{:?}", result); // >>> [Some("value1"), Some("value2"), None] - // REMOVE_START - assert_eq!(result, vec![Some("value1".to_string()), Some("value2".to_string()), None]); - // REMOVE_END - } - Err(e) => println!("Error: {e}"), - } - // STEP_END - - // REMOVE_START - let _: Result = r.del("myhash").await; - // REMOVE_END - } -} -``` - -## Key Reference Files - -| File | Purpose | -|------|---------| -| `for-ais-only/tcedocs/README.md` | Overview of TCE system | -| `for-ais-only/tcedocs/SPECIFICATION.md` | Complete technical specification | -| `for-ais-only/tcedocs/CLI_COMMAND_EXTRACTION_QUICK_REFERENCE.md` | CLI parsing rules | -| `config.toml` | Client configuration and display order | -| `data/examples.json` | Existing example implementations | -| `data/command-api-mapping/*.json` | Client API mappings per command | - -## Tips - -1. **Check API mappings first** - Don't guess method names; use the JSON files -2. **Match existing patterns** - If `cmds_hash` exists, follow its structure exactly -3. **Test incrementally** - Implement 2-3 languages, test, then continue -4. **Output comments matter** - `// >>> value` comments are extracted for documentation -5. **Use proper types** - Check return types in API mappings for correct variable types -6. **Clean up keys** - Always delete test keys in REMOVE blocks before and after \ No newline at end of file diff --git a/.agents/skills/redis-use-case-ports/SKILL.md b/.agents/skills/redis-use-case-ports/SKILL.md index 4fd80bfdda..f664a59bf9 100644 --- a/.agents/skills/redis-use-case-ports/SKILL.md +++ b/.agents/skills/redis-use-case-ports/SKILL.md @@ -19,7 +19,7 @@ Use this skill when: Do NOT use this skill for: -- Single-language code samples (use [`generate-tce-examples`](../generate-tce-examples/SKILL.md) for tabbed multi-language examples within a single doc page). +- Single-language code samples (use [`tce-examples`](../../../.claude/skills/tce-examples/SKILL.md) for tabbed multi-language examples within a single doc page). - Bug fixes on an existing use case (one targeted edit doesn't need the fan-out). - Cross-cutting refactors that touch many use cases at once (handle per-use-case, in sequence). @@ -116,7 +116,7 @@ Goal: catch bugs the structured audits missed by handing the codebase to a fresh Phase 4's targeted audits work well for known bug classes (the rows in `audit-checklist.md`). They're less good at the unknown unknowns — bugs where the *shape* of the audit prompt anchors the auditor to a false-positive answer. The pub/sub project's first Phase 4 said all 8 sibling ports passed the subscribe-ack check; an independent Codex review then found that Jedis returned its Subscription before the spawned thread had even sent the SUBSCRIBE, PHP's `waitForSubscription` silently fell through on timeout, PHP's Linux branch recorded the wrong PID, and Rust's duplicate-name check released its lock across the await. All four were real correctness bugs that Phase 4 had cleared. -Run an independent reviewer (different model, fresh context — the [`codex:rescue`](../../codex/) skill is a good fit, with a prompt that lists files plus the specific concerns: correctness bugs, cross-client divergence, doc drift) **before** declaring Phase 4 done. Treat its findings as candidates for the Phase 5 retrofit, with the orchestrator triaging which to accept (some "race conditions" are safe by accident — e.g. redis-py and go-redis subscribe-ack — because the synchronous socket write closes the window before the helper returns). +Run an independent reviewer (different model, fresh context — the [`claude-review`](../../../.codex/skills/claude-review/) Codex skill is a good fit, with a prompt that lists files plus the specific concerns: correctness bugs, cross-client divergence, doc drift) **before** declaring Phase 4 done. Treat its findings as candidates for the Phase 5 retrofit, with the orchestrator triaging which to accept (some "race conditions" are safe by accident — e.g. redis-py and go-redis subscribe-ack — because the synchronous socket write closes the window before the helper returns). **Verify each finding against the current file before fixing it.** Independent reviewers occasionally work from a stale snapshot — the file they reviewed was correct when they started, but a parallel agent kept editing it during the review window. Several of the Jedis and PHP findings on the semantic-cache project turned out to be the agent re-discovering a fix that had already landed minutes earlier (the EXISTS-race comment, the 1 MiB body cap, the docs paragraph about classpath resources). `grep` the finding's described pattern against the current file before opening an Edit — a one-second sanity check saves an inadvertent revert. diff --git a/.claude/settings.json b/.claude/settings.json index 6863603494..b2f6c5c6d1 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -10,7 +10,10 @@ ] }, "sandbox": { - "excludedCommands": ["gh"] + "excludedCommands": ["gh", "docker"], + "network": { + "allowLocalBinding": true + } }, "hooks": { "PostToolUse": [ diff --git a/.claude/skills/tce-examples/SKILL.md b/.claude/skills/tce-examples/SKILL.md new file mode 100644 index 0000000000..db32f0420e --- /dev/null +++ b/.claude/skills/tce-examples/SKILL.md @@ -0,0 +1,204 @@ +--- +name: tce-examples +description: "Create or extend tabbed code examples (TCEs) — the multi-language code blocks rendered by the clients-example shortcode. Use when a docs page needs client examples added, when auditing a command page for missing client coverage, when an example set needs a new step, or when a client needs backfilling into an existing set. Covers audit, parallel generation across clients, live testing, and Codex review." +--- + +# Tabbed code examples (TCEs) + +Take a docs page that demonstrates Redis commands and produce working, tested examples for every +supported client library, wired into the page with the `clients-example` shortcode. + +This is **AI-in-the-loop work.** The parsing is mechanical and already scripted (Phase 0); the +code generation is not. Each client has real API differences, real return-shape differences, and +real idioms — that judgment is yours. What the reference files give you is the durable facts, so +you don't re-derive (or misremember) them. + +## The client table + +**`build/example-test-harness/clients.tsv` is the single source of truth for client identity** — +display names, component ids, API-mapping keys, `local_examples` directory aliases, filename +conventions, and where each client is testable. Read it before you touch anything: + +```bash +column -t -s$'\t' build/example-test-harness/clients.tsv +``` + +Do not restate its contents in prose, in a brief, or in another table. The five duplicated +client tables in the skill this replaced had all drifted from `config.toml`; that is the failure +mode this file exists to prevent. + +## Three file locations, one lifecycle + +An example lives in up to three places, and **which one you read from matters**: + +| Location | Role | Notes | +|---|---|---| +| `local_examples///` | **Staging** — tested, pending upstream merge | Usually the *most complete* version. Has steps the client repo doesn't. | +| Client repo (`repo_path` in clients.tsv) | **Source of truth** — merged upstream | What the site build actually fetches. | +| `tmp/clients/examples//` | **Test bed** — gitignored, transient | Where files get staged to run. Never author here. | + +> **Read `local_examples/` first.** If the file exists there it supersedes the client repo copy. +> Starting from the client repo — or from a blank file — silently drops the pending steps. This +> is the single most common way TCE work goes wrong. + +## Phases + +Each phase has a gate. Don't skip them; the whole point of the ordering is that a mistake in +Phase 1 gets multiplied by every agent in Phase 2. + +### Phase 0 — Audit + +```bash +python3 .claude/skills/tce-examples/scripts/audit_page.py content/commands/hset.md +``` + +Wraps the repo's existing parsers (`build/components/cli_parser.py`, +`build/components/markdown_parser.py`) so the four CLI source formats and the +`> ` / `redis> ` / `127.0.0.1:6379> ` prompt handling stay correct without being restated here. +Emits a human table plus `--json` for a machine-readable work plan: per CLI block, the source +format, line range, commands, `set`/`step` if already wired, existing coverage from +`data/examples.json`, and which clients are missing. + +**Gate:** you can name the set id, the step names, and the exact client list before writing code. + +### Phase 1 — Reference implementation + +Write **one** client by hand — default `redis-py`, because Python surfaces design decisions most +plainly. Test it (Phase 3 for that client alone). This fixes the step names, the command +sequence, and the expected-output comments that every other client must match. + +**Gate: show the user the reference before Phase 2.** Its conventions propagate to a dozen +parallel agents; a wrong step name here costs a dozen retrofits. + +### Phase 2 — Parallel fan-out + +Spawn one subagent per remaining client, **all in a single message** so they run concurrently. +Fill in `assets/brief-template.md` per agent. Each brief must carry: + +- The reference implementation file, and the step names as a closed list. +- The client's row from `clients.tsv` (target path, filename, aliases). +- The relevant `data/command-api-mapping/.json` entries — the real signatures. Do not + let an agent guess a method name. +- The client's `assets//*_TEST_PATTERNS.md` **and** its working sample. +- **The most complete existing version of the file**, per the lifecycle table above, with an + explicit instruction to extend rather than replace. +- For C#: which flavor. See "The two C# clients" below — this is not optional context. + +Each agent returns `assets/report-template.md`. Read all of them before Phase 3: repeated +questions mean the brief was ambiguous, and identical divergences across agents usually mean a +convention is missing rather than that a dozen agents each erred. + +### Phase 3 — Test + +```bash +redis-server --daemonize yes # scratch instance; the harness FLUSHes it +build/example-test-harness/run.sh --fidelity cmds_hash +``` + +See `reference/testing.md` for both environments, what each client needs, and the known traps. + +**Gate:** every client PASS, or SKIP with a stated reason. A silent SKIP is a failure. + +### Phase 4 — Codex review + +An independent reviewer with fresh context, invoked per client in parallel. Catches the class the +harness structurally cannot: tests that pass while the expected-output comments are wrong, step +names that drifted, an API the docs shouldn't showcase, scaffolding that leaks into the rendered +page. Invocation and schema in `reference/testing.md`. + +**Gate:** no unresolved `severity: high`. + +### Phase 5 — Retrofit + +Fix, then re-run Phases 3–4 for touched clients only. + +> **Verify each finding against the current file before editing it.** Reviewers work from a +> snapshot; a parallel agent may have already fixed the thing being reported. `grep` for the +> described pattern first — a one-second check that prevents reverting a good fix. + +### Phase 6 — Wire into the docs + +1. Place files at `local_examples///` per `clients.tsv`. +2. Add or update the shortcode. Use **named** parameters: + `set`, `step`, `description`, `difficulty`, `buildsUpon`. The guidance in + `for-ais-only/tcedocs/README.md` on writing descriptions and choosing difficulty is current + and good — follow it there rather than guessing. +3. Rebuild and confirm the steps landed: + ```bash + python3 build/make.py + jq '. | keys' data/examples.json + ``` + +**Gate:** `hugo serve`, open the page, confirm the right tabs appear, the correct step is +highlighted, and no `REMOVE`/`HIDE` scaffolding is visible. + +## Using the samples as templates + +Each `assets//sample_*` file is a **working, runnable** example — that's the point, it +gives you a compilable starting shape. Two rules when working from one: + +1. **Never copy the banner comment.** Each sample opens with a ~18-line header explaining the + markers. Comment lines that are not inside a `HIDE` or `REMOVE` block are **published + verbatim** into the rendered docs. A real example starts directly with `// EXAMPLE: ` + on line 1, optionally `BINDER_ID` on line 2. +2. **Take the structure, not the step list.** Samples deliberately demonstrate several unrelated + steps to show the range of patterns. A generated example normally covers one command page. + +Always read the matching `*_TEST_PATTERNS.md` alongside the sample — it carries the per-client +traps (surefire include rules, Rust file placement, PHP test-base class, C# fixture API). + +## Markers + +`EXAMPLE:`, `BINDER_ID`, `HIDE_START`/`HIDE_END`, `REMOVE_START`/`REMOVE_END`, +`STEP_START `/`STEP_END`. Full semantics — including what each does to the rendered output +— are specified in `for-ais-only/tcedocs/SPECIFICATION.md`; read it there rather than working +from memory. The structural pattern: + +``` +EXAMPLE marker → HIDE: imports + connection → REMOVE: pre-test cleanup +→ STEP: the example code, with `>>>` output comments +→ REMOVE: assertions → REMOVE: post-test cleanup → HIDE: disconnect +``` + +**Step naming:** one example per command page → the command name (`hmget`). Multiple → numbered +(`scan1`, `scan2`). Lowercase, concise, and **identical across every client in the set**. + +## The two C# clients + +`C#-Sync (NRedisStack)` and `C#-Sync (SE.Redis)` are not two codebases. Both are fed from the +same `NRedisStack` repo directory, partitioned by a content filter in `data/components/`: + +- imports `using NRedisStack` → the **NRedisStack** tabs +- does **not** import it → the **SE.Redis** tabs +- `using NRedisStack.Tests` is excluded from that test — every file has it for fixtures + +So a single `.cs` file feeds one tab or the other, never both, and the deciding factor is an +import line. Most command-page C# examples are SE.Redis-flavored, because plain hash/list/string +commands don't need NRedisStack. Both flavors carry identical test scaffolding, so this is a +**generation** distinction, not a testing one — the same runner handles both. + +Getting the flavor wrong puts a working, passing example in the wrong tab. Say which flavor you +want in the brief, and check it in review. + +## Validation checklist + +- [ ] Every client in the Phase 0 work plan implemented (not "all 12" — read `clients.tsv`) +- [ ] Method signatures taken from `data/command-api-mapping/`, not guessed +- [ ] Step names identical across clients, and matching the reference +- [ ] Pre-existing steps preserved, not overwritten +- [ ] Markers placed so no scaffolding or banner text reaches the rendered page +- [ ] Expected-output comments (`>>>`) match what the client actually returns +- [ ] Assertions and cleanup inside `REMOVE` blocks +- [ ] Test keys deleted both before and after +- [ ] C# files carry the intended flavor + +## Guardrails + +- **Don't invent method names.** If `data/command-api-mapping/` lacks the command for a client, + read the client source (see `.claude/skills/command-api-mapping/` for how) or omit that client + and say so — don't guess a plausible signature. +- **Don't add a client that doesn't support the command.** An empty or wrong tab is worse than + an absent one. +- **Don't edit `data/examples.json`.** It's generated by `build/make.py`. +- **Don't author in `tmp/clients/`.** It's gitignored and transient; work lands in + `local_examples/`. diff --git a/.claude/skills/tce-examples/assets/brief-template.md b/.claude/skills/tce-examples/assets/brief-template.md new file mode 100644 index 0000000000..483dd690c4 --- /dev/null +++ b/.claude/skills/tce-examples/assets/brief-template.md @@ -0,0 +1,144 @@ +# Phase 2 fan-out brief + +Fill this in per client and pass it as the subagent prompt. One agent per client, **all +spawned in a single message** so they run concurrently. + +Everything below is required. An agent missing the "most complete existing version" pointer +will create a fresh file and silently drop pending steps — the single most common way this +work goes wrong. + +--- + +## Brief: `{{CLIENT_KEY}}` for example set `{{SET_ID}}` + +You are implementing one client's version of a tabbed code example (TCE) for the Redis docs. +Other agents are doing the other clients concurrently from the same reference. Do not +coordinate with them; do not touch any file outside the one you are told to write. + +### What to produce + +A single file at: + +``` +{{TARGET_PATH}} +``` + +Filename convention for this client: `{{FILENAME_CONVENTION}}` +(from `build/example-test-harness/clients.tsv`, column `filename`) + +### The spec + +The reference implementation is **`{{REFERENCE_PATH}}`**. Match it: same Redis commands, same +order, same observable behaviour. Where this client's idiom differs, follow the idiom — but +the commands and the results must line up. + +Steps to implement, exactly these names, exactly this spelling: + +``` +{{STEP_NAMES}} +``` + +Do not rename, add, split, or merge steps. Step names are how the docs page addresses your +code; a drifted name renders an empty tab. + +### Start from the existing file — do not start from scratch + +{{EXISTING_FILE_INSTRUCTION}} + + + +### API signatures — do not guess + +The real signatures for this client are in: + +``` +{{API_MAPPING_FILES}} +``` + +Read them. If a command you need is missing for this client, say so in your report and stop +rather than inventing a plausible method name. + +### Client conventions + +- Patterns: `.claude/skills/tce-examples/assets/{{ASSETS_DIR}}/{{PATTERNS_FILE}}` +- Working sample: `.claude/skills/tce-examples/assets/{{ASSETS_DIR}}/{{SAMPLE_FILE}}` + +Read both. The patterns file carries the traps specific to this client. + +**Two rules about the sample:** + +1. **Never copy its banner comment.** The sample opens with a ~18-line header explaining the + markers. Comment lines outside a `HIDE` or `REMOVE` block are published verbatim into the + docs. Your file starts directly with `{{COMMENT_PREFIX}} EXAMPLE: {{SET_ID}}` on line 1. +2. **Take its structure, not its step list.** The sample demonstrates several unrelated steps + to show the range of patterns. You implement the step list above. + +### Markers + +Structure, in order: + +``` +EXAMPLE marker → HIDE: imports + connection → REMOVE: pre-test cleanup +→ STEP: example code with `>>>` output comments +→ REMOVE: assertions → REMOVE: post-test cleanup → HIDE: disconnect +``` + +Full semantics: `for-ais-only/tcedocs/SPECIFICATION.md`. Do not work from memory. + +The `>>> ` comments are extracted into the docs as the shown output. They must match what +this client **actually returns** — right type, right formatting, right null representation. +A passing test does not prove a correct output comment. + +{{CSHARP_FLAVOR_NOTE}} + + + +### Do NOT run the test harness + +State this explicitly in the brief. **Concurrent agents must not run the harness**: it +`FLUSHALL`s a shared Redis between clients and stages into shared directories under +`tmp/clients/examples/`, so parallel runs corrupt each other's state and produce +meaningless pass/fail. The orchestrator tests serially in Phase 3. + +Tell the agent to report its file as **untested**, and to derive its `>>>` values from the +reference plus this client's documented return types — then say which values it could not +confirm. An honest "unconfirmed" is what makes Phase 3 worth running. + +Offline checks that touch neither Redis nor shared state are fine and worth asking for: +`node --check`, `ruby -c`, `php -l`, `gofmt -e`, `python3 -m py_compile`. + +> If you are running a **single** agent rather than a batch, it can test: +> `build/example-test-harness/run.sh {{SET_ID}} {{CLIENT_KEY}}` (needs a scratch Redis; use +> `--list` first if it SKIPs). Only lift the restriction when exactly one agent is running. +> +> Argument order is **set first, then client(s)** — `run.sh [client ...]`. +> Reversing them makes the set name be read as a client key, which now exits 2 with +> "unknown client" rather than doing something subtly wrong. + +### Report back + +Return `.claude/skills/tce-examples/assets/report-template.md`, filled in. The orchestrator +reads all reports together — repeated questions across agents mean the brief was ambiguous, +and that's worth knowing. diff --git a/.agents/skills/generate-tce-examples/assets/go-redis/GO_REDIS_TEST_PATTERNS.md b/.claude/skills/tce-examples/assets/go-redis/GO_REDIS_TEST_PATTERNS.md similarity index 100% rename from .agents/skills/generate-tce-examples/assets/go-redis/GO_REDIS_TEST_PATTERNS.md rename to .claude/skills/tce-examples/assets/go-redis/GO_REDIS_TEST_PATTERNS.md diff --git a/.agents/skills/generate-tce-examples/assets/go-redis/sample_test.go b/.claude/skills/tce-examples/assets/go-redis/sample_test.go similarity index 100% rename from .agents/skills/generate-tce-examples/assets/go-redis/sample_test.go rename to .claude/skills/tce-examples/assets/go-redis/sample_test.go diff --git a/.agents/skills/generate-tce-examples/assets/hiredis/HIREDIS_TEST_PATTERNS.md b/.claude/skills/tce-examples/assets/hiredis/HIREDIS_TEST_PATTERNS.md similarity index 100% rename from .agents/skills/generate-tce-examples/assets/hiredis/HIREDIS_TEST_PATTERNS.md rename to .claude/skills/tce-examples/assets/hiredis/HIREDIS_TEST_PATTERNS.md diff --git a/.agents/skills/generate-tce-examples/assets/hiredis/sample_test.c b/.claude/skills/tce-examples/assets/hiredis/sample_test.c similarity index 100% rename from .agents/skills/generate-tce-examples/assets/hiredis/sample_test.c rename to .claude/skills/tce-examples/assets/hiredis/sample_test.c diff --git a/.agents/skills/generate-tce-examples/assets/ioredis/IOREDIS_TEST_PATTERNS.md b/.claude/skills/tce-examples/assets/ioredis/IOREDIS_TEST_PATTERNS.md similarity index 100% rename from .agents/skills/generate-tce-examples/assets/ioredis/IOREDIS_TEST_PATTERNS.md rename to .claude/skills/tce-examples/assets/ioredis/IOREDIS_TEST_PATTERNS.md diff --git a/.agents/skills/generate-tce-examples/assets/ioredis/sample_test.js b/.claude/skills/tce-examples/assets/ioredis/sample_test.js similarity index 100% rename from .agents/skills/generate-tce-examples/assets/ioredis/sample_test.js rename to .claude/skills/tce-examples/assets/ioredis/sample_test.js diff --git a/.agents/skills/generate-tce-examples/assets/jedis/JEDIS_TEST_PATTERNS.md b/.claude/skills/tce-examples/assets/jedis/JEDIS_TEST_PATTERNS.md similarity index 100% rename from .agents/skills/generate-tce-examples/assets/jedis/JEDIS_TEST_PATTERNS.md rename to .claude/skills/tce-examples/assets/jedis/JEDIS_TEST_PATTERNS.md diff --git a/.agents/skills/generate-tce-examples/assets/jedis/SampleTest.java b/.claude/skills/tce-examples/assets/jedis/SampleTest.java similarity index 100% rename from .agents/skills/generate-tce-examples/assets/jedis/SampleTest.java rename to .claude/skills/tce-examples/assets/jedis/SampleTest.java diff --git a/.agents/skills/generate-tce-examples/assets/lettuce-async/LETTUCE_ASYNC_TEST_PATTERNS.md b/.claude/skills/tce-examples/assets/lettuce-async/LETTUCE_ASYNC_TEST_PATTERNS.md similarity index 100% rename from .agents/skills/generate-tce-examples/assets/lettuce-async/LETTUCE_ASYNC_TEST_PATTERNS.md rename to .claude/skills/tce-examples/assets/lettuce-async/LETTUCE_ASYNC_TEST_PATTERNS.md diff --git a/.agents/skills/generate-tce-examples/assets/lettuce-async/SampleTest.java b/.claude/skills/tce-examples/assets/lettuce-async/SampleTest.java similarity index 100% rename from .agents/skills/generate-tce-examples/assets/lettuce-async/SampleTest.java rename to .claude/skills/tce-examples/assets/lettuce-async/SampleTest.java diff --git a/.agents/skills/generate-tce-examples/assets/lettuce-reactive/LETTUCE_REACTIVE_TEST_PATTERNS.md b/.claude/skills/tce-examples/assets/lettuce-reactive/LETTUCE_REACTIVE_TEST_PATTERNS.md similarity index 100% rename from .agents/skills/generate-tce-examples/assets/lettuce-reactive/LETTUCE_REACTIVE_TEST_PATTERNS.md rename to .claude/skills/tce-examples/assets/lettuce-reactive/LETTUCE_REACTIVE_TEST_PATTERNS.md diff --git a/.agents/skills/generate-tce-examples/assets/lettuce-reactive/SampleTest.java b/.claude/skills/tce-examples/assets/lettuce-reactive/SampleTest.java similarity index 100% rename from .agents/skills/generate-tce-examples/assets/lettuce-reactive/SampleTest.java rename to .claude/skills/tce-examples/assets/lettuce-reactive/SampleTest.java diff --git a/.claude/skills/tce-examples/assets/lettuce-sync/LETTUCE_SYNC_TEST_PATTERNS.md b/.claude/skills/tce-examples/assets/lettuce-sync/LETTUCE_SYNC_TEST_PATTERNS.md new file mode 100644 index 0000000000..4a28e49f4e --- /dev/null +++ b/.claude/skills/tce-examples/assets/lettuce-sync/LETTUCE_SYNC_TEST_PATTERNS.md @@ -0,0 +1,167 @@ +# Lettuce Sync Test File Patterns + +This document describes the conventions used in Lettuce **synchronous** documentation test +files. For the other two Lettuce flavours see `../lettuce-async/` and `../lettuce-reactive/`. + +## Purpose + +These test files serve dual purposes: +1. **Executable JUnit tests** - Validate code snippets work correctly +2. **Documentation source** - Code is extracted for redis.io documentation + +## File Locations + +- **Staging**: `local_examples//lettuce-sync/*.java` + (also the older `local_examples/client-specific/lettuce-sync/`) +- **Upstream**: `lettuce` repo, `src/test/java/io/redis/examples/sync/` +- **Sample template**: `SampleTest.java` (in this directory) +- **Package**: `io.redis.examples.sync` — must match the directory, and differs from the + async (`...async`) and reactive (`...reactive`) packages + +## Marker Reference + +| Marker | Purpose | +|--------|---------| +| `// EXAMPLE: ` | Identifies example name (matches docs folder) | +| `// BINDER_ID ` | Optional identifier for online code runners | +| `// HIDE_START` / `// HIDE_END` | Code hidden from docs but still executed | +| `// REMOVE_START` / `// REMOVE_END` | Code completely removed from docs | +| `// STEP_START ` / `// STEP_END` | Named section for targeted doc inclusion | + +## File Structure Template + +```java +// EXAMPLE: example_name +package io.redis.examples.sync; + +import io.lettuce.core.*; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.api.StatefulRedisConnection; + +// REMOVE_START +import org.junit.jupiter.api.Test; +// REMOVE_END +// REMOVE_START +import static org.assertj.core.api.Assertions.assertThat; +// REMOVE_END + +public class CmdsHashExample { + + // REMOVE_START + @Test + // REMOVE_END + public void run() { + RedisClient redisClient = RedisClient.create("redis://localhost:6379"); + + try (StatefulRedisConnection connection = redisClient.connect()) { + RedisCommands syncCommands = connection.sync(); + + // STEP_START step_name + String res1 = syncCommands.set("mykey", "Hello"); + System.out.println(res1); // >>> OK + // STEP_END + + // HIDE_START + } finally { + redisClient.shutdown(); + } + // HIDE_END + } +} +``` + +## Key Patterns + +### 1. Values return directly — no futures + +This is the whole point of the sync flavour, and why it reads best in docs. Compare: + +```java +// sync +String res = syncCommands.set("mykey", "Hello"); + +// async — same call, wrapped +CompletableFuture res = asyncCommands.set("mykey", "Hello").toCompletableFuture(); +``` + +Don't import `CompletableFuture` or chain `thenCompose` in a sync example. If you catch +yourself doing that, you're porting the async example rather than writing the sync one. + +### 2. Class name must end in `Example` + +Surefire's default includes only match `*Test` / `*Tests`. Both the portable and fidelity +poms therefore add an explicit `**/*Example.java`. A class named anything +else runs **zero tests and the build still exits 0** — a false green. The Java wrappers now +fail on a zero test count, but the naming rule is what prevents the situation. + +(The template in this directory is called `SampleTest.java` precisely so it is *not* picked +up as a real example. Run it with `mvn test -Dtest=SampleTest` if you want to see it work.) + +### 3. `hset` returns boolean, `hmset` returns String + +A common porting mistake, because redis-py returns a count for both: + +```java +boolean created = syncCommands.hset("myhash", "field1", "value1"); // true only if NEW +String ok = syncCommands.hmset("myhash", fields); // "OK" +``` + +An `hset` that updates an existing field returns `false`, not `true` — say so in the output +comment when the example overwrites a field. + +### 4. `hgetall` ordering is not guaranteed + +`Map` iteration order isn't stable, so a bare `System.out.println(map)` can print a different +order between runs and the `>>>` comment becomes a lie. Wrap in a `TreeMap` when the output +comment needs to be deterministic: + +```java +Map res = syncCommands.hgetall("myhash"); +System.out.println(new TreeMap<>(res)); +// >>> {field1=value1, field2=value2, field3=value3} +``` + +Note Java's `Map.toString()` form is `{k=v, k=v}` — no quotes, `=` not `=>`. Output comments +are published verbatim, so it must match what Java actually prints. + +### 5. Numeric replies are `long` + +`hincrby`, `incr`, `zadd` and friends return `long` (or `Long`). Assert with the `L` suffix +or AssertJ compares against the wrong boxed type: + +```java +long res = syncCommands.hincrby("bike:1:stats", "rides", 1); +assertThat(res).isEqualTo(1L); +``` + +### 6. Shut the client down, not just the connection + +`try`-with-resources closes the `StatefulRedisConnection`, but `RedisClient` holds the event +loop group and needs `shutdown()` or the JVM hangs at the end of the test. Put it in a +`finally` inside a `HIDE` block. + +## Running Tests + +```bash +# Via the harness +build/example-test-harness/run.sh --fidelity cmds_hash lettuce-sync +build/example-test-harness/run.sh cmds_hash lettuce-sync # portable + +# Directly, from the fidelity dir +cd tmp/clients/examples/lettuce-sync && mvn test +``` + +`SampleTest.java` in this directory has been compiled and run against **lettuce-core 7.4.0** +on Redis 8.10; its `>>>` comments are the real observed output. + +> Note: the portable harness pins lettuce-core **6.5.5.RELEASE** while fidelity pins +> **7.4.0.RELEASE**. See the "Known divergence" section of +> `.claude/skills/tce-examples/reference/testing.md` — a Java example can pass in one mode and +> fail in the other until that's reconciled. + +## See Also + +- `../lettuce-async/LETTUCE_ASYNC_TEST_PATTERNS.md` — the `CompletableFuture` flavour +- `../lettuce-reactive/LETTUCE_REACTIVE_TEST_PATTERNS.md` — the `Mono`/`Flux` flavour +- `build/example-test-harness/clients.tsv` — filename convention and paths +- `for-ais-only/tcedocs/SPECIFICATION.md` — full marker semantics diff --git a/.claude/skills/tce-examples/assets/lettuce-sync/SampleTest.java b/.claude/skills/tce-examples/assets/lettuce-sync/SampleTest.java new file mode 100644 index 0000000000..37219158c1 --- /dev/null +++ b/.claude/skills/tce-examples/assets/lettuce-sync/SampleTest.java @@ -0,0 +1,114 @@ +// ============================================================================= +// CANONICAL LETTUCE SYNC TEST FILE TEMPLATE +// ============================================================================= +// This file demonstrates the structure and conventions used for Lettuce sync +// documentation test files. These tests serve dual purposes: +// 1. Executable tests that validate code snippets +// 2. Source for documentation code examples (processed via special markers) +// +// MARKER REFERENCE: +// - EXAMPLE: - Identifies the example name (matches docs folder name) +// - BINDER_ID - Optional identifier for online code runners +// - HIDE_START/HIDE_END - Code hidden from documentation but executed in tests +// - REMOVE_START/REMOVE_END - Code removed entirely from documentation output +// - STEP_START /STEP_END - Named code section for targeted doc inclusion +// +// Lettuce sync returns values directly — no CompletableFuture chaining. This is +// what makes it the most readable of the three Lettuce flavours for docs. +// RUN: mvn test -Dtest=SampleTest +// ============================================================================= + +// EXAMPLE: sample_example +package io.redis.examples.sync; + +import io.lettuce.core.*; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.api.StatefulRedisConnection; + +// REMOVE_START +import org.junit.jupiter.api.Test; +// REMOVE_END +import java.util.*; +// REMOVE_START +import static org.assertj.core.api.Assertions.assertThat; +// REMOVE_END + +public class SampleTest { + + // REMOVE_START + @Test + // REMOVE_END + public void run() { + RedisClient redisClient = RedisClient.create("redis://localhost:6379"); + + try (StatefulRedisConnection connection = redisClient.connect()) { + RedisCommands syncCommands = connection.sync(); + + // REMOVE_START + syncCommands.del("mykey", "myhash", "bike:1:stats"); + // REMOVE_END + + // STEP_START string_ops + String res1 = syncCommands.set("mykey", "Hello"); + System.out.println(res1); // >>> OK + + String res2 = syncCommands.get("mykey"); + System.out.println(res2); // >>> Hello + // STEP_END + + // REMOVE_START + assertThat(res1).isEqualTo("OK"); + assertThat(res2).isEqualTo("Hello"); + syncCommands.del("mykey"); + // REMOVE_END + + // STEP_START hash_ops + // hset returns true only when the field is NEW; an update returns false. + boolean res3 = syncCommands.hset("myhash", "field1", "value1"); + System.out.println(res3); // >>> true + + Map fields = new HashMap<>(); + fields.put("field2", "value2"); + fields.put("field3", "value3"); + String res4 = syncCommands.hmset("myhash", fields); + System.out.println(res4); // >>> OK + + String res5 = syncCommands.hget("myhash", "field1"); + System.out.println(res5); // >>> value1 + + // hgetall returns a Map; iteration order is not guaranteed, so sort keys + // before printing if the output comment has to be stable. + Map res6 = syncCommands.hgetall("myhash"); + System.out.println(new TreeMap<>(res6)); + // >>> {field1=value1, field2=value2, field3=value3} + // STEP_END + + // REMOVE_START + assertThat(res3).isTrue(); + assertThat(res4).isEqualTo("OK"); + assertThat(res5).isEqualTo("value1"); + assertThat(res6).containsEntry("field2", "value2"); + syncCommands.del("myhash"); + // REMOVE_END + + // STEP_START numeric_ops + syncCommands.hset("bike:1:stats", "rides", "0"); + long res7 = syncCommands.hincrby("bike:1:stats", "rides", 1); + System.out.println(res7); // >>> 1 + + long res8 = syncCommands.hincrby("bike:1:stats", "rides", 1); + System.out.println(res8); // >>> 2 + // STEP_END + + // REMOVE_START + assertThat(res7).isEqualTo(1L); + assertThat(res8).isEqualTo(2L); + syncCommands.del("bike:1:stats"); + // REMOVE_END + // HIDE_START + } finally { + redisClient.shutdown(); + } + // HIDE_END + } +} diff --git a/.agents/skills/generate-tce-examples/assets/node-redis/NODE_REDIS_TEST_PATTERNS.md b/.claude/skills/tce-examples/assets/node-redis/NODE_REDIS_TEST_PATTERNS.md similarity index 100% rename from .agents/skills/generate-tce-examples/assets/node-redis/NODE_REDIS_TEST_PATTERNS.md rename to .claude/skills/tce-examples/assets/node-redis/NODE_REDIS_TEST_PATTERNS.md diff --git a/.agents/skills/generate-tce-examples/assets/node-redis/sample_test.js b/.claude/skills/tce-examples/assets/node-redis/sample_test.js similarity index 100% rename from .agents/skills/generate-tce-examples/assets/node-redis/sample_test.js rename to .claude/skills/tce-examples/assets/node-redis/sample_test.js diff --git a/.agents/skills/generate-tce-examples/assets/nredisstack/NREDISSTACK_TEST_PATTERNS.md b/.claude/skills/tce-examples/assets/nredisstack/NREDISSTACK_TEST_PATTERNS.md similarity index 100% rename from .agents/skills/generate-tce-examples/assets/nredisstack/NREDISSTACK_TEST_PATTERNS.md rename to .claude/skills/tce-examples/assets/nredisstack/NREDISSTACK_TEST_PATTERNS.md diff --git a/.agents/skills/generate-tce-examples/assets/nredisstack/nredisstack_sample_test.cs b/.claude/skills/tce-examples/assets/nredisstack/nredisstack_sample_test.cs similarity index 100% rename from .agents/skills/generate-tce-examples/assets/nredisstack/nredisstack_sample_test.cs rename to .claude/skills/tce-examples/assets/nredisstack/nredisstack_sample_test.cs diff --git a/.agents/skills/generate-tce-examples/assets/predis/PREDIS_TEST_PATTERNS.md b/.claude/skills/tce-examples/assets/predis/PREDIS_TEST_PATTERNS.md similarity index 100% rename from .agents/skills/generate-tce-examples/assets/predis/PREDIS_TEST_PATTERNS.md rename to .claude/skills/tce-examples/assets/predis/PREDIS_TEST_PATTERNS.md diff --git a/.agents/skills/generate-tce-examples/assets/predis/SampleTest.php b/.claude/skills/tce-examples/assets/predis/SampleTest.php similarity index 100% rename from .agents/skills/generate-tce-examples/assets/predis/SampleTest.php rename to .claude/skills/tce-examples/assets/predis/SampleTest.php diff --git a/.agents/skills/generate-tce-examples/assets/redis-py/REDIS_PY_TEST_PATTERNS.md b/.claude/skills/tce-examples/assets/redis-py/REDIS_PY_TEST_PATTERNS.md similarity index 100% rename from .agents/skills/generate-tce-examples/assets/redis-py/REDIS_PY_TEST_PATTERNS.md rename to .claude/skills/tce-examples/assets/redis-py/REDIS_PY_TEST_PATTERNS.md diff --git a/.agents/skills/generate-tce-examples/assets/redis-py/sample_test.py b/.claude/skills/tce-examples/assets/redis-py/sample_test.py similarity index 100% rename from .agents/skills/generate-tce-examples/assets/redis-py/sample_test.py rename to .claude/skills/tce-examples/assets/redis-py/sample_test.py diff --git a/.claude/skills/tce-examples/assets/report-template.md b/.claude/skills/tce-examples/assets/report-template.md new file mode 100644 index 0000000000..8c487cb45d --- /dev/null +++ b/.claude/skills/tce-examples/assets/report-template.md @@ -0,0 +1,60 @@ +# Phase 2 report + +Return this filled in. Terse is fine; complete matters. The orchestrator reads every report +together before Phase 3, so state facts rather than reassurance — an honest "not tested, +toolchain missing" is more useful than an optimistic "should work". + +--- + +## Report: `{{CLIENT_KEY}}` / `{{SET_ID}}` + +**File written:** `` + +**Started from:** `local_examples/...` | client repo `...` | new from sample +(If you started from an existing file, how many steps did it already have, and did you keep +all of them?) + +**Steps implemented:** `` + +**Pre-existing steps preserved:** `` | none in file + +### Tested + +``` + +``` + +**Result:** PASS | FAIL | NOT RUN + +If NOT RUN, say why (missing toolchain, no Redis, SKIP with unresolved path). Do not claim +a pass you didn't observe. + +If FAIL, paste the relevant lines from `build/example-test-harness/results/_.log`. + +### Output comments + +How did you determine each `>>> ` value — observed from the actual run, or derived from the +reference? Flag any you could not confirm by running. + +### API signatures + +**Source:** `data/command-api-mapping/.json` + +Any command missing an entry for this client? Any signature in the mapping that looked wrong +against the client's real API? + +### Deviations from the reference + +Anything you did differently, and why. Client idiom is a legitimate reason; guessing is not. +If you changed the observable behaviour or the command sequence, say so loudly — that breaks +cross-client consistency and the orchestrator needs to decide, not discover. + +### Questions the brief didn't answer + +List them even if you worked around them. Repeated questions across agents mean the brief +needs fixing for next time. + +### C# only — flavour + +Which tab this file targets, and the deciding import line as written: +`using NRedisStack` present | absent diff --git a/.claude/skills/tce-examples/assets/ruby/RUBY_TEST_PATTERNS.md b/.claude/skills/tce-examples/assets/ruby/RUBY_TEST_PATTERNS.md new file mode 100644 index 0000000000..6f5c0ee652 --- /dev/null +++ b/.claude/skills/tce-examples/assets/ruby/RUBY_TEST_PATTERNS.md @@ -0,0 +1,145 @@ +# redis-rb (Ruby) Test File Patterns + +This document describes the conventions used in redis-rb documentation test files. + +## Purpose + +These test files serve dual purposes: +1. **Executable Ruby scripts** - Validate code snippets work correctly +2. **Documentation source** - Code is extracted for redis.io documentation + +## File Locations + +- **Staging**: `local_examples//ruby/*.rb` (also the older flat `local_examples/ruby/*.rb`) +- **Upstream**: `redis-rb` repo, `examples/` +- **Sample template**: `sample_test.rb` (in this directory) + +## Marker Reference + +| Marker | Purpose | +|--------|---------| +| `# EXAMPLE: ` | Identifies example name (matches docs folder) | +| `# BINDER_ID ` | Optional identifier for online code runners | +| `# HIDE_START` / `# HIDE_END` | Code hidden from docs but still executed | +| `# REMOVE_START` / `# REMOVE_END` | Code completely removed from docs | +| `# STEP_START ` / `# STEP_END` | Named section for targeted doc inclusion | + +## File Structure Template + +```ruby +# EXAMPLE: example_name + +# HIDE_START +require 'redis' + +r = Redis.new +# HIDE_END + +# REMOVE_START +def assert_equal(expected, actual) + raise "Expected #{expected.inspect}, got #{actual.inspect}" unless actual == expected +end + +r.del('mykey') +# REMOVE_END + +# STEP_START operation_name +res1 = r.set('mykey', 'Hello') +puts res1 # >>> OK +# STEP_END + +# REMOVE_START +assert_equal('OK', res1) +r.del('mykey') +# REMOVE_END +``` + +## Key Patterns + +### 1. No test framework — assert locally + +Ruby examples do **not** use minitest or RSpec. Define `assert_equal` inside a `REMOVE` +block and let it `raise`. A raise gives `ruby` a non-zero exit status, which is what the +harness reads. Every file that asserts needs its own copy of the helper — there is no shared +base class to inherit it from, unlike the Java, C#, and PHP clients. + +```ruby +# REMOVE_START +def assert_equal(expected, actual) + raise "Expected #{expected.inspect}, got #{actual.inspect}" unless actual == expected +end +# REMOVE_END +``` + +### 2. Connection + +`Redis.new` with no arguments connects to `localhost:6379` db 0. No options hash needed, and +the docs read better without one. + +```ruby +r = Redis.new +``` + +### 3. `inspect` for collections, bare `puts` for scalars + +`puts` on a Hash or Array prints an unhelpful form (and `puts []` prints nothing at all). Use +`.inspect` for any collection, and write the `>>>` comment exactly as `inspect` renders it: + +```ruby +res = r.hgetall('myhash') +puts res.inspect +# >>> {"field1"=>"value1", "field2"=>"value2"} +``` + +Note the `=>` hash-rocket form — that is what `inspect` actually emits. Output comments are +published verbatim, so writing the modern `{field1: "value1"}` there would show readers +something Ruby never printed. + +### 4. Replies are strings, except counters + +Values read back from Redis are strings even when written as integers. The reply of an +increment command **is** an Integer. This is the most common assertion failure: + +```ruby +r.hset('bike:1', 'price', 4972) +assert_equal('4972', r.hget('bike:1', 'price')) # String, not 4972 +assert_equal(1, r.hincrby('bike:1:stats', 'rides', 1)) # Integer +``` + +### 5. Multi-value writes take Ruby collections + +`hset` takes a Hash; `zadd` takes an array of `[score, member]` pairs — not flat argument +lists: + +```ruby +r.hset('myhash', { 'field1' => 'value1', 'field2' => 'value2' }) +r.zadd('myzset', [[1, 'one'], [2, 'two']]) +``` + +### 6. Cleanup + +Delete keys in a `REMOVE` block both before and after the steps that use them. Prefer +targeted `r.del(...)` over `flushall` — the harness already flushes between clients, and a +`flushall` inside an example is a hazard if the harness is ever pointed at a real database. + +## Running Tests + +```bash +# Via the harness (resolves the path, flushes Redis, reports pass/fail) +build/example-test-harness/run.sh cmds_sorted_set ruby + +# Directly +ruby local_examples/cmds_sorted_set/ruby/cmds_sorted_set.rb +``` + +Needs the `redis` gem and a scratch Redis on `localhost:6379`. In fidelity mode the gem is +pinned by `build/example-test-harness/fidelity/Gemfile-ruby`. + +`sample_test.rb` in this directory has been run against Redis 8.10 and exits 0; its `>>>` +comments are the real observed output. + +## See Also + +- `build/example-test-harness/clients.tsv` — filename convention and paths for this client +- `for-ais-only/tcedocs/SPECIFICATION.md` — full marker semantics +- `.claude/skills/tce-examples/SKILL.md` — the workflow these files feed diff --git a/.claude/skills/tce-examples/assets/ruby/sample_test.rb b/.claude/skills/tce-examples/assets/ruby/sample_test.rb new file mode 100644 index 0000000000..6f362d8a7c --- /dev/null +++ b/.claude/skills/tce-examples/assets/ruby/sample_test.rb @@ -0,0 +1,95 @@ +# ============================================================================= +# CANONICAL redis-rb (RUBY) TEST FILE TEMPLATE +# ============================================================================= +# This file demonstrates the structure and conventions used for redis-rb +# documentation test files. These tests serve dual purposes: +# 1. Executable Ruby scripts that validate code snippets +# 2. Source for documentation code examples (processed via special markers) +# +# MARKER REFERENCE: +# - EXAMPLE: - Identifies the example name (matches docs folder name) +# - BINDER_ID - Optional identifier for online code runners +# - HIDE_START/HIDE_END - Code hidden from documentation but executed in tests +# - REMOVE_START/REMOVE_END - Code removed entirely from documentation output +# - STEP_START /STEP_END - Named code section for targeted doc inclusion +# +# RUN: ruby sample_test.rb (needs the `redis` gem and a scratch Redis) +# ============================================================================= + +# EXAMPLE: sample_example + +# HIDE_START +require 'redis' + +r = Redis.new +# HIDE_END + +# REMOVE_START +# redis-rb examples have no test framework: define the assertion locally and let it +# raise. A raise gives ruby a non-zero exit status, which is what the harness reads. +def assert_equal(expected, actual) + raise "Expected #{expected.inspect}, got #{actual.inspect}" unless actual == expected +end + +r.del('mykey', 'myhash', 'mylist') +# REMOVE_END + +# STEP_START string_ops +res1 = r.set('mykey', 'Hello') +puts res1 # >>> OK + +res2 = r.get('mykey') +puts res2 # >>> Hello +# STEP_END + +# REMOVE_START +assert_equal('OK', res1) +assert_equal('Hello', res2) +r.del('mykey') +# REMOVE_END + +# STEP_START hash_ops +res3 = r.hset('myhash', 'field1', 'value1') +puts res3 # >>> 1 + +# A hash is set from a Ruby Hash, not a flat argument list. +res4 = r.hset('myhash', { 'field2' => 'value2', 'field3' => 'value3' }) +puts res4 # >>> 2 + +res5 = r.hget('myhash', 'field1') +puts res5 # >>> value1 + +# inspect is needed for collections: puts on a Hash prints its to_s, which is not +# the form the docs should show. +res6 = r.hgetall('myhash') +puts res6.inspect +# >>> {"field1"=>"value1", "field2"=>"value2", "field3"=>"value3"} +# STEP_END + +# REMOVE_START +assert_equal(1, res3) +assert_equal(2, res4) +assert_equal('value1', res5) +assert_equal({ 'field1' => 'value1', 'field2' => 'value2', 'field3' => 'value3' }, res6) +r.del('myhash') +# REMOVE_END + +# STEP_START numeric_ops +# Values come back as strings; only the reply of an increment is an Integer. +r.hset('bike:1:stats', 'rides', 0) +res7 = r.hincrby('bike:1:stats', 'rides', 1) +puts res7 # >>> 1 + +res8 = r.hincrby('bike:1:stats', 'rides', 1) +puts res8 # >>> 2 +# STEP_END + +# REMOVE_START +assert_equal(1, res7) +assert_equal(2, res8) +r.del('bike:1:stats') +# REMOVE_END + +# HIDE_START +r.close +# HIDE_END diff --git a/.agents/skills/generate-tce-examples/assets/rust-async/RUST_ASYNC_TEST_PATTERNS.md b/.claude/skills/tce-examples/assets/rust-async/RUST_ASYNC_TEST_PATTERNS.md similarity index 100% rename from .agents/skills/generate-tce-examples/assets/rust-async/RUST_ASYNC_TEST_PATTERNS.md rename to .claude/skills/tce-examples/assets/rust-async/RUST_ASYNC_TEST_PATTERNS.md diff --git a/.agents/skills/generate-tce-examples/assets/rust-async/sample_test.rs b/.claude/skills/tce-examples/assets/rust-async/sample_test.rs similarity index 100% rename from .agents/skills/generate-tce-examples/assets/rust-async/sample_test.rs rename to .claude/skills/tce-examples/assets/rust-async/sample_test.rs diff --git a/.agents/skills/generate-tce-examples/assets/rust-sync/RUST_SYNC_TEST_PATTERNS.md b/.claude/skills/tce-examples/assets/rust-sync/RUST_SYNC_TEST_PATTERNS.md similarity index 100% rename from .agents/skills/generate-tce-examples/assets/rust-sync/RUST_SYNC_TEST_PATTERNS.md rename to .claude/skills/tce-examples/assets/rust-sync/RUST_SYNC_TEST_PATTERNS.md diff --git a/.agents/skills/generate-tce-examples/assets/rust-sync/sample_test.rs b/.claude/skills/tce-examples/assets/rust-sync/sample_test.rs similarity index 100% rename from .agents/skills/generate-tce-examples/assets/rust-sync/sample_test.rs rename to .claude/skills/tce-examples/assets/rust-sync/sample_test.rs diff --git a/.claude/skills/tce-examples/assets/seredis/SEREDIS_TEST_PATTERNS.md b/.claude/skills/tce-examples/assets/seredis/SEREDIS_TEST_PATTERNS.md new file mode 100644 index 0000000000..557654d57b --- /dev/null +++ b/.claude/skills/tce-examples/assets/seredis/SEREDIS_TEST_PATTERNS.md @@ -0,0 +1,201 @@ +# StackExchange.Redis (SE.Redis) Test File Patterns + +Conventions for the **`C#-Sync (SE.Redis)`** and **`C#-Async (SE.Redis)`** documentation tabs. +For examples that use NRedisStack's module APIs, see `../nredisstack/`. + +## The thing to get right first: which tab this feeds + +The two C# client families are **not two codebases**. Both are fed from the same directory in +the `NRedisStack` repo (`tests/Doc`), and `data/components/` partitions them with a content +filter on a single import: + +| The file… | feeds | +|---|---| +| does **not** contain `using NRedisStack` | the **SE.Redis** tabs | +| **does** contain `using NRedisStack` | the **NRedisStack** tabs | + +`using NRedisStack.Tests` is explicitly excluded from that test, because every file in +`tests/Doc` has it for the test fixtures. + +So an SE.Redis example is defined by what it *doesn't* import. Adding an NRedisStack import — +even an unused one, even while debugging — silently relocates the finished example to the +other tab. Both flavours share identical test scaffolding, so nothing fails; the example just +appears in the wrong place. + +Most command-page C# examples belong here, not in `../nredisstack/`: plain string, hash, list, +set, and sorted-set commands need only SE.Redis. Reach for NRedisStack when the example uses +a module API (JSON, search, time series, probabilistic types). + +## File Locations + +- **Staging**: `local_examples//seredis/*.cs` +- **Upstream**: `NRedisStack` repo, `tests/Doc/` (sync) and `tests/Doc/Async/` (async) +- **Sample template**: `sample_test.cs` (in this directory) + +> The async component config points at `tests/Doc/Async/`, which does not currently exist in +> the clone. An async example is the first thing that would need that directory created. + +## Marker Reference + +| Marker | Purpose | +|--------|---------| +| `// EXAMPLE: ` | Identifies example name (matches docs folder) | +| `// BINDER_ID ` | Optional identifier for online code runners | +| `// HIDE_START` / `// HIDE_END` | Code hidden from docs but still executed | +| `// REMOVE_START` / `// REMOVE_END` | Code completely removed from docs | +| `// STEP_START ` / `// STEP_END` | Named section for targeted doc inclusion | + +## File Structure Template + +The scaffolding is fixed — copy it exactly. It is what lets the same file be both a real +xunit test in the NRedisStack repo and a clean snippet on the docs page. + +```csharp +// EXAMPLE: cmds_hash +// HIDE_START +using StackExchange.Redis; +// HIDE_END +// REMOVE_START +using NRedisStack.Tests; + +namespace Doc; + +[Collection("DocsTests")] +// REMOVE_END + +// HIDE_START +public class CmdsHashExample +// REMOVE_START + : AbstractNRedisStackTest, IDisposable +// REMOVE_END +{ + // REMOVE_START + public CmdsHashExample(EndpointsFixture fixture) : base(fixture) { } + + [Fact] + // REMOVE_END + public void Run() + { + // REMOVE_START + SkipIfTargetConnectionDoesNotExist(EndpointsFixture.Env.Standalone); + var _ = GetCleanDatabase(EndpointsFixture.Env.Standalone); + // REMOVE_END + var muxer = ConnectionMultiplexer.Connect("localhost:6379"); + var db = muxer.GetDatabase(); + // REMOVE_START + db.KeyDelete("myhash"); + // REMOVE_END + // HIDE_END + + // STEP_START hset + bool res1 = db.HashSet("myhash", "field1", "value1"); + Console.WriteLine(res1); // >>> True + // STEP_END + + // REMOVE_START + Assert.True(res1); + db.KeyDelete("myhash"); + // REMOVE_END + + // HIDE_START + muxer.Close(); + } +} +// HIDE_END +``` + +## Key Patterns + +### 1. The fixture API is `EndpointsFixture`, not `RedisFixture` + +The current NRedisStack test base takes an `EndpointsFixture` and its helpers take an +`Env` argument: + +```csharp +public CmdsHashExample(EndpointsFixture fixture) : base(fixture) { } +... +SkipIfTargetConnectionDoesNotExist(EndpointsFixture.Env.Standalone); +var _ = GetCleanDatabase(EndpointsFixture.Env.Standalone); +``` + +Older docs and older examples show `RedisFixture`, a no-argument +`SkipIfTargetConnectionDoesNotExist()`, and `GetCleanDatabase(muxer)`. Those no longer +compile. + +### 2. The example creates its own multiplexer + +Even though the fixture can hand you a database, the snippet on the docs page has to show a +reader how to connect. So `GetCleanDatabase(...)` is called and discarded into `var _`, and +the visible code does its own `ConnectionMultiplexer.Connect(...)`. + +### 3. The method is `Run()`, capitalised + +`[Fact]` sits inside the `REMOVE` block immediately above it, so the docs show a plain method +and the test runner still finds it. If the `[Fact]` ends up removed or commented, +`dotnet test` matches nothing and **still exits 0** — the generated C# wrapper now treats +`No test matches` as a failure for exactly this reason. + +### 4. Return types are specific, and not always what you'd guess + +```csharp +bool res1 = db.StringSet("mykey", "Hello"); // bool, not "OK" +bool res2 = db.HashSet("myhash", "f", "v"); // true only when the field is NEW + db.HashSet("myhash", [new("f2","v2")]); // multi-field overload returns VOID +RedisValue res3 = db.HashGet("myhash", "f"); // RedisValue, not string +long res4 = db.HashIncrement("myhash", "n", 1); // long +HashEntry[] res5 = db.HashGetAll("myhash"); // HashEntry[], not a dictionary +``` + +### 5. A missing value is `RedisValue.Null`, which prints as empty + +`Console.WriteLine(db.HashGet("myhash", "nofield"))` prints **nothing** — an empty line. That +makes a bare `// >>> ` comment ambiguous for the reader, so print the check instead: + +```csharp +RedisValue res = db.HashGet("myhash", "nofield"); +Console.WriteLine(res.IsNull); // >>> True +``` + +### 6. Collections need explicit formatting + +There is no useful `ToString()` for `HashEntry[]` or `RedisValue[]`. Project and join, and +make the `>>>` comment match the joined form exactly: + +```csharp +HashEntry[] res = db.HashGetAll("myhash"); +Console.WriteLine(string.Join(", ", res.Select(h => $"{h.Name}: {h.Value}"))); +// >>> field1: value1, field2: value2, field3: value3 +``` + +### 7. Async flavour + +The async tabs use the `*Async` methods and `await`, with the class method as +`public async Task Run()`. Same scaffolding, `namespace Doc;` unchanged, package directory +`tests/Doc/Async/`. + +## Running Tests + +```bash +# Portable: xunit + SE.Redis 3.0.0, with dotnet/stubs.cs standing in for the fixtures +build/example-test-harness/run.sh cmds_hash seredis + +# Fidelity: inside the NRedisStack clone, against its real Doc.csproj and real fixtures +build/example-test-harness/run.sh --fidelity cmds_hash seredis +``` + +Both C# flavours share the `dotnet` runner — the flavour distinction is about which docs tab +the file feeds, not about how it is tested. + +`sample_test.cs` in this directory has been compiled and run against **SE.Redis 3.0.0** on +Redis 8.10 (1 test, passing); its `>>>` comments are the real observed output. + +> Portable mode pins SE.Redis 3.0.0 / xunit 2.9.2 / net9.0; the real `Doc.csproj` uses +> SE.Redis 3.0.25 / xunit.v3 / `net8.0;net10.0;net481`. `[Fact]` and `[Collection]` behave the +> same across those, but fidelity mode is the one that proves it. + +## See Also + +- `../nredisstack/NREDISSTACK_TEST_PATTERNS.md` — the module-API flavour +- `build/example-test-harness/dotnet/stubs.cs` — what portable mode substitutes for the fixtures +- `build/example-test-harness/clients.tsv` — filename convention and paths +- `for-ais-only/tcedocs/SPECIFICATION.md` — full marker semantics diff --git a/.claude/skills/tce-examples/assets/seredis/sample_test.cs b/.claude/skills/tce-examples/assets/seredis/sample_test.cs new file mode 100644 index 0000000000..0d3f112375 --- /dev/null +++ b/.claude/skills/tce-examples/assets/seredis/sample_test.cs @@ -0,0 +1,133 @@ +// ============================================================================= +// CANONICAL StackExchange.Redis (SE.Redis) TEST FILE TEMPLATE +// ============================================================================= +// This file demonstrates the structure and conventions used for the SE.Redis +// documentation tabs. These tests serve dual purposes: +// 1. Executable xunit tests that validate code snippets +// 2. Source for documentation code examples (processed via special markers) +// +// MARKER REFERENCE: +// - EXAMPLE: - Identifies the example name (matches docs folder name) +// - BINDER_ID - Optional identifier for online code runners +// - HIDE_START/HIDE_END - Code hidden from documentation but executed in tests +// - REMOVE_START/REMOVE_END - Code removed entirely from documentation output +// - STEP_START /STEP_END - Named code section for targeted doc inclusion +// +// CRITICAL — WHICH TAB THIS FEEDS: +// The "C#-Sync (SE.Redis)" and "C#-Sync (NRedisStack)" docs tabs are fed from the +// SAME directory in the NRedisStack repo (tests/Doc). They are separated by a +// content filter on whether the file imports NRedisStack: +// this file must NOT contain `using NRedisStack` -> SE.Redis tab +// a file that DOES import it -> NRedisStack tab +// `using NRedisStack.Tests` is excluded from that test — every file has it for the +// test fixtures. Adding an NRedisStack import to this file silently moves the +// finished example into the wrong tab. +// +// RUN: dotnet test tests/Doc --filter FullyQualifiedName~SampleExample +// ============================================================================= + +// EXAMPLE: sample_example +// HIDE_START +using StackExchange.Redis; +// HIDE_END +// REMOVE_START +using NRedisStack.Tests; + +namespace Doc; + +[Collection("DocsTests")] +// REMOVE_END + +// HIDE_START +public class SampleExample +// REMOVE_START + : AbstractNRedisStackTest, IDisposable +// REMOVE_END +{ + // REMOVE_START + public SampleExample(EndpointsFixture fixture) : base(fixture) { } + + [Fact] + // REMOVE_END + public void Run() + { + // REMOVE_START + SkipIfTargetConnectionDoesNotExist(EndpointsFixture.Env.Standalone); + var _ = GetCleanDatabase(EndpointsFixture.Env.Standalone); + // REMOVE_END + var muxer = ConnectionMultiplexer.Connect("localhost:6379"); + var db = muxer.GetDatabase(); + // REMOVE_START + db.KeyDelete("mykey"); + db.KeyDelete("myhash"); + db.KeyDelete("bike:1:stats"); + // REMOVE_END + // HIDE_END + + // STEP_START string_ops + bool res1 = db.StringSet("mykey", "Hello"); + Console.WriteLine(res1); // >>> True + + RedisValue res2 = db.StringGet("mykey"); + Console.WriteLine(res2); // >>> Hello + // STEP_END + + // REMOVE_START + Assert.True(res1); + Assert.Equal("Hello", res2); + db.KeyDelete("mykey"); + // REMOVE_END + + // STEP_START hash_ops + // HashSet with a single field returns true only when the field is NEW. + bool res3 = db.HashSet("myhash", "field1", "value1"); + Console.WriteLine(res3); // >>> True + + // The multi-field overload returns void, not a count. + db.HashSet("myhash", + [ + new("field2", "value2"), + new("field3", "value3") + ] + ); + + RedisValue res4 = db.HashGet("myhash", "field1"); + Console.WriteLine(res4); // >>> value1 + + // A missing field yields RedisValue.Null, which prints as an empty string. + RedisValue res5 = db.HashGet("myhash", "nofield"); + Console.WriteLine(res5.IsNull); // >>> True + + HashEntry[] res6 = db.HashGetAll("myhash"); + Console.WriteLine(string.Join(", ", res6.Select(h => $"{h.Name}: {h.Value}"))); + // >>> field1: value1, field2: value2, field3: value3 + // STEP_END + + // REMOVE_START + Assert.True(res3); + Assert.Equal("value1", res4); + Assert.True(res5.IsNull); + Assert.Equal(3, res6.Length); + db.KeyDelete("myhash"); + // REMOVE_END + + // STEP_START numeric_ops + db.HashSet("bike:1:stats", "rides", 0); + long res7 = db.HashIncrement("bike:1:stats", "rides", 1); + Console.WriteLine(res7); // >>> 1 + + long res8 = db.HashIncrement("bike:1:stats", "rides", 1); + Console.WriteLine(res8); // >>> 2 + // STEP_END + + // REMOVE_START + Assert.Equal(1, res7); + Assert.Equal(2, res8); + db.KeyDelete("bike:1:stats"); + // REMOVE_END + + // HIDE_START + muxer.Close(); + } +} +// HIDE_END diff --git a/.claude/skills/tce-examples/reference/testing.md b/.claude/skills/tce-examples/reference/testing.md new file mode 100644 index 0000000000..a69e639268 --- /dev/null +++ b/.claude/skills/tce-examples/reference/testing.md @@ -0,0 +1,156 @@ +# Testing and evaluating TCE examples + +Two environments and one Codex gate. Client identity for all of them comes from +`build/example-test-harness/clients.tsv`. + +## Which environment + +| | `--portable` (default) | `--fidelity` | +|---|---|---| +| 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) | +| Speed | seconds once warm | minutes — full toolchain install per client | + +**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 +class, the real dependency versions, or the real project layout. + +```bash +redis-server --daemonize yes # scratch instance — the harness FLUSHes it +build/example-test-harness/run.sh cmds_hash # portable, all clients +build/example-test-harness/run.sh cmds_hash redis-py jedis # portable, some clients +build/example-test-harness/run.sh --fidelity cmds_hash # fidelity +build/example-test-harness/run.sh --list cmds_hash # resolve sources only, no Redis +``` + +`--list` is the fastest way to answer "did it even find my file?" before blaming a toolchain. + +## Setting up fidelity mode + +```bash +build/example-test-harness/bootstrap.sh # scaffold + clone/update client repos +build/example-test-harness/bootstrap.sh --no-clone # scaffold only +build/example-test-harness/bootstrap.sh --check # report gaps, change nothing +``` + +It materialises `tmp/clients/examples/` (gitignored) from `fidelity/` and generates each +client's `run.sh`. The wrappers are generated, not tracked, because they are one to three +lines each and differed only in the command — three different argument conventions between +them was what made the old environment impossible to drive. Re-run it any time; it's +idempotent. + +`bootstrap.sh` ends with a toolchain report. A `MISSING` line there is why a client SKIPs. + +## Why the Redis phase is serial + +Several examples call `FLUSHALL`/`FLUSHDB`, and the harness flushes before each client, so +runs cannot share a Redis instance concurrently. Per-DB isolation doesn't help — `FLUSHALL` +crosses databases. Running each client against its own instance would mean rewriting the +connection string in the example under test, which defeats the point of testing what ships. + +So the harness runs clients one at a time. **The parallelism in this workflow is in Phase 2 +code generation, not test execution.** Portable mode's dependency cache is what makes the +iteration loop fast; fidelity mode reinstalls everything per client by design (see +"Teardown", below). + +## Teardown + +Every fidelity wrapper deletes its dependency cache on exit (`venv`, `node_modules`, +`target`, `vendor`, `Cargo.lock`, `.gems`). This is deliberate — it matches the hand-built +environment and guarantees no stale artifacts — but it means fidelity mode cannot be made +fast, and dependency/build work cannot be hoisted into a parallel phase. If fidelity runs +ever need to be quick, that's the trade to revisit first. + +Portable mode caches in `work/` and does not tear down. + +## Traps this harness now guards against + +These are all real failures that produced green results before: + +- **Surefire runs zero tests and exits 0.** The Java classes are named `*Example`, not + `*Test`, so a `pom.xml` without `**/*Example.java` matches nothing and + "passes". `fidelity/pom-lettuce-async.xml` and `pom-lettuce-reactive.xml` were both + missing it. The generated Java wrapper now fails unless it sees `Tests run: [1-9]`. +- **A wrapper's exit code was the teardown's.** Every original wrapper ended with + `rm -fr `, so the script exited 0 whatever the test did — a failing example reported + success. The generated wrappers capture `rc` before teardown and `exit $rc`. +- **`dotnet test` matching no tests.** If the `[Fact]` doesn't survive outside a `REMOVE` + block, the filter matches nothing. The generated C# wrapper treats + `No test matches`/`No test is available` as failure. +- **Wrong-case example paths.** `cmds_*` sets use `local_examples//NRedisStack/` while + `geoindex`, `search_quickstart`, and `time_series_tutorial` use `nredisstack/`. Both are + tracked in git. On a case-insensitive filesystem (macOS default) a glob on the wrong + spelling succeeds and yields a path that doesn't exist in git and fails on Linux CI, so + resolution matches directory names with exact case. +- **One C# file counted as four clients.** Four `clients.tsv` rows share the `dotnet` + runner. Only the primary row may claim a legacy path entry, or a single `.cs` file gets + reported as four passing clients — and an NRedisStack-flavoured file gets credited to the + SE.Redis tab. + +## Known divergence: Java versions + +| | jedis | lettuce-core | +|---|---|---| +| portable (`pom-*.xml`) | 7.5.3 | 6.5.5.RELEASE | +| fidelity (`fidelity/pom-*.xml`) | 7.4.0 | 7.4.0.RELEASE | + +Not yet reconciled. The fidelity pins are what the current `local_examples/` Java files were +actually tested against, so they're the known-good pair; the portable jedis 7.5.3 bump was +deliberate (the search examples need the `RedisClient` API it introduced). Settling on one +version per client requires running both Java toolchains against the search sets and the +Java-heavy command sets. Until that's done, **a Java example that passes in one mode may +fail in the other**, and that's information rather than a bug. + +## Codex evaluation (Phase 4) + +An independent reviewer with fresh context. It catches what the harness structurally cannot: +the harness proves the code *runs*, not that it's the right code. Wrong `>>>` output +comments, drifted step names, an API the docs shouldn't showcase, a C# file in the wrong +flavour, scaffolding that leaks into the rendered page — all of these pass tests. + +The binary ships inside the ChatGPT desktop app and is **not on `PATH`**: + +```bash +CODEX="$(command -v codex || echo /Applications/ChatGPT.app/Contents/Resources/codex)" +``` + +This makes the gate macOS-and-desktop-app dependent, which is why it's a local pre-merge +step rather than CI. + +One invocation per client, in parallel (these are read-only, so they don't contend): + +```bash +SKILL=.claude/skills/tce-examples +"$CODEX" exec \ + --cd "$PWD" \ + --sandbox read-only \ + --output-schema "$SKILL/schema/codex-verdict.json" \ + -o "$TMPDIR/verdict-$CLIENT.json" \ + "Review the TCE example at $FILE for the $CLIENT client. + + Reference implementation (the spec): $REFERENCE + Step names, exactly: $STEPS + Client API signatures: data/command-api-mapping/$COMMAND.json + Per-client conventions: $SKILL/assets/$ASSETS/*_TEST_PATTERNS.md + + Check, in priority order: + 1. Do the '>>> ' output comments match what this client actually returns, including + type and formatting? (Tests passing does not prove this.) + 2. Are step names identical to the reference, spelled the same way? + 3. Are the method signatures the ones in the API mapping? + 4. Would any scaffolding, assertion, banner comment, or REMOVE/HIDE content reach the + rendered page? + 5. For C#: does the presence or absence of 'using NRedisStack' match the intended tab? + + Report only actionable defects in this file. Do not restate what is correct." +``` + +`--output-schema` forces schema-valid JSON, so the orchestrator can gate on it without +parsing prose. Read `.codex/skills/claude-review/references/tce-review-patterns.md` for the +recurring defect classes; it's a map of where to look, not evidence. + +**Gate:** no unresolved `severity: "high"`. Verify every finding against the current file +before acting on it — a parallel agent may already have fixed it. diff --git a/.claude/skills/tce-examples/schema/codex-verdict.json b/.claude/skills/tce-examples/schema/codex-verdict.json new file mode 100644 index 0000000000..222d17d032 --- /dev/null +++ b/.claude/skills/tce-examples/schema/codex-verdict.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "TCE per-client review verdict", + "description": "Structured result of one Codex review of one client's TCE example file. Passed to `codex exec --output-schema` so the orchestrator can gate on findings without parsing prose.", + "type": "object", + "additionalProperties": false, + "//": "NOTE: `codex exec --output-schema` uses OpenAI structured outputs in strict mode, which requires `required` to list EVERY key in `properties` at every level. There is no way to mark a field optional — a schema with a genuinely optional field is rejected with a 400 before the model runs. Fields that may have nothing to say use an empty string or empty array instead.", + "required": ["client", "file", "verdict", "findings", "not_checked"], + "properties": { + "client": { + "type": "string", + "description": "Client key from build/example-test-harness/clients.tsv (e.g. redis-py, lettuce-async, seredis)." + }, + "file": { + "type": "string", + "description": "Repo-relative path of the example file reviewed." + }, + "verdict": { + "type": "string", + "enum": ["pass", "fail"], + "description": "fail if any finding has severity high; otherwise pass." + }, + "findings": { + "type": "array", + "description": "Actionable defects only. Empty when the file is correct — do not pad with observations about what is right.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["severity", "category", "line", "summary", "why_it_matters", "suggested_fix"], + "properties": { + "severity": { + "type": "string", + "enum": ["high", "medium", "low"], + "description": "high = wrong on the rendered page or wrong for the reader (bad output comment, wrong API, leaked scaffolding, wrong C# flavour). medium = inconsistent with the reference or the set. low = style." + }, + "category": { + "type": "string", + "enum": [ + "output-comment-mismatch", + "step-name-drift", + "wrong-api-signature", + "marker-placement", + "leaked-scaffolding", + "dropped-existing-step", + "csharp-flavor", + "assertion-weak", + "cleanup-missing", + "other" + ] + }, + "line": { + "type": "integer", + "minimum": 0, + "description": "1-indexed line in `file`. 0 when the finding is about the file as a whole." + }, + "summary": { + "type": "string", + "description": "One sentence stating the defect." + }, + "why_it_matters": { + "type": "string", + "description": "The concrete consequence — what a reader or maintainer sees because of this. Not a restatement of the summary." + }, + "suggested_fix": { + "type": "string", + "description": "The minimal change that resolves it. Empty string when there is nothing useful to suggest — strict mode forbids optional fields." + } + } + } + }, + "not_checked": { + "type": "array", + "description": "Anything the review could not assess (a file it could not read, an API it could not confirm). Empty array when nothing was skipped. Better to declare a gap than to guess.", + "items": { "type": "string" } + } + } +} diff --git a/.claude/skills/tce-examples/scripts/audit_page.py b/.claude/skills/tce-examples/scripts/audit_page.py new file mode 100644 index 0000000000..c92dfaa134 --- /dev/null +++ b/.claude/skills/tce-examples/scripts/audit_page.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +"""Phase 0 of the tce-examples skill: audit a docs page for TCE coverage. + +Finds every CLI example block on a page, extracts the Redis commands it demonstrates, +and reports which client libraries already implement it. Emits a human table by +default, or --json for a machine-readable work plan to drive the Phase 2 fan-out. + +Command extraction delegates to build/components/cli_parser.extract_cli_commands so +the prompt-parsing rules live in exactly one place (and stay covered by +build/test_cli_parser.py). One wrinkle it does NOT handle: that parser recognises +"> " and "redis> " but not a full "127.0.0.1:6379> " prompt, which appears in real +pages. We normalise host:port prompts to "> " before delegating rather than +duplicating the parser. + +Usage: + python3 .claude/skills/tce-examples/scripts/audit_page.py content/commands/hset.md + python3 .../audit_page.py --json content/commands/hset.md + python3 .../audit_page.py content/develop/data-types/*.md +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) +sys.path.insert(0, os.path.join(REPO_ROOT, "build")) + +from components.cli_parser import extract_cli_commands # noqa: E402 + +CLIENTS_TSV = os.path.join(REPO_ROOT, "build", "example-test-harness", "clients.tsv") +EXAMPLES_JSON = os.path.join(REPO_ROOT, "data", "examples.json") + +# A full redis-cli prompt, e.g. "127.0.0.1:6379> " or "localhost:6379> ". Normalised to +# "> " so extract_cli_commands sees a form it recognises. +HOSTPORT_PROMPT = re.compile(r"^\s*[\w.\-]+:\d+>\s") + +# Block openers. Each entry: (format label, compiled opener, compiled closer). +# +# Closers are REGEXES, not literals. Hugo does not require the space before the closing +# delimiter, and real pages use both forms: 588 use `{{< /clients-example >}}` while +# content/commands/lpop.md, content/develop/get-started/data-store.md and +# content/develop/ai/search-and-query/query/exact-match.md use `{{< /clients-example>}}`. +# Matching a literal meant the block never closed on those pages: the scanner ran to end of +# file, so it reported a wrong line range and could absorb later CLI blocks' commands. +BLOCK_PATTERNS = [ + ("redis-cli", re.compile(r"{{%\s*redis-cli\s*%}}"), + re.compile(r"{{%\s*/\s*redis-cli\s*%}}")), + ("highlight", re.compile(r"{{<\s*highlight\b[^>]*>}}"), + re.compile(r"{{<\s*/\s*highlight\s*>}}")), + ("clients-example", re.compile(r"{{<\s*clients-example\b[^>]*(?}}"), + re.compile(r"{{<\s*/\s*clients-example\s*>}}")), +] +SELF_CLOSING = re.compile(r"{{<\s*clients-example\b[^>]*/>}}") + +# An OPENING fence may carry an info string: a language word optionally followed by Hugo +# attributes — ```checklist {id="x"}, ```mermaid {width="80%"}, ```hierarchy {type="..."}. +# 60 such openers exist under content/. An earlier pattern anchored at the language word +# ([\w.+-]*\s*$) matched none of them, so the block's real closing fence was mistaken for an +# opener and every fence afterwards paired one step out of phase. +FENCE_OPEN = re.compile(r"^\s*(`{3,}|~{3,})\s*([\w.+-]*)[ \t]*(.*)$") + +# A CLOSING fence, per CommonMark: same character, at least as long as the opener, and NO +# info string. So a ```bash line *inside* a block is content (or a malformed new opener), +# never a closer — treating it as one would silently truncate any block whose body contains +# fenced markdown, which is common in docs that document markdown. +FENCE_CLOSE = re.compile(r"^\s*(`{3,}|~{3,})[ \t]*$") + +# set=/step= as named params, and the positional form: {{< clients-example id step ... >}} +NAMED_SET = re.compile(r'\bset="([^"]*)"') +NAMED_STEP = re.compile(r'\bstep="([^"]*)"') +POSITIONAL = re.compile(r'{{<\s*clients-example\s+"?([\w.\-]+)"?(?:\s+"?([\w.\-]*)"?)?') + +# Languages that indicate a fenced block may hold CLI content rather than client code. +CLI_FENCE_LANGS = {"", "bash", "sh", "shell", "text", "plaintext", "console", "redis"} + + +def load_clients(): + """Return the ordered list of client display names from clients.tsv.""" + displays = [] + if not os.path.exists(CLIENTS_TSV): + return displays + with open(CLIENTS_TSV, encoding="utf-8") as fh: + for line in fh: + line = line.rstrip("\n") + if not line.strip() or line.startswith("#"): + continue + fields = line.split("\t") + if len(fields) > 1: + displays.append(fields[1]) + return displays + + +def load_examples(): + """Return parsed data/examples.json, or {} if the site hasn't been built.""" + if not os.path.exists(EXAMPLES_JSON): + return {} + try: + with open(EXAMPLES_JSON, encoding="utf-8") as fh: + return json.load(fh) + except (OSError, json.JSONDecodeError): + return {} + + +def commands_in(lines): + """Extract Redis command names from block lines, normalising host:port prompts.""" + normalised = [] + for line in lines: + if HOSTPORT_PROMPT.match(line): + line = "> " + HOSTPORT_PROMPT.sub("", line, count=1) + normalised.append(line.strip()) + return extract_cli_commands("\n".join(normalised)) + + +def parse_shortcode_args(opener): + """Pull (set, step) out of a clients-example opener, named or positional.""" + set_id = NAMED_SET.search(opener) + step = NAMED_STEP.search(opener) + if set_id or step: + return (set_id.group(1) if set_id else "", step.group(1) if step else "") + pos = POSITIONAL.search(opener) + if pos: + return (pos.group(1) or "", pos.group(2) or "") + return ("", "") + + +def scan(path): + """Find every CLI example block in a markdown file.""" + with open(path, encoding="utf-8") as fh: + lines = fh.read().split("\n") + + blocks = [] + i = 0 + while i < len(lines): + line = lines[i] + + # Self-closing clients-example: a reference to an existing example, no inline CLI. + if SELF_CLOSING.search(line): + set_id, step = parse_shortcode_args(line) + blocks.append({ + "format": "clients-example (self-closing)", + "start": i + 1, "end": i + 1, + "set": set_id, "step": step, "commands": [], + }) + i += 1 + continue + + matched = False + for label, opener, closer in BLOCK_PATTERNS: + if not opener.search(line): + continue + set_id, step = parse_shortcode_args(line) if label == "clients-example" else ("", "") + body, j = [], i + 1 + while j < len(lines) and not closer.search(lines[j]): + body.append(lines[j]) + j += 1 + if j >= len(lines): + # Ran off the end: an unbalanced shortcode, or a closer form this scanner + # doesn't recognise. Say so rather than silently reporting a bogus range. + print(f"warning: {path}: unclosed {label} block opened at line {i + 1}", + file=sys.stderr) + blocks.append({ + "format": label, + "start": i + 1, "end": min(j + 1, len(lines)), + "set": set_id, "step": step, "commands": commands_in(body), + }) + i = j + 1 + matched = True + break + if matched: + continue + + # Fenced code block. + fence = FENCE_OPEN.match(line) + if fence: + marker, lang = fence.group(1), fence.group(2).lower() + body, j = [], i + 1 + while j < len(lines): + closing = FENCE_CLOSE.match(lines[j]) + if (closing and closing.group(1)[0] == marker[0] + and len(closing.group(1)) >= len(marker)): + break + body.append(lines[j]) + j += 1 + if j >= len(lines): + # Same failure mode as an unclosed shortcode: the body swallows the rest of + # the file and the reported range is meaningless. Warn here too — a silent + # bogus range is what makes a Phase 0 work plan quietly wrong. + print(f"warning: {path}: unclosed fenced block opened at line {i + 1}", + file=sys.stderr) + if lang in CLI_FENCE_LANGS: + cmds = commands_in(body) + if cmds: + blocks.append({ + "format": f"fenced ({lang or 'no lang'})", + "start": i + 1, "end": min(j + 1, len(lines)), + "set": "", "step": "", "commands": cmds, + }) + i = j + 1 + continue + + i += 1 + return blocks + + +def coverage(examples, displays, set_id, step): + """Return (implemented, missing, other) client display names for a set/step. + + Keys in data/examples.json that aren't client display names are reported as + `other` rather than counted as coverage — a set carries metadata keys such as + "steps_commands" alongside the per-client entries, and an empty `step` would + otherwise tally those as implemented clients. + """ + if not set_id or set_id not in examples: + return ([], list(displays), []) + known = set(displays) + implemented, other = [], [] + for display, data in examples[set_id].items(): + if display not in known: + other.append(display) + continue + if not isinstance(data, dict): + continue + steps = data.get("named_steps") or {} + # An empty step means the set-level example, so presence of the client counts. + if not step or step in steps: + implemented.append(display) + ordered = [d for d in displays if d in implemented] + return (ordered, [d for d in displays if d not in implemented], sorted(other)) + + +def audit(path, examples, displays): + blocks = scan(path) + for block in blocks: + impl, missing, other = coverage(examples, displays, block["set"], block["step"]) + block["implemented"] = impl + block["missing"] = missing + block["unrecognised_keys"] = other + if not block["set"]: + block["status"] = "needs TCE (not wired to a shortcode)" + elif not impl: + block["status"] = "wired, no implementations found" + elif missing: + block["status"] = f"partial ({len(impl)} present, {len(missing)} missing)" + else: + block["status"] = "complete" + return {"file": os.path.relpath(path, REPO_ROOT), "blocks": blocks} + + +def render(report, displays): + rel = report["file"] + blocks = report["blocks"] + print(f"\n## {rel}") + if not blocks: + print(" No CLI example blocks found.") + return + print(f" {len(blocks)} block(s); {len(displays)} clients in clients.tsv\n") + for n, b in enumerate(blocks, 1): + loc = f"L{b['start']}" if b["start"] == b["end"] else f"L{b['start']}-{b['end']}" + ident = f'set="{b["set"]}" step="{b["step"]}"' if b["set"] else "—" + print(f" {n}. {b['format']} {loc}") + print(f" shortcode : {ident}") + print(f" commands : {', '.join(b['commands']) or '—'}") + print(f" status : {b['status']}") + if b["missing"] and b["set"]: + print(f" missing : {', '.join(b['missing'])}") + print() + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("paths", nargs="+", help="markdown file(s) to audit") + ap.add_argument("--json", action="store_true", + help="emit a machine-readable work plan instead of a table") + args = ap.parse_args() + + displays = load_clients() + if not displays: + print(f"warning: no clients read from {CLIENTS_TSV}", file=sys.stderr) + examples = load_examples() + if not examples: + print("warning: data/examples.json missing or unreadable — coverage will read as " + "empty. Run `python3 build/make.py` first.", file=sys.stderr) + + reports = [] + for path in args.paths: + if not os.path.exists(path): + print(f"error: no such file: {path}", file=sys.stderr) + return 2 + reports.append(audit(path, examples, displays)) + + if args.json: + json.dump({"clients": displays, "reports": reports}, sys.stdout, indent=2) + sys.stdout.write("\n") + else: + for report in reports: + render(report, displays) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.codex/skills/claude-review/SKILL.md b/.codex/skills/claude-review/SKILL.md index 1d0c995c19..077158b33e 100644 --- a/.codex/skills/claude-review/SKILL.md +++ b/.codex/skills/claude-review/SKILL.md @@ -14,6 +14,12 @@ Read these before reviewing: - [`references/review-style.md`](references/review-style.md) for output format and severity. - [`references/claude-review-patterns.md`](references/claude-review-patterns.md) for recurring review patterns. +If the change touches tabbed code examples — `local_examples/`, a client repo's doctests, +`data/examples.json`, the client config in `config.toml`, or +`.claude/skills/tce-examples/` — also read +[`references/tce-review-patterns.md`](references/tce-review-patterns.md). Those defects mostly +survive a green test run, so the test result is not the review. + Treat the pattern file as a map of places to inspect, not as evidence. The current diff and current workspace always win. ## Workflow diff --git a/.codex/skills/claude-review/references/tce-review-patterns.md b/.codex/skills/claude-review/references/tce-review-patterns.md new file mode 100644 index 0000000000..a1e5de3b37 --- /dev/null +++ b/.codex/skills/claude-review/references/tce-review-patterns.md @@ -0,0 +1,169 @@ +# TCE review patterns + +Recurring defect classes in tabbed code examples (TCEs) — the multi-language blocks rendered +by the `clients-example` shortcode. Use when the review target touches +`local_examples/`, a client repo's doctests, `data/examples.json`, `config.toml`'s client +config, or `.claude/skills/tce-examples/`. + +**This is a map of where to look, not evidence.** Verify every suspected hit against the +current file before reporting it. The generating agents work in parallel, so a defect you +recall from one file may already be fixed in another. + +## Why a review is needed at all when tests pass + +The harness proves the code *runs*. It cannot prove it is the *right* code. Everything in the +first section below passes a green test run. + +--- + +## Class 1 — Output comments that don't match reality + +**Schema:** `>>> ` comments are extracted and published as the shown output. A test asserting +on a variable says nothing about whether the comment next to it is right. + +Look for: + +- **Type drift.** Redis returns strings; the comment shows an integer. `# >>> 4972` where the + client actually returns `'4972'`. +- **Language-native formatting written from memory rather than observed.** Ruby `inspect` + emits `{"a"=>"b"}`, not `{a: "b"}`. Java `Map.toString()` emits `{a=b}`, no quotes. C# + `HashEntry[]` has no useful `ToString()` at all. +- **Null representation.** `None` / `null` / `nil` / `` / `RedisValue.Null` all differ, + and SE.Redis's null prints as an *empty string* — so `// >>> ` with nothing after it is + indistinguishable from a formatting mistake. +- **Non-deterministic ordering presented as fixed.** `HGETALL` into a hash map, `SMEMBERS`, + or an unsorted `ZRANGE` can reorder between runs. A stable `>>>` comment requires the + example to sort explicitly (`TreeMap`, `sorted()`). +- **Formatting that varies by language runtime version.** An output comment is pinned to + whatever the *authoring* machine printed, which may not be what a reader's runtime prints. + The known live case: **Ruby 3.4 changed `Hash#inspect`** to put spaces around the rocket — + `{"field1" => "Hello"}` where 3.3 and earlier emit `{"field1"=>"Hello"}`. Every Ruby TCE + that inspects a hash carries the no-space form, so on Ruby 3.4+ those comments are one + character off. Flag it when reviewing a *new* Ruby example, but note it is a repo-wide + exposure: fixing one file in isolation makes it inconsistent with its siblings rather than + correct. The same class covers time-sensitive values — an `httl` comment of `[10, 10]` + becomes `[9, 10]` on a slow run, which is why such assertions should be range-based even + when the published comment is exact. + +**Report as:** `output-comment-mismatch`, severity high — the reader copies this and sees +something else. + +## Class 2 — Step-name drift across clients + +**Schema:** the `clients-example` shortcode addresses code by `step=`. Every client in a set +must use the identical step name, spelled identically. + +Look for a step present in most clients and misspelled, pluralised, hyphenated-vs-underscored, +or case-shifted in one. The symptom is a **silently empty tab** for that language: nothing +errors, the reader just gets a blank panel. + +Cross-check against `data/examples.json` → `` → `` → `named_steps`, and against +`{{< clients-example set="…" step="…" >}}` on the page. + +**Report as:** `step-name-drift`, severity high. + +## Class 3 — Dropped pre-existing steps + +**Schema:** three locations hold an example — `local_examples///` (staging, most +complete), the client repo (merged), and `tmp/clients/examples/` (transient). Staging routinely +has steps the client repo does not. + +An agent that regenerates a file from a template, or starts from the client repo copy, silently +deletes the pending steps. Check the diff for **removed** `STEP_START` markers, not just added +ones. A file whose step count went down is the tell. + +**Report as:** `dropped-existing-step`, severity high. + +## Class 4 — Marker placement leaking scaffolding + +**Schema:** content outside `HIDE`/`REMOVE` blocks is published verbatim — **including +comments**. + +Look for: + +- **The sample banner.** Every `assets//sample_*` file opens with a ~18-line header + explaining the markers. It is not inside a `HIDE`/`REMOVE` block (it precedes the + `EXAMPLE:` marker), so an agent that copies the sample wholesale publishes an essay about + markers into the docs page. A real example starts directly with `EXAMPLE:` on line 1. +- Assertions, fixture setup, or `del`/`flushall` cleanup outside a `REMOVE` block. +- Unbalanced markers — a `REMOVE_START` with no `REMOVE_END` swallows the rest of the file, so + the tab renders short or empty. +- Test annotations (`@Test`, `[Fact]`, `#[test]`) left visible. + +**Report as:** `leaked-scaffolding` or `marker-placement`, severity high. + +## Class 5 — C# example in the wrong tab + +**Schema:** `C#-*(NRedisStack)` and `C#-*(SE.Redis)` are fed from the *same* NRedisStack repo +directory, partitioned by a content filter: presence of `using NRedisStack` → NRedisStack +tabs; absence → SE.Redis tabs. `using NRedisStack.Tests` is excluded from that test (every +file has it for fixtures). + +So the deciding factor is one import line, and both flavours compile and pass identically. +An unused or debug-leftover `using NRedisStack` relocates a finished example to the other tab +with no error anywhere. + +Most command-page examples should be SE.Redis-flavoured — plain hash/list/string commands +don't need NRedisStack. Check the import against the intent stated in the PR or the brief. + +**Report as:** `csharp-flavor`, severity high. + +## Class 6 — Signatures not from the API mapping + +**Schema:** `data/command-api-mapping/.json` carries the real per-client signatures, +keyed by `mappingClientId` (see `build/example-test-harness/clients.tsv`). + +Look for a plausible-but-wrong method name, a wrong overload, or a call that only compiles +because the argument happens to be permissive. Also flag the inverse: a mapping entry that +contradicts the example, since the mapping may be the thing that's stale. + +Historic instance: a Jedis snippet that imported `RedisClient` and then called `jedis.hset(...)` +on it — two different APIs spliced together. + +**Report as:** `wrong-api-signature`, severity high if the shown call doesn't exist. + +## Class 7 — Test scaffolding that can't fail + +**Schema:** these files are the test. If the assertions are vacuous, a broken example is green. + +Look for: + +- No assertion at all in the `REMOVE` blocks — just prints. +- Assertions on the wrong variable, or comparing a value to itself. +- A caught-and-swallowed error path (`Err(e) => println!(...)` in Rust, an empty `catch`) that + turns a failure into a pass. +- Missing cleanup before the steps run, so the example only passes on a freshly flushed db — + fine under the harness, misleading for a reader. + +**Report as:** `assertion-weak` / `cleanup-missing`, severity medium unless it hides a real bug. + +## Class 8 — Environment defects that manufacture false greens + +Not defects in an example, but in what tests it. Worth flagging when a PR touches the harness: + +- **Surefire matching zero tests.** The Java classes are `*Example`, not `*Test`, so a + `pom.xml` lacking `**/*Example.java` runs nothing and exits 0. + `fidelity/pom-lettuce-async.xml` and `pom-lettuce-reactive.xml` both shipped without it. +- **A wrapper exiting with the teardown's status.** A `run.sh` ending in + `rm -fr ` exits 0 regardless of the test result. All 11 original wrappers did this. +- **`dotnet test` filtering to nothing** when the `[Fact]` didn't survive outside a `REMOVE` + block. +- **Wrong-case paths.** `cmds_*` sets use `local_examples//NRedisStack/`; `geoindex`, + `search_quickstart`, and `time_series_tutorial` use `nredisstack/`. Both are tracked. On a + case-insensitive filesystem a glob on the wrong spelling succeeds locally and breaks on + Linux CI. + +**Report as:** `other`, severity high — a false green is worse than a red. + +## Class 9 — Shortcode and metadata drift + +- `set=`/`step=` on the page not matching any key in `data/examples.json` → empty tab. +- A `lang_filter` value that isn't an exact `config.toml` display name. Matching is exact, not + substring: `lang_filter="C"` does **not** match `C#-Sync (NRedisStack)`, and + `lang_filter="Node"` does **not** match `Node.js`. +- `buildsUpon` naming a step that isn't on the same page. +- Hand-edited `data/examples.json` — it is generated by `build/make.py`. +- A client added to a set that doesn't actually support the command. An empty or wrong tab is + worse than an absent one. + +**Report as:** `other`, severity medium to high depending on whether a tab breaks. diff --git a/AI_AGENT_DEVELOPER_GUIDE.md b/AI_AGENT_DEVELOPER_GUIDE.md index 661a655d87..17d2d388c7 100644 --- a/AI_AGENT_DEVELOPER_GUIDE.md +++ b/AI_AGENT_DEVELOPER_GUIDE.md @@ -31,11 +31,11 @@ outcomes: code_examples: description: "Adding/modifying code examples" - guidance: "Read [`for-ais-only/tcedocs/README.md`](for-ais-only/tcedocs/README.md) first, especially the sections on [Writing effective descriptions](for-ais-only/tcedocs/README.md#writing-effective-descriptions) and [Choosing difficulty levels](for-ais-only/tcedocs/README.md#choosing-difficulty-levels). Then review examples in [`content/develop/data-types/strings.md`](../content/develop/data-types/strings.md) to see the pattern in action. Once you understand the pattern, proceed with adding or modifying your code examples." + guidance: "For tabbed multi-language examples (the `clients-example` shortcode), use the [`tce-examples`](.claude/skills/tce-examples/SKILL.md) skill — it covers auditing a page for missing client coverage, generating examples across every client, testing them against a live Redis, and reviewing them. Start there rather than working from prose. For background, read [`for-ais-only/tcedocs/README.md`](for-ais-only/tcedocs/README.md), especially [Writing effective descriptions](for-ais-only/tcedocs/README.md#writing-effective-descriptions) and [Choosing difficulty levels](for-ais-only/tcedocs/README.md#choosing-difficulty-levels), then review [`content/develop/data-types/strings.md`](content/develop/data-types/strings.md) to see the pattern in action. Client identity (display names, paths, filename conventions) comes from [`build/example-test-harness/clients.tsv`](build/example-test-harness/clients.tsv)." render_hook: description: "Creating a new render hook" - guidance: "Read [`for-ais-only/render_hook_docs/README.md`](for-ais-only/render_hook_docs/README.md) for an overview, then study [`for-ais-only/render_hook_docs/AI_RENDER_HOOK_LESSONS.md`](for-ais-only/render_hook_docs/AI_RENDER_HOOK_LESSONS.md) (Lessons 1-12). Review existing render hooks in [`layouts/_default/_markup/`](../layouts/_default/_markup/) as examples. Once you understand the patterns, implement your render hook." + guidance: "Read [`for-ais-only/render_hook_docs/README.md`](for-ais-only/render_hook_docs/README.md) for an overview, then study [`for-ais-only/render_hook_docs/AI_RENDER_HOOK_LESSONS.md`](for-ais-only/render_hook_docs/AI_RENDER_HOOK_LESSONS.md) (Lessons 1-12). Review existing render hooks in [`layouts/_default/_markup/`](layouts/_default/_markup/) as examples. Once you understand the patterns, implement your render hook." metadata: description: "Working with page metadata" @@ -43,7 +43,7 @@ outcomes: build_system: description: "Understanding the build system" - guidance: "Read [`for-ais-only/BUILD_SYSTEM_ARCHITECTURE.md`](for-ais-only/BUILD_SYSTEM_ARCHITECTURE.md) for an overview, then check the [`Makefile`](../Makefile) and [`build/make.py`](../build/make.py) for specific implementation details. Once you understand the system, proceed with your build-related task." + guidance: "Read [`for-ais-only/BUILD_SYSTEM_ARCHITECTURE.md`](for-ais-only/BUILD_SYSTEM_ARCHITECTURE.md) for an overview, then check the [`Makefile`](Makefile) and [`build/make.py`](build/make.py) for specific implementation details. Once you understand the system, proceed with your build-related task." general: description: "Other tasks" diff --git a/build/example-test-harness/README.md b/build/example-test-harness/README.md index 0a6e0ea147..c963f55a29 100644 --- a/build/example-test-harness/README.md +++ b/build/example-test-harness/README.md @@ -8,21 +8,50 @@ but reusable for any example set. ## Usage ```bash -./run.sh [client ...] -# all clients: +./run.sh [--portable|--fidelity] [client ...] +# all clients, portable (default): ./run.sh ss_tutorial # one/some: ./run.sh set_tutorial rust-sync dotnet +# resolve source paths only — no Redis, no toolchains: +./run.sh --list cmds_hash ``` -`example_set` covers the data-type tutorials (`ss_tutorial`, `set_tutorial`, -`hash_tutorial`, `sets_tutorial`, `time_series_tutorial`), the search sets -(`search_quickstart`, `geoindex`), and the per-command sets (e.g. `cmds_sorted_set`) — -add more in `src_path()`. Results print as a matrix; per-run logs land in -`results/_.log`. +**Two modes.** `--portable` (default) is the original behaviour: each toolchain is +self-bootstrapped into a cached `work/` dir, no client repo clones needed, with C#/PHP running +against local stubs. `--fidelity` runs in `tmp/clients/examples/` using the tracked manifests in +`fidelity/` and real client repo clones, so an example executes the way it does upstream — run +`./bootstrap.sh` first. Iterate in portable; confirm in fidelity. + +`example_set` no longer needs a code change. Paths resolve by convention from +`local_examples///`, using the directory aliases in `clients.tsv`; the explicit +`legacy_src_path()` case block covers only the older sets whose files live elsewhere +(`local_examples/tmp/datatypes/...`, `ruby/`, `php/`, `client-specific/`). Results print as a +matrix; per-run logs land in `results/_.log`. ⚠️ Several examples call `FLUSHALL`/`FLUSHDB`, and the harness flushes before each run. -Point it only at a scratch Redis. +Point it only at a scratch Redis. Runs are serial for this reason. + +## Compatibility notes + +If you have muscle memory or notes from the pre-`clients.tsv` version: + +- **Old client names still work.** `python`, `node`, `go`, `php`, `dotnet` and the rest are + accepted as aliases and resolve to the same files. `clients.tsv` column `key` holds the new + canonical names (`redis-py`, `node-redis`, `go-redis`, `predis`, `nredisstack`). +- **Log filenames use the canonical key.** `results/cmds_hash_redis-py.log`, not + `..._python.log`. Anything grepping the old path needs updating. +- **A bare `./run.sh ` now attempts 16 clients, not 12** — `lettuce-sync` (which had a POM + and a runner but was missing from `CLIENTS_ALL`) plus the three additional C# rows. Clients + with no source for the set report `SKIP`, so the extra rows are informational. +- **An unknown client name is now a hard error** (exit 2) instead of a mid-run + "command not found". +- **Seven sets became testable** that previously had no entry and were skipped by omission: + `cmds_hash`, `cmds_string`, `cmds_generic`, `cmds_cnxmgmt`, `arrays_tutorial`, + `fastapi_tutorial`, `vecset_tutorial`. `cmds_cnxmgmt` is deliberately skipped via + `illustrative_reason()` — its files contain only `auth1`/`auth2`, which need a `test-user` + ACL identity a scratch Redis has no reason to define. Skipping it explicitly keeps the rule + that **a red result always means a real defect**. ## Clients & how each is run @@ -32,8 +61,8 @@ Point it only at a scratch Redis. | node | `npm i redis`, ESM | run as `.mjs` | | ioredis | `npm i ioredis`, ESM | run as `.mjs` (separate `work/ioredis` dir from node-redis) | | 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 | +| jedis | Maven + `jedis:7.5.3` | surefire include `**/*Example.java` (classes aren't `*Test`) | +| lettuce-sync / -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]` | @@ -43,14 +72,64 @@ Point it only at a scratch Redis. ## 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. + to compile `flushall` in a REMOVE block. Portable: Jedis 7.5.3, Lettuce 6.5.5, + StackExchange.Redis 3.0.0. - **Surefire only runs `*Test`/`*Tests` by default** — these classes are `*Example`, so the POMs add an explicit ``. First run looked green with **zero tests** without it. + `fidelity/pom-lettuce-async.xml` and `pom-lettuce-reactive.xml` shipped without it and were + silently reporting PASS having executed nothing. The generated fidelity Java wrapper now fails + unless it observes `Tests run: [1-9]`, and the C# wrapper fails on `No test matches`. +- **A wrapper must not end with its teardown.** Every original `tmp/clients/examples/*/run.sh` + ended with `rm -fr `, so the script's exit status was the `rm`'s — always 0 — and a + failing example reported success. Generated wrappers capture `rc` before teardown and + `exit $rc`. +- **Never stage into a real clone's source tree.** For the C# clients `fid_sub` is + `tests/Doc` *inside the NRedisStack clone*, and the staged filename is identical to the + upstream one (`CmdsHashExample.cs` is both). A naive stage-then-delete overwrote a tracked + upstream file and then removed it, leaving the clone with a deleted source. `run_fidelity` + now backs up anything it is about to clobber and restores it afterwards, so a fidelity run + is a no-op on the clone. +- **Both modes need the zero-test guards, not just fidelity.** Portable is the *default* + mode, so a guard present only in the generated fidelity wrappers leaves the false green + exactly where it is most likely to be hit. `run_maven_java` and `run_dotnet` now fail + unless they observe `Tests run: [1-9]` / `Passed: [1-9]`. +- **`Doc.csproj` multi-targets `net481`, which needs a mono host.** On a plain macOS/Linux box + the net8.0 and net10.0 legs pass, then net481 aborts and takes the run's exit code with it — + a FAIL that says nothing about the example. The generated C# wrapper now pins the lowest + modern TFM the project declares. +- **The C# project directory can't be hardcoded.** Async examples stage under + `tests/Doc/Async`; a wrapper fixed to `tests/Doc` would silently exercise the sync tree. + `run_fidelity` passes the staged subdirectory through as a second argument. - 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/`. +Usually nothing to do. If the files live at `local_examples///`, resolution finds +them by convention — check with `./run.sh --list `. + +You only need to touch `run.sh` when: + +- the files live somewhere non-conforming → add lines to `legacy_src_path()`; +- a client directory uses a new spelling → add it to that client's `local_dirs` aliases in + `clients.tsv` (pipe-separated). Match the on-disk case exactly: `cmds_*` sets use + `NRedisStack/` while `geoindex` and friends use `nredisstack/`, and a case-insensitive + filesystem will happily resolve the wrong one into a path that doesn't exist in git; +- the set cannot run against a scratch Redis by design → add it to `illustrative_reason()` + so it reports `SKIP` with a reason instead of `FAIL`. + +## Fidelity mode dependency versions + +`fidelity/` holds the tracked manifests. They intentionally differ from the portable POMs: + +| | portable | fidelity | +|---|---|---| +| jedis | 7.5.3 | 7.4.0 | +| lettuce-core | 6.5.5.RELEASE | 7.4.0.RELEASE | + +The fidelity pins are what the current `local_examples/` Java files were actually tested +against; the portable jedis 7.5.3 bump was deliberate (the search examples need the +`RedisClient` API it introduced). **Not yet reconciled** — a Java example can pass in one mode +and fail in the other. Settling on one version per client means running both Java toolchains +against `search_quickstart`, `geoindex`, and the Java-heavy command sets. diff --git a/build/example-test-harness/bootstrap.sh b/build/example-test-harness/bootstrap.sh new file mode 100755 index 0000000000..519ae745b7 --- /dev/null +++ b/build/example-test-harness/bootstrap.sh @@ -0,0 +1,314 @@ +#!/usr/bin/env bash +# Materialise the fidelity test environment at tmp/clients/examples/. +# +# ./bootstrap.sh # scaffold all clients; clone/update client repos +# ./bootstrap.sh --no-clone # scaffold only (skip git clone/fetch) +# ./bootstrap.sh --check # report what's missing, change nothing +# +# Fidelity mode runs each example the way the client repo runs it: real dependency +# manifests (tracked in fidelity/), real toolchains. That environment used to exist only +# as a zip passed around by hand — this script replaces it, so the setup is reviewable in +# a PR and reproducible on a new machine. +# +# The per-client run.sh wrappers are GENERATED here rather than tracked. They are one to +# three lines each and differed only in the command; committing thirteen near-identical +# wrappers (with three different argument conventions between them) was the thing that +# made the old environment hard to drive. Client identity comes from clients.tsv. +# +# Idempotent: safe to re-run. Keep bash 3.2 compatible (macOS system bash). +set -uo pipefail + +HARNESS="$(cd "$(dirname "$0")" && pwd)" +REPO="$(cd "$HARNESS/../.." && pwd)" +TSV="$HARNESS/clients.tsv" +FID="$HARNESS/fidelity" +DEST="$REPO/tmp/clients/examples" +CLONES="$REPO/tmp/clients" + +CLONE=1; CHECK=0 +while [ $# -gt 0 ]; do + case "$1" in + --no-clone) CLONE=0; shift ;; + --check) CHECK=1; CLONE=0; shift ;; + -h|--help) sed -n '2,9p' "$0"; exit 0 ;; + *) echo "unknown flag: $1" >&2; exit 2 ;; + esac +done + +[ -f "$TSV" ] || { echo "ERROR: missing $TSV" >&2; exit 1; } +say() { printf '%s\n' "$*"; } +warn() { printf 'WARN: %s\n' "$*" >&2; } + +# Repos that must be cloned for fidelity testing. NRedisStack is special: it is a full +# clone, not scaffolding — the C# examples build against that repo's own Doc.csproj, +# which project-references NRedisStack.csproj and NRedisStack.Tests.csproj. +CLONE_REPOS=" +NRedisStack|https://github.com/redis/NRedisStack +lettuce|https://github.com/redis/lettuce +redis-rb|https://github.com/redis/redis-rb +" + +# client-key|source file in fidelity/|destination filename +MANIFESTS=" +redis-py|requirements-redis-py.txt|requirements.txt +node-redis|package-node-redis.json|package.json +ioredis|package-ioredis.json|package.json +go-redis|go-redis.mod|go.mod +jedis|pom-jedis.xml|pom.xml +lettuce-sync|pom-lettuce-sync.xml|pom.xml +lettuce-async|pom-lettuce-async.xml|pom.xml +lettuce-reactive|pom-lettuce-reactive.xml|pom.xml +predis|composer-predis.json|composer.json +ruby|Gemfile-ruby|Gemfile +rust-sync|Cargo-rust-sync.toml|Cargo.toml +rust-async|Cargo-rust-async.toml|Cargo.toml +hiredis|| +" + +tsv_get() { awk -F'\t' -v k="$1" -v f="$2" '!/^#/ && NF>1 && $1==k {print $f; exit}' "$TSV"; } + +# Emit the run.sh for one client. $1 = client key. +# Each wrapper takes the staged file's basename as $1 so the driver can treat every +# client identically, then tears down its dependency cache — matching the behaviour of +# the hand-built environment this replaces (every run installs from scratch). +emit_runner() { + case "$1" in + redis-py) cat <<'EOF' +#!/bin/bash +# usage: ./run.sh +set -uo pipefail +python3 -m venv venv +./venv/bin/pip -q install -r requirements.txt +./venv/bin/python "$1"; rc=$? +rm -fr venv +exit $rc +EOF +;; + node-redis|ioredis) cat <<'EOF' +#!/bin/bash +# usage: ./run.sh +set -uo pipefail +npm install --silent +node "$1"; rc=$? +rm -fr node_modules package-lock.json +exit $rc +EOF +;; + go-redis) cat <<'EOF' +#!/bin/bash +# usage: ./run.sh +set -uo pipefail +# The examples declare `package example_commands`, so the module needs a second file in +# that package or the build fails before any test runs. +[ -f lib.go ] || printf 'package example_commands\n' > lib.go +go mod tidy >/dev/null 2>&1 +go test -v ./...; rc=$? +rm -f go.sum +exit $rc +EOF +;; + jedis|lettuce-sync|lettuce-async|lettuce-reactive) cat <<'EOF' +#!/bin/bash +# usage: ./run.sh +set -uo pipefail +out="$(mvn -B test 2>&1)"; rc=$? +printf '%s\n' "$out" +# Surefire exits 0 when it matches no tests. These classes are named *Example, not +# *Test, so a pom missing the *Example include silently "passes" having run nothing. +# Treat a zero/absent test count as failure rather than a green. +if ! printf '%s' "$out" | grep -qE 'Tests run: [1-9]'; then + echo "HARNESS ERROR: surefire ran zero tests — check the *Example include in pom.xml" + rc=1 +fi +rm -fr target +exit $rc +EOF +;; + predis) cat <<'EOF' +#!/bin/bash +# usage: ./run.sh +set -uo pipefail +composer install -q +./vendor/bin/phpunit "$1"; rc=$? +rm -fr composer.lock vendor +exit $rc +EOF +;; + ruby) cat <<'EOF' +#!/bin/bash +# usage: ./run.sh +set -uo pipefail +bundle install --quiet --path .gems +bundle exec ruby "$1"; rc=$? +rm -fr .gems Gemfile.lock .bundle +exit $rc +EOF +;; + rust-sync|rust-async) cat <<'EOF' +#!/bin/bash +# usage: ./run.sh (staged into tests/) +# Cargo builds tests/*.rs as an integration-test target, and compiles it with --test so the +# file's `#[cfg(test)] mod` is enabled. No src/ target is needed for that to work. +set -uo pipefail +out="$(cargo test -- --nocapture 2>&1)"; rc=$? +printf '%s\n' "$out" +# Same false-green class as surefire and dotnet: cargo exits 0 when it runs no tests at all +# (e.g. the #[cfg(test)] mod was renamed, or the file landed where cargo doesn't scan). +# Require at least one binary to report a non-zero passed count. +if [ "$rc" -eq 0 ] && ! printf '%s' "$out" | grep -qE 'test result: ok\. [1-9][0-9]* passed'; then + echo "HARNESS ERROR: cargo ran zero tests — check the #[cfg(test)] mod and #[test]/#[tokio::test] markers" + rc=1 +fi +rm -fr Cargo.lock target +exit $rc +EOF +;; + hiredis) cat <<'EOF' +#!/bin/bash +# usage: ./run.sh +set -uo pipefail +bin="${1%.c}" +cc "$1" -I/usr/local/include -I/opt/homebrew/include \ + -L/usr/local/lib -L/opt/homebrew/lib -lhiredis -o "$bin" || exit 1 +"./$bin"; rc=$? +rm -f "$bin" +exit $rc +EOF +;; + nredisstack|seredis|nredisstack-async|seredis-async) cat <<'EOF' +#!/bin/bash +# usage: ./run.sh [project_dir] +# Runs inside the NRedisStack clone: the Doc project builds against that repo's own +# Doc.csproj, so both C# flavours (NRedisStack-importing and plain SE.Redis) execute +# with the real fixtures rather than stubs. +set -uo pipefail +cls="$(basename "$1" .cs)" +# The async C# examples are staged under tests/Doc/Async, so the project directory can't +# be hardcoded to tests/Doc or async runs would silently exercise the sync tree instead. +proj="${2:-tests/Doc}" +# Doc.csproj multi-targets net8.0;net10.0;net481. The net481 leg needs a mono host, which +# isn't present on a plain macOS/Linux dev box: the other legs pass, then net481 aborts and +# takes the whole run's exit code with it. Pin to a modern TFM the project actually declares, +# preferring the lowest so behaviour matches the oldest supported runtime. +tfm="" +for cand in net8.0 net10.0; do + if grep -q "$cand" "$proj"/*.csproj 2>/dev/null; then tfm="$cand"; break; fi +done +[ -n "$tfm" ] && set -- --framework "$tfm" || set -- +out="$(dotnet test "$proj" --nologo "$@" --filter "FullyQualifiedName~$cls" 2>&1)"; rc=$? +printf '%s\n' "$out" +if printf '%s' "$out" | grep -qE 'No test (matches|is available)'; then + echo "HARNESS ERROR: no test matched $cls — check the [Fact] survived outside a REMOVE block" + rc=1 +fi +# A negative check alone is not enough: dotnet test can exit 0 having executed nothing +# without ever printing "No test matches". Require a positive passing count. +# +# Two summary shapes exist and Doc.csproj could produce either: the classic VSTest line +# ("Passed! - Failed: 0, Passed: 2, ...") and Microsoft.Testing.Platform's ("succeeded: 2" +# / "total: 2"), which xunit.v3 uses when built against MTP. Accept both, and if NEITHER is +# recognised, fail loudly rather than guessing — an unparsed summary is exactly the state +# where a silent pass would be least justified. +if [ "$rc" -eq 0 ]; then + if printf '%s' "$out" | grep -qE 'Passed! *- *Failed: *[0-9]+, *Passed: *[1-9]'; then + : # VSTest summary, at least one test passed + elif printf '%s' "$out" | grep -qE '(succeeded|passed): *[1-9]'; then + : # Microsoft.Testing.Platform summary + else + echo "HARNESS ERROR: could not confirm any test passed for $cls." + echo " Neither a VSTest nor an MTP summary with a non-zero pass count was found." + echo " If dotnet's summary format changed, update this check in bootstrap.sh." + rc=1 + fi +fi +exit $rc +EOF +;; + *) return 1 ;; + esac +} + +# --- clone / update client repos --------------------------------------------- +if [ "$CLONE" = 1 ]; then + mkdir -p "$CLONES" + for entry in $CLONE_REPOS; do + name="${entry%%|*}"; url="${entry##*|}" + if [ -d "$CLONES/$name/.git" ]; then + say ">> updating $name" + ( cd "$CLONES/$name" && git fetch --quiet --depth 1 origin ) || warn "fetch failed for $name" + else + say ">> cloning $name" + git clone --quiet --depth 1 "$url" "$CLONES/$name" || warn "clone failed for $name" + fi + done +fi + +# --- scaffold each client ---------------------------------------------------- +missing=0 +for entry in $MANIFESTS; do + client="$(echo "$entry" | cut -d'|' -f1)" + src="$(echo "$entry" | cut -d'|' -f2)" + dst="$(echo "$entry" | cut -d'|' -f3)" + dir="$(tsv_get "$client" 9)" + sub="$(tsv_get "$client" 10)" + [ -n "$dir" ] && [ "$dir" != "-" ] || { warn "$client has no fid_dir in clients.tsv"; continue; } + target="$DEST/$dir" + + if [ "$CHECK" = 1 ]; then + if [ -x "$target/run.sh" ]; then say "ok $client -> $target" + else say "MISSING $client -> $target"; missing=$((missing+1)); fi + continue + fi + + mkdir -p "$target" + [ "$sub" = "." ] || mkdir -p "$target/$sub" + if [ -n "$src" ]; then + [ -f "$FID/$src" ] || { warn "missing fidelity/$src"; continue; } + cp "$FID/$src" "$target/$dst" + fi + if emit_runner "$client" > "$target/run.sh"; then + chmod +x "$target/run.sh" + say "scaffolded $client -> $target" + else + rm -f "$target/run.sh"; warn "no runner defined for $client" + fi +done + +# The C# clients share the NRedisStack clone; give it the runner too. +if [ "$CHECK" = 0 ] && [ -d "$DEST/NRedisStack" ]; then + emit_runner nredisstack > "$DEST/NRedisStack/run.sh" && chmod +x "$DEST/NRedisStack/run.sh" + say "scaffolded nredisstack/seredis -> $DEST/NRedisStack" +elif [ "$CHECK" = 0 ]; then + # Fidelity C# needs the full repo, not scaffolding: link the clone into place. + if [ -d "$CLONES/NRedisStack" ]; then + ln -sfn "$CLONES/NRedisStack" "$DEST/NRedisStack" + emit_runner nredisstack > "$CLONES/NRedisStack/run.sh" && chmod +x "$CLONES/NRedisStack/run.sh" + say "linked NRedisStack clone -> $DEST/NRedisStack" + else + warn "no NRedisStack clone; C# fidelity testing unavailable (re-run without --no-clone)" + fi +fi + +# --- toolchain report -------------------------------------------------------- +say "" +say "=== toolchains ===" +# redis-server is listed separately from redis-cli on purpose: the harness needs the CLI +# to ping and flush, but nothing runs without a server to point it at, and having only +# the CLI installed is an easy state to end up in. +for t in python3 node npm go mvn cargo php composer ruby bundle dotnet redis-cli redis-server; do + if command -v "$t" >/dev/null 2>&1; then printf ' %-10s ok\n' "$t" + else printf ' %-10s MISSING\n' "$t"; fi +done +if ! (echo '#include ' | cc -fsyntax-only -I/usr/local/include -I/opt/homebrew/include -xc - 2>/dev/null); then + printf ' %-10s MISSING (brew install hiredis) — C examples cannot be tested\n' "hiredis" +else + printf ' %-10s ok\n' "hiredis" +fi + +if [ "$CHECK" = 1 ]; then + say ""; say "$missing client(s) not scaffolded" + [ "$missing" -eq 0 ] || exit 1 +fi +say "" +say "Next: start a scratch Redis, then" +say " build/example-test-harness/run.sh --fidelity " diff --git a/build/example-test-harness/clients.tsv b/build/example-test-harness/clients.tsv new file mode 100644 index 0000000000..939ae8a8c5 --- /dev/null +++ b/build/example-test-harness/clients.tsv @@ -0,0 +1,64 @@ +# TCE client table — the single source of truth for client identity in this repo. +# +# Read it: column -t -s$'\t' build/example-test-harness/clients.tsv +# Parse it: skip blank lines and lines starting with '#'; fields are tab-separated. +# +# Before this file existed the same mapping was duplicated across five tables in +# .agents/skills/generate-tce-examples/SKILL.md, and they had drifted out of sync with +# config.toml, data/components/, and the files actually on disk. Every consumer now reads +# this one file: run.sh, bootstrap.sh, and .claude/skills/tce-examples/. +# +# COLUMNS +# key Canonical client key. What you pass to run.sh and name in a fan-out brief. +# display config.toml [params] clientsExamples entry. Exact match; this is the tab label +# and what clients-example lang_filter matches against. +# component data/components/.json, registered in that dir's index.json. +# NB: index.json registers by FILENAME. go_redis.json and redis_vl.json declare +# internal ids ("go-redis", "redisvl") that differ from their filename — do not +# "fix" one to match the other without checking build/components/component.py. +# mapping mappingClientId, the key into data/command-api-mapping/.json. +# Both SE.Redis rows deliberately reuse the NRedisStack ids (see config.toml). +# "-" means the command-API mapping has no entry for this client. +# local_dirs Accepted subdirectory names under local_examples//. Pipe-separated, +# preferred name first. These are aliases for ONE client, not variants: the +# naming grew organically, so cmds_* sets use NRedisStack/ while geoindex, +# search_quickstart, and time_series_tutorial use nredisstack/. Both are +# tracked in git. Globbing on a single name silently misses examples. +# +# DO NOT add NRedisStack to the seredis row's aliases. Some SE.Redis-flavoured +# files live under a directory named for the other client (e.g. +# local_examples/cmds_hash/NRedisStack/CmdsHashExample.cs imports only +# StackExchange.Redis, so it feeds the SE.Redis tab). Which tab a .cs file +# feeds is decided by its imports, never by its path — see data/components/ +# seredis_sync.json. Aliasing the directory to both rows would make nredisstack +# and seredis resolve the SAME file, so one .cs would be reported as two +# passing clients and one file's result would be credited to a tab it never +# fed. The file is tested once, under nredisstack; that is intended. +# assets .claude/skills/tce-examples/assets// — patterns file + working sample. +# filename Filename convention. {set} = set id snake_case (cmds_hash), {set-} = kebab +# (cmds-hash), {Set} = PascalCase (CmdsHash). +# repo_path Path within the client repo where merged examples live (the source of truth +# the build fetches from). "-" = this client has no upstream doctests yet. +# fid_dir Directory under tmp/clients/examples/ for fidelity-mode testing. +# fid_sub Path within fid_dir where the staged file goes. "." = directory root. +# portable Runner key in run.sh --portable. "-" = no portable runner; fidelity only. +# +# key display component mapping local_dirs assets filename repo_path fid_dir fid_sub portable +redis-py Python redis_py redis_py redis-py redis-py {set}.py doctests redis-py . python +node-redis Node.js node_redis node_redis node-redis|nodejs node-redis {set-}.js doctests node-redis . node +ioredis ioredis ioredis ioredis ioredis ioredis {set-}.js examples ioredis . ioredis +jedis Java-Sync jedis jedis jedis jedis {Set}Example.java src/test/java/io/redis/examples jedis src/test/java/io/redis/examples jedis +lettuce-sync Lettuce-Sync lettuce_sync lettuce_sync lettuce-sync lettuce-sync {Set}Example.java src/test/java/io/redis/examples/sync lettuce-sync src/test/java/io/redis/examples/sync lettuce-sync +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 . - +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 +seredis-async C#-Async (SE.Redis) seredis_async nredisstack_async - seredis {Set}Example.cs tests/Doc/Async NRedisStack tests/Doc/Async dotnet +redis-vl RedisVL redis_vl redis_vl - - {set}.py doctests - - - +predis PHP php php predis|php predis {Set}Test.php examples predis . php +ruby Ruby redis_rb redis_rb ruby ruby {set}.rb examples ruby . ruby +rust-sync Rust-Sync redis_rs_sync redis_rs_sync rust-sync rust-sync {set}.rs redis/examples rust-sync tests rust-sync +rust-async Rust-Async redis_rs_async redis_rs_async rust-async rust-async {set}.rs redis/examples rust-async tests rust-async diff --git a/build/example-test-harness/fidelity/Cargo-rust-async.toml b/build/example-test-harness/fidelity/Cargo-rust-async.toml new file mode 100644 index 0000000000..e7bac162e9 --- /dev/null +++ b/build/example-test-harness/fidelity/Cargo-rust-async.toml @@ -0,0 +1,13 @@ +[package] +name = "redis_examples_async" +version = "0.1.0" +edition = "2021" + +[dependencies] +redis = { version = "1.0", features = ["tokio-comp"] } +tokio = { version = "1", features = ["full"] } +futures-util = "0.3" + +[dev-dependencies] +# Tests use the same dependencies + diff --git a/build/example-test-harness/fidelity/Cargo-rust-sync.toml b/build/example-test-harness/fidelity/Cargo-rust-sync.toml new file mode 100644 index 0000000000..4c6c40b83f --- /dev/null +++ b/build/example-test-harness/fidelity/Cargo-rust-sync.toml @@ -0,0 +1,11 @@ +[package] +name = "redis_examples" +version = "0.1.0" +edition = "2021" + +[dependencies] +redis = "1.0" + +[dev-dependencies] +# Tests use the same redis dependency + diff --git a/build/example-test-harness/fidelity/Gemfile-ruby b/build/example-test-harness/fidelity/Gemfile-ruby new file mode 100644 index 0000000000..17e52a308f --- /dev/null +++ b/build/example-test-harness/fidelity/Gemfile-ruby @@ -0,0 +1,14 @@ +source "https://rubygems.org" + +# Ruby TCE examples use the redis-rb client and assert with plain `raise` / +# a locally-defined assert_equal, so no test framework is needed. +# +# Version is chosen from the running Ruby, because the two constraints conflict: +# * Hash-field TTL commands (HEXPIRE/HTTL, redis-rb `hexpire`/`httl`) landed in +# redis-rb 6.0.0 — they do NOT exist in 5.4.1. +# * redis-rb 6.x requires Ruby >= 3.2, but macOS still ships system Ruby 2.6. +# On a modern Ruby we test what the docs actually target; on an old system Ruby we +# still test every step that 5.4.1 supports rather than failing to install at all. +# A box on Ruby < 3.2 cannot exercise the hexpire step — that is a toolchain gap, +# not an example defect, so the harness reports it as a SKIP. +gem "redis", (RUBY_VERSION >= "3.2" ? "~> 6.0" : "~> 5.4") diff --git a/build/example-test-harness/fidelity/composer-predis.json b/build/example-test-harness/fidelity/composer-predis.json new file mode 100644 index 0000000000..e25fac6b22 --- /dev/null +++ b/build/example-test-harness/fidelity/composer-predis.json @@ -0,0 +1,23 @@ +{ + "name": "redis/predis-examples", + "description": "Predis example tests for Redis documentation", + "type": "project", + "require": { + "php": ">=8.1", + "predis/predis": "^3.4.2" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "autoload": { + "psr-4": { + "Redis\\Examples\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Redis\\Examples\\Tests\\": "tests/" + } + } +} + diff --git a/build/example-test-harness/fidelity/go-redis.mod b/build/example-test-harness/fidelity/go-redis.mod new file mode 100644 index 0000000000..bbc7c2ebe4 --- /dev/null +++ b/build/example-test-harness/fidelity/go-redis.mod @@ -0,0 +1,11 @@ +module redis_examples + +go 1.21 + +require github.com/redis/go-redis/v9 v9.18.0 + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + go.uber.org/atomic v1.11.0 // indirect +) diff --git a/build/example-test-harness/fidelity/package-ioredis.json b/build/example-test-harness/fidelity/package-ioredis.json new file mode 100644 index 0000000000..62219e2f74 --- /dev/null +++ b/build/example-test-harness/fidelity/package-ioredis.json @@ -0,0 +1,11 @@ +{ + "name": "ioredis-examples", + "version": "1.0.0", + "type": "module", + "scripts": { + "test": "node sample_test.js" + }, + "dependencies": { + "ioredis": "^5.4.0" + } +} diff --git a/build/example-test-harness/fidelity/package-node-redis.json b/build/example-test-harness/fidelity/package-node-redis.json new file mode 100644 index 0000000000..4f04414224 --- /dev/null +++ b/build/example-test-harness/fidelity/package-node-redis.json @@ -0,0 +1,12 @@ +{ + "name": "node-redis-examples", + "version": "1.0.0", + "type": "module", + "scripts": { + "test": "node sample_test.js" + }, + "dependencies": { + "redis": "^5.11.0" + } +} + diff --git a/build/example-test-harness/fidelity/pom-jedis.xml b/build/example-test-harness/fidelity/pom-jedis.xml new file mode 100644 index 0000000000..b6c867193c --- /dev/null +++ b/build/example-test-harness/fidelity/pom-jedis.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + io.redis.examples + jedis-examples + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + + redis.clients + jedis + 7.4.0 + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + **/*Example.java + **/*Test.java + + + + + + diff --git a/build/example-test-harness/fidelity/pom-lettuce-async.xml b/build/example-test-harness/fidelity/pom-lettuce-async.xml new file mode 100644 index 0000000000..1567f2d5da --- /dev/null +++ b/build/example-test-harness/fidelity/pom-lettuce-async.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + io.redis.examples + lettuce-async-examples + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + + io.lettuce + lettuce-core + 7.4.0.RELEASE + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + org.assertj + assertj-core + 3.24.0 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + **/*Example.java + + + + + + + diff --git a/build/example-test-harness/fidelity/pom-lettuce-reactive.xml b/build/example-test-harness/fidelity/pom-lettuce-reactive.xml new file mode 100644 index 0000000000..4867ac6afa --- /dev/null +++ b/build/example-test-harness/fidelity/pom-lettuce-reactive.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + io.redis.examples + lettuce-reactive-examples + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + + io.lettuce + lettuce-core + 7.4.0.RELEASE + + + io.projectreactor + reactor-core + 3.5.0 + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + org.assertj + assertj-core + 3.24.0 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + **/*Example.java + + + + + + + diff --git a/build/example-test-harness/fidelity/pom-lettuce-sync.xml b/build/example-test-harness/fidelity/pom-lettuce-sync.xml new file mode 100644 index 0000000000..12789271ae --- /dev/null +++ b/build/example-test-harness/fidelity/pom-lettuce-sync.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + io.redis.examples + lettuce-sync-examples + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + + io.lettuce + lettuce-core + 7.4.0.RELEASE + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + org.assertj + assertj-core + 3.24.0 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + **/*Example.java + + + + + + + diff --git a/build/example-test-harness/fidelity/requirements-redis-py.txt b/build/example-test-harness/fidelity/requirements-redis-py.txt new file mode 100644 index 0000000000..9dcaa29648 --- /dev/null +++ b/build/example-test-harness/fidelity/requirements-redis-py.txt @@ -0,0 +1,2 @@ +# Redis Python client +redis>=7.4.0 diff --git a/build/example-test-harness/run.sh b/build/example-test-harness/run.sh index ffdd6718dd..c89cbae023 100755 --- a/build/example-test-harness/run.sh +++ b/build/example-test-harness/run.sh @@ -3,24 +3,204 @@ # Runs a docs example set's client source files against a live (throwaway) Redis # on localhost:6379, using each library's real in-file assertions. # -# Usage: ./run.sh [client ...] -# example_set : ss_tutorial | set_tutorial (add more in src_path()) -# client : one or more of the CLIENTS below; default = all +# Usage: ./run.sh [--portable|--fidelity] [client ...] +# --portable (default) self-bootstrap each toolchain into work/. No client repo +# clones needed; C#/PHP run against local stubs. Cached across runs. +# --fidelity run in tmp/clients/examples/, using the tracked manifests in +# fidelity/ and real client repo clones, so examples execute the way +# they do upstream. Requires ./bootstrap.sh first. +# example_set : e.g. cmds_hash, ss_tutorial, search_quickstart +# client : one or more client keys (see clients.tsv); default = all +# +# Client identity — names, aliases, paths, filename conventions — comes from +# clients.tsv. Sets that follow the local_examples/// convention need +# no code change here; only the older non-conforming sets are listed in src_path(). # # Assumes a SCRATCH Redis on 6379 — several examples FLUSH the db. +# Keep this bash 3.2 compatible (macOS system bash): no associative arrays. set -uo pipefail REPO="$(cd "$(dirname "$0")/../.." && pwd)" HARNESS="$(cd "$(dirname "$0")" && pwd)" WORK="$HARNESS/work"; mkdir -p "$WORK" -SET="${1:?usage: run.sh [client...]}"; shift || true +TSV="$HARNESS/clients.tsv" +FIDELITY_ROOT="$REPO/tmp/clients/examples" + +MODE=portable +LIST=0 +while [ $# -gt 0 ]; do + case "$1" in + --portable) MODE=portable; shift ;; + --fidelity) MODE=fidelity; shift ;; + --list) LIST=1; shift ;; + -h|--help) sed -n '2,18p' "$0"; exit 0 ;; + --*) echo "unknown flag: $1" >&2; exit 2 ;; + *) break ;; + esac +done +SET="${1:?usage: run.sh [--portable|--fidelity] [client...]}"; shift || true + +[ -f "$TSV" ] || { echo "ERROR: missing $TSV" >&2; exit 1; } + +# --- clients.tsv accessors ---------------------------------------------------- +# field numbers: 1 key 2 display 3 component 4 mapping 5 local_dirs 6 assets +# 7 filename 8 repo_path 9 fid_dir 10 fid_sub 11 portable +tsv_get() { # tsv_get + awk -F'\t' -v k="$1" -v f="$2" '!/^#/ && NF>1 && $1==k {print $f; exit}' "$TSV" +} +# Accept either a canonical key or a legacy portable runner name on the command line. +canon_key() { + local k="$1" + if awk -F'\t' -v k="$k" '!/^#/ && NF>1 && $1==k {found=1} END{exit !found}' "$TSV"; then + printf '%s' "$k"; return + fi + awk -F'\t' -v k="$k" '!/^#/ && NF>1 && $11==k {print $1; exit}' "$TSV" +} +# Clients testable in the current mode, in clients.tsv (i.e. docs tab) order. +clients_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=(python node ioredis go jedis ruby rust-sync rust-async lettuce-async lettuce-reactive php dotnet) -CLIENTS=("$@"); [ ${#CLIENTS[@]} -eq 0 ] && CLIENTS=("${CLIENTS_ALL[@]}") +CLIENTS_ALL=() +while IFS= read -r c; do [ -n "$c" ] && CLIENTS_ALL+=("$c"); done < <(clients_for_mode) +CLIENTS=() +if [ $# -eq 0 ]; then + CLIENTS=("${CLIENTS_ALL[@]}") +else + for arg in "$@"; do + ck="$(canon_key "$arg")" + if [ -z "$ck" ]; then echo "ERROR: unknown client '$arg' (see clients.tsv)" >&2; exit 2; fi + CLIENTS+=("$ck") + done +fi # --- example_set + client -> repo-relative source path ----------------------- +# Two-step resolution: +# 1. legacy_src_path() — explicit entries for the older sets whose files do NOT live +# at local_examples/// (the data-type tutorials point into +# local_examples/tmp/datatypes/..., ruby/, php/, client-specific/, and so on). +# Explicit wins, so existing sets resolve exactly as they did before. +# 2. convention — glob local_examples/// using the local_dirs aliases +# from clients.tsv. Any set that follows the convention needs no entry here. src_path() { + local set="$1" client="$2" portable legacy rel + # Convention first for anything that has a per-set directory: it is the only + # resolution that can tell the C# flavours apart (NRedisStack/ vs seredis/). + rel="$(convention_src_path "$set" "$client")" + if [ -n "$rel" ]; then printf '%s' "$rel"; return; fi + # Legacy entries are keyed by portable runner name, which predates the four-way C# + # split — four rows share the "dotnet" runner. Only the primary row for a runner may + # claim its legacy entry, or one .cs file would be reported as four passing clients + # and an NRedisStack-flavoured file would be credited to the SE.Redis tab. + portable="$(tsv_get "$client" 11)" + [ -z "$portable" ] || [ "$portable" = "-" ] && return + [ "$client" = "$(primary_for_portable "$portable")" ] || return + legacy="$(legacy_src_path "$set" "$portable")" + [ -n "$legacy" ] && [ -f "$REPO/$legacy" ] && printf '%s' "$legacy" +} + +# First clients.tsv row using a given portable runner key. +primary_for_portable() { + awk -F'\t' -v p="$1" '!/^#/ && NF>1 && $11==p {print $1; exit}' "$TSV" +} + +# Sets that are illustrative BY DESIGN: the code is correct for a reader but cannot execute +# against a scratch Redis. Reported as SKIP with a reason rather than FAIL, so that a red +# result always means a real defect. +# +# This list exists because path resolution is now convention-based. Under the old hardcoded +# src_path() these sets simply had no entry, so they were skipped by omission — the intent was +# invisible and easy to lose. Stating it explicitly is the point. +# Capability gaps in the LOCAL toolchain, as opposed to defects in the example. An example +# that is correct for the version the docs target, but unrunnable with what is installed +# here, must report SKIP with a reason — reporting FAIL would break the rule that a red +# result always means a real defect. +toolchain_skip_reason() { # $1 = set, $2 = canonical client key, $3 = repo-relative source + case "$2" in + go-redis) + # Portable mode copies the pinned fidelity/go-redis.mod, which carries a `go` directive. + # If the installed toolchain is older, `go test` dies with "toolchain not available" + # before the example runs at all — a FAIL that says nothing about the example. (This is + # the failure the unpinned `go get` used to produce; pinning moved it, so guard it.) + local need have + need="$(awk '/^go [0-9]/ {print $2; exit}' "$HARNESS/fidelity/go-redis.mod" 2>/dev/null)" + have="$(go env GOVERSION 2>/dev/null | sed 's/^go//')" + if [ -n "$need" ] && [ -n "$have" ]; then + # Numeric compare on major.minor only; sort -V orders versions correctly. + if [ "$(printf '%s\n%s\n' "$need" "$have" | sort -V | head -1)" != "$need" ]; then + printf 'go.mod needs Go >= %s but this box has %s' "$need" "$have" + return + fi + elif [ -z "$have" ]; then + printf 'no go toolchain found on PATH' + 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 + # through method_missing and omits the FIELDS token: "ERR wrong number of arguments". + if grep -qE '\.(hexpire|httl|hpexpire|hpttl)\b' "$REPO/$3" 2>/dev/null; then + if ! ruby -e 'exit(RUBY_VERSION >= "3.2" ? 0 : 1)' 2>/dev/null; then + printf 'uses hexpire/httl, which need redis-rb >= 6.0 and therefore Ruby >= 3.2; this box has %s' \ + "$(ruby -e 'print RUBY_VERSION' 2>/dev/null || echo 'no ruby')" + return + fi + fi + ;; + esac + printf '' +} + +illustrative_reason() { # $1 = set, $2 = canonical client key + case "$1:$2" in + # Every file in this set contains only auth1/auth2, which call AUTH with a `test-user` + # ACL identity that a throwaway Redis has no reason to define. Matches the shipped + # redis-py and node-redis examples, which have the same property. (PR #3627 triage.) + cmds_cnxmgmt:*) printf 'AUTH needs a test-user ACL identity; illustrative by design' ;; + *) printf '' ;; + esac +} + +# Glob local_examples/// for this client's example file. Aliases exist +# because the directory naming grew organically: cmds_* sets use NRedisStack/ while +# geoindex and search_quickstart use nredisstack/, and both are tracked in git. +convention_src_path() { + local set="$1" client="$2" aliases alias setdir cand dir f + aliases="$(tsv_get "$client" 5)" + [ -z "$aliases" ] || [ "$aliases" = "-" ] && return + setdir="$REPO/local_examples/$set" + [ -d "$setdir" ] || return + for alias in $(printf '%s' "$aliases" | tr '|' ' '); do + # Match the directory name with EXACT case. A glob on "$setdir/$alias" would + # succeed against the wrong case on a case-insensitive filesystem (macOS default) + # and yield a path that does not exist in git: cmds_* sets use NRedisStack/ while + # geoindex, search_quickstart and time_series_tutorial use nredisstack/. Globbing + # expands from readdir, so basename here is the true on-disk spelling. + dir="" + for cand in "$setdir"/*; do + [ -d "$cand" ] || continue + if [ "$(basename "$cand")" = "$alias" ]; then dir="$cand"; break; fi + done + [ -n "$dir" ] || continue + for f in "$dir"/*; do + [ -f "$f" ] || continue + case "$(basename "$f")" in + .*|*.md|README*) continue ;; + esac + printf '%s' "${f#$REPO/}" + return + done + done +} + +legacy_src_path() { local set="$1" client="$2" + [ -z "$client" ] && return case "$set:$client" in ss_tutorial:python) echo local_examples/tmp/datatypes/sorted-sets/dt_ss.py ;; ss_tutorial:node) echo local_examples/tmp/datatypes/sorted-sets/dt-ss.js ;; @@ -146,11 +326,18 @@ run_ioredis() { } run_go() { local d="$WORK/go"; mkdir -p "$d" - if [ ! -f "$d/go.mod" ]; then - (cd "$d" && go mod init tce.local >/dev/null 2>&1 && printf 'package example_commands\n' >lib.go \ - && go get github.com/redis/go-redis/v9 >/dev/null 2>&1) - fi - cp "$1" "$d/ex_test.go"; (cd "$d" && go test ./... ) >"$LOG" 2>&1; rc=$? + # Use the TRACKED go.mod rather than `go mod init` + an unpinned `go get`. An unpinned + # get floats to the newest go-redis, which eventually requires a newer Go toolchain than + # is installed — the failure is "toolchain not available", reported as a FAIL against a + # perfectly good example. Copying the pinned manifest also keeps portable and fidelity + # mode on the same dependency version. Re-copied every run so a stale cached go.mod + # (e.g. one already floated to a newer `go` directive) is corrected rather than inherited. + cp "$HARNESS/fidelity/go-redis.mod" "$d/go.mod" + # The examples declare `package example_commands`, so the module needs a second file in + # that package or the build fails before any test runs. + printf 'package example_commands\n' >"$d/lib.go" + cp "$1" "$d/ex_test.go" + (cd "$d" && go mod tidy >/dev/null 2>&1 && go test ./... ) >"$LOG" 2>&1; rc=$? } run_rust_sync() { rust_run "$WORK/rust-sync" "$1" 'redis = "1.3"' ; } run_rust_async(){ rust_run "$WORK/rust-async" "$1" 'redis = { version = "1.3", features = ["tokio-comp"] } @@ -175,7 +362,15 @@ run_maven_java() { # $1=src $2=workdir $3=package-relpath [ -f "$d/pom.xml" ] || cp "$HARNESS/pom-$(basename "$d").xml" "$d/pom.xml" rm -f "$d/src/test/java/$3"/*.java cp "$1" "$d/src/test/java/$3/" - (cd "$d" && mvn -q -B test) >"$LOG" 2>&1; rc=$? + (cd "$d" && mvn -B test) >"$LOG" 2>&1; rc=$? + # Surefire exits 0 when it matches no tests. These classes are named *Example, not *Test, + # so a pom missing the *Example include "passes" having run nothing. The generated fidelity + # wrappers guard this; portable mode is the DEFAULT mode, so it needs the same guard or the + # false green survives exactly where it is most likely to be hit. + if [ "${rc:-1}" -eq 0 ] && ! grep -qE 'Tests run: [1-9]' "$LOG"; then + printf '\nHARNESS ERROR: surefire ran zero tests — check the *Example include in pom.xml\n' >>"$LOG" + rc=1 + fi } run_jedis() { run_maven_java "$1" "$WORK/jedis" io/redis/examples ; } run_lettuce_sync() { run_maven_java "$1" "$WORK/lettuce-sync" io/redis/examples/sync ; } @@ -188,35 +383,167 @@ run_dotnet() { # stubs NRedisStack.Tests fixtures so the file runs under plain x cp "$HARNESS/dotnet/GlobalUsings.cs" "$d/GlobalUsings.cs" rm -f "$d"/Example_*.cs; cp "$1" "$d/Example_src.cs" (cd "$d" && dotnet test --nologo) >"$LOG" 2>&1; rc=$? + # Same false-green class as surefire: if the [Fact] didn't survive outside a REMOVE block, + # the runner discovers nothing and still exits 0. + # + # Accept both summary shapes, mirroring the generated fidelity wrapper: the classic VSTest + # line ("Passed! - Failed: 0, Passed: 2, ...") and Microsoft.Testing.Platform's + # ("succeeded: 2"). Matching only VSTest would reject a genuinely passing MTP run — the + # inverse false-negative of the bug this guard exists to prevent. + if [ "${rc:-1}" -eq 0 ] \ + && ! grep -qE 'Passed! *- *Failed: *[0-9]+, *Passed: *[1-9]' "$LOG" \ + && ! grep -qE '(succeeded|passed): *[1-9]' "$LOG"; then + printf '\nHARNESS ERROR: could not confirm any test passed. Neither a VSTest nor an MTP summary with a non-zero pass count was found — check the [Fact] survived outside a REMOVE block, or update this check if dotnet changed its summary format.\n' >>"$LOG" + rc=1 + fi } run_php() { local d="$WORK/php"; mkdir -p "$d" [ -d "$d/vendor/predis" ] || { printf '{}\n' >"$d/composer.json"; (cd "$d" && composer -q require predis/predis >/dev/null 2>&1); } + # Two test-base styles exist in local_examples: some PHP examples extend predis's own + # PredisTestCase, others extend PHPUnit\Framework\TestCase. Portable mode installs neither + # (only predis), so stub one assertion class and alias BOTH names onto it — otherwise a + # perfectly good example dies with "Class ... not found", which reads as an example defect. + # The PHPUnit alias is guarded so it yields to the real class if phpunit is ever installed. cat >"$d/bootstrap.php" <<'PHP' $e)) throw new Exception("assertGreaterThan: $a not > $e"); } + function assertContains($n,$h,$m=''){ if(!in_array($n,$h)) throw new Exception("assertContains failed"); } +} +class_alias('HarnessTestCase', 'PredisTestCase'); +if (!class_exists('PHPUnit\\Framework\\TestCase')) { + class_alias('HarnessTestCase', 'PHPUnit\\Framework\\TestCase'); } PHP cp "$1" "$d/example.php" + # Honour PHPUnit's setUp/tearDown lifecycle: examples that build their client in setUp() + # would otherwise run against an unset property. Aliases declared in bootstrap.php are + # already in $before, so they are never mistaken for the example's own class. (cd "$d" && php -r ' require "bootstrap.php"; $before=get_declared_classes(); require "example.php"; $cls=array_values(array_diff(get_declared_classes(),$before)); + // An example that declares no class leaves $cls empty; end() then returns false and + // `new false()` fatals with a message about the harness rather than the example. Fail + // with a diagnostic that names the actual problem. + if(!$cls){ + fwrite(STDERR,"HARNESS ERROR: example.php declared no class — a PHP TCE must define a test class\n"); + exit(1); + } $c=end($cls); $o=new $c(); - foreach(get_class_methods($o) as $m){ if(strpos($m,"test")===0){ $o->$m(); } } - fwrite(STDERR,"OK\n"); + // setUp/tearDown are protected by PHPUnit convention, so they need reflection. + $call=function($o,$name){ + if(!method_exists($o,$name)) return; + $r=new ReflectionMethod($o,$name); $r->setAccessible(true); $r->invoke($o); + }; + $ran=0; + foreach(get_class_methods($o) as $m){ + if(strpos($m,"test")!==0) continue; + $call($o,"setUp"); + $o->$m(); + $call($o,"tearDown"); + $ran++; + } + if($ran===0) { fwrite(STDERR,"HARNESS ERROR: no test* method found on $c\n"); exit(1); } + fwrite(STDERR,"OK ($ran test method(s))\n"); ') >"$LOG" 2>&1; rc=$? } +# --- fidelity mode ------------------------------------------------------------ +# Stage the file into tmp/clients/examples/// and hand off to that +# directory's run.sh, which bootstrap.sh materialised from fidelity/. The staged +# filename is the source basename: files in local_examples/ already follow the naming +# convention, so there is nothing to template here. (The `filename` column in +# clients.tsv is guidance for *authoring* a new example, not for staging one.) +run_fidelity() { # $1 = absolute source path, $2 = canonical client key + local src="$1" client="$2" dir sub root dest base saved="" + dir="$(tsv_get "$client" 9)"; sub="$(tsv_get "$client" 10)" + if [ -z "$dir" ] || [ "$dir" = "-" ]; then + printf 'no fidelity directory for %s in clients.tsv\n' "$client" >"$LOG"; rc=1; return + fi + root="$FIDELITY_ROOT/$dir" + if [ ! -x "$root/run.sh" ]; then + printf 'missing %s/run.sh — run build/example-test-harness/bootstrap.sh first\n' \ + "$root" >"$LOG"; rc=1; return + fi + dest="$root"; [ "$sub" = "." ] || dest="$root/$sub" + base="$(basename "$src")" + mkdir -p "$dest" + + # For the C# clients, fid_sub points at tests/Doc inside a real CLONE of the NRedisStack + # repo — not a throwaway scaffold. The staged filename is identical to the upstream one + # (CmdsHashExample.cs is both), so a naive stage-then-delete OVERWRITES a tracked upstream + # file and then removes it, leaving the clone with a deleted source. Back up anything we + # are about to clobber and restore it afterwards, so a run is always a no-op on the clone. + if [ -f "$dest/$base" ]; then + saved="$(mktemp "${TMPDIR:-/tmp}/tce-staged-XXXXXX")" + cp "$dest/$base" "$saved" + fi + + # Record what needs undoing BEFORE staging, and install a trap, so an interrupt or a kill + # partway through the test still restores the clone. Without this, Ctrl-C during a + # multi-minute dotnet/mvn run leaves the clone holding staged content indefinitely. + STAGED_PATH="$dest/$base"; STAGED_BACKUP="$saved" + trap 'unstage_fidelity; exit 130' INT TERM + + cp "$src" "$dest/$base" + # Pass the staged subdirectory through as the project/working dir. The C# runner needs it + # to tell tests/Doc from tests/Doc/Async; runners that don't take a second argument ignore it. + ( cd "$root" && ./run.sh "$base" "$sub" ) >"$LOG" 2>&1; rc=$? + + unstage_fidelity + trap - INT TERM +} + +# Undo whatever run_fidelity staged. Idempotent, and safe to call from a signal handler. +unstage_fidelity() { + [ -n "${STAGED_PATH:-}" ] || return 0 + if [ -n "${STAGED_BACKUP:-}" ]; then + # Restore the upstream file, and only drop the backup once the copy has demonstrably + # succeeded. Deleting it unconditionally would destroy the sole copy of a tracked + # upstream source whenever the restore failed — the exact loss this backup exists to + # prevent. If it fails, keep the backup and say where it is. + if cp "$STAGED_BACKUP" "$STAGED_PATH"; then + rm -f "$STAGED_BACKUP" + else + printf 'ERROR: could not restore %s — your backup is preserved at %s\n' \ + "$STAGED_PATH" "$STAGED_BACKUP" >&2 + log "ERROR: failed to restore $STAGED_PATH; backup kept at $STAGED_BACKUP" + fi + else + rm -f "$STAGED_PATH" + fi + STAGED_PATH=""; STAGED_BACKUP="" +} + +# --- list: resolve sources and exit (no Redis, no toolchains) ----------------- +if [ "$LIST" = 1 ]; then + for c in "${CLIENTS[@]}"; do + rel="$(src_path "$SET" "$c")" + printf '%-18s %s\n' "$c" "${rel:-(none)}" + done + exit 0 +fi + # --- drive -------------------------------------------------------------------- -log "=== TCE sweep: $SET (redis @ localhost:6379) ===" +log "=== TCE sweep: $SET [$MODE] (redis @ localhost:6379) ===" +if [ "$MODE" = fidelity ] && [ ! -d "$FIDELITY_ROOT" ]; then + log "ERROR: $FIDELITY_ROOT does not exist — run build/example-test-harness/bootstrap.sh" + exit 1 +fi # Fail fast if the scratch Redis is unreachable — every run FLUSHes it and relies # on a clean db, so silently proceeding would give misleading pass/fail results. if ! redis-cli ping >/dev/null 2>&1; then @@ -225,8 +552,16 @@ if ! redis-cli ping >/dev/null 2>&1; then fi mkdir -p "$HARNESS/results" for c in "${CLIENTS[@]}"; do + why="$(illustrative_reason "$SET" "$c")" + if [ -n "$why" ]; then + SUMMARY+=("$c SKIP ($why)"); log ">> $c: SKIP — $why"; continue + fi rel="$(src_path "$SET" "$c")" if [ -z "$rel" ] || [ ! -f "$REPO/$rel" ]; then SUMMARY+=("$c SKIP (no source)"); log ">> $c: SKIP"; continue; fi + why="$(toolchain_skip_reason "$SET" "$c" "$rel")" + if [ -n "$why" ]; then + SUMMARY+=("$c SKIP ($why)"); log ">> $c: SKIP — $why"; continue + fi LOG="$HARNESS/results/${SET}_${c}.log"; rc=1 # A failed flush means stale keys leak into the next example -> unreliable # results, so abort loudly rather than test against leftover state. @@ -234,8 +569,18 @@ for c in "${CLIENTS[@]}"; do log "ERROR: 'redis-cli flushall' failed before $c — aborting to avoid testing against stale keys." exit 1 fi - log ">> $c: running..." - "run_${c//-/_}" "$REPO/$rel" + log ">> $c: running... ($rel)" + if [ "$MODE" = fidelity ]; then + run_fidelity "$REPO/$rel" "$c" + else + # Portable runners are named after the legacy runner key, not the canonical one. + portable="$(tsv_get "$c" 11)" + if [ -z "$portable" ] || [ "$portable" = "-" ]; then + printf 'no portable runner for %s; try --fidelity\n' "$c" >"$LOG"; rc=1 + else + "run_${portable//-/_}" "$REPO/$rel" + fi + fi if [ "${rc:-1}" -eq 0 ]; then SUMMARY+=("$c PASS"); log ">> $c: PASS" else SUMMARY+=("$c FAIL (results/${SET}_${c}.log)"); log ">> $c: FAIL"; fi done diff --git a/content/commands/hlen.md b/content/commands/hlen.md index 9921dacf33..683b4ff1d7 100644 --- a/content/commands/hlen.md +++ b/content/commands/hlen.md @@ -57,14 +57,14 @@ The name of the key that holds the hash. ## Examples -{{% redis-cli %}} +{{< clients-example set="cmds_hash" step="hlen" description="Foundational: Count the fields in a hash with HLEN when you need a hash's size without transferring its contents" difficulty="beginner" >}} redis> HSET myhash field1 "Hello" (integer) 1 redis> HSET myhash field2 "World" (integer) 1 redis> HLEN myhash (integer) 2 -{{% /redis-cli %}} +{{< /clients-example >}} ## Redis Software and Redis Cloud compatibility diff --git a/for-ais-only/tcedocs/README.md b/for-ais-only/tcedocs/README.md index 20f025b09a..f8b99c1d71 100644 --- a/for-ais-only/tcedocs/README.md +++ b/for-ais-only/tcedocs/README.md @@ -8,9 +8,15 @@ There are two sections that need to updated when new languages are added. 1. In the `[params]` section: ```toml - clientsExamples = ["Python", "Node.js", "Java-Sync", "Lettuce-Sync", "Java-Async", "Java-Reactive", "Go", "C", "C#-Sync", "C#-Async", "RedisVL", "PHP", "Rust-Sync", "Rust-Async"] + clientsExamples = ["Python", "Node.js", "ioredis", "Java-Sync", "Lettuce-Sync", "Java-Async", "Java-Reactive", "Go", "C", "C#-Sync (NRedisStack)", "C#-Async (NRedisStack)", "C#-Sync (SE.Redis)", "C#-Async (SE.Redis)", "RedisVL", "PHP", "Ruby", "Rust-Sync", "Rust-Async"] ``` + > The authoritative list — display names alongside component ids, API-mapping keys, + > `local_examples` directory names, and filename conventions — is + > [`build/example-test-harness/clients.tsv`](../../build/example-test-harness/clients.tsv). + > Read it with `column -t -s$'\t'`. The snippets in this file are illustrative and can + > fall behind; that table is validated against `config.toml` and `data/components/`. + The order of the `clientsExamples` list matters: it's the order in which the language tabs are presented for each code example. 1. In the `[params.clientsConfig]` section: @@ -104,6 +110,7 @@ TEST_MARKER = { PREFIXES = { 'python': '#', 'node.js': '//', + 'ioredis': '//', 'java': '//', 'java-sync': '//', 'java-async': '//', @@ -111,12 +118,20 @@ PREFIXES = { 'go': '//', 'c': '//', 'c#': '//', + 'c#-sync': '//', + 'c#-async': '//', 'redisvl': '#', 'php': '//', - 'rust': '//' + 'ruby': '#', + 'rust': '//', + 'rust-sync': '//', + 'rust-async': '//' } ``` +Check these against [`build/components/example.py`](../../build/components/example.py) rather +than trusting the excerpt above — it is a copy and can drift. + The `TEST_MARKER` dictionary maps programming languages to test framework annotations, which allows the parser to filter such source code lines out. The `PREFIXES` dictionary maps each language to its comment prefix. Python, for example, uses a hashtag (`#`) to start a comment. ⚠️ **CRITICAL**: The `PREFIXES` dictionary is **essential** for the example parser to work. If you add a new language, you **must** add an entry to this dictionary, or examples will fail to process with an "Unknown language" error. This is the most commonly missed step when adding a new language. diff --git a/local_examples/cmds_hash/NRedisStack/CmdsHashExample.cs b/local_examples/cmds_hash/NRedisStack/CmdsHashExample.cs index d7ee541d45..079c54d4bf 100644 --- a/local_examples/cmds_hash/NRedisStack/CmdsHashExample.cs +++ b/local_examples/cmds_hash/NRedisStack/CmdsHashExample.cs @@ -209,5 +209,23 @@ public void Run() Assert.Equal("NoSuchField", string.Join(", ", hexpireRes3)); db.KeyDelete("myhash"); // REMOVE_END + + // STEP_START hlen + bool hlenRes1 = db.HashSet("myhash", "field1", "Hello"); + Console.WriteLine(hlenRes1); // >>> True + + bool hlenRes2 = db.HashSet("myhash", "field2", "World"); + Console.WriteLine(hlenRes2); // >>> True + + long hlenRes3 = db.HashLength("myhash"); + Console.WriteLine(hlenRes3); // >>> 2 + // STEP_END + + // REMOVE_START + Assert.True(hlenRes1); + Assert.True(hlenRes2); + Assert.Equal(2, hlenRes3); + db.KeyDelete("myhash"); + // REMOVE_END } } diff --git a/local_examples/cmds_hash/go-redis/cmds_hash_test.go b/local_examples/cmds_hash/go-redis/cmds_hash_test.go index 6fbeee8882..b280d87311 100644 --- a/local_examples/cmds_hash/go-redis/cmds_hash_test.go +++ b/local_examples/cmds_hash/go-redis/cmds_hash_test.go @@ -377,4 +377,55 @@ func ExampleClient_hexpire() { // [1 1] // 2 // [-2] -} \ No newline at end of file +} + +func ExampleClient_hlen() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password + DB: 0, // use default DB + }) + + // REMOVE_START + // start with fresh database + rdb.FlushDB(ctx) + rdb.Del(ctx, "myhash") + // REMOVE_END + + // STEP_START hlen + hlen1, err := rdb.HSet(ctx, "myhash", "field1", "Hello").Result() + + if err != nil { + panic(err) + } + + fmt.Println(hlen1) // >>> 1 + + hlen2, err := rdb.HSet(ctx, "myhash", "field2", "World").Result() + + if err != nil { + panic(err) + } + + fmt.Println(hlen2) // >>> 1 + + hlen3, err := rdb.HLen(ctx, "myhash").Result() + + if err != nil { + panic(err) + } + + fmt.Println(hlen3) // >>> 2 + // STEP_END + + // REMOVE_START + rdb.Del(ctx, "myhash") + // REMOVE_END + + // Output: + // 1 + // 1 + // 2 +} diff --git a/local_examples/cmds_hash/hiredis/cmds_hash.c b/local_examples/cmds_hash/hiredis/cmds_hash.c index 5c24901ba5..372534a6da 100644 --- a/local_examples/cmds_hash/hiredis/cmds_hash.c +++ b/local_examples/cmds_hash/hiredis/cmds_hash.c @@ -72,6 +72,45 @@ int main(int argc, char **argv) { redisCommand(c, "DEL myhash"); // REMOVE_END + // STEP_START hlen + // Add two new fields to the hash + reply = redisCommand(c, "HSET %s %s %s", "myhash", "field1", "Hello"); + printf("HSET myhash field1 Hello: %lld\n", reply->integer); // >>> 1 + // REMOVE_START + if (reply->integer != 1) { + printf("ASSERTION FAILED: Expected 1, got %lld\n", reply->integer); + } + // REMOVE_END + freeReplyObject(reply); + + reply = redisCommand(c, "HSET %s %s %s", "myhash", "field2", "World"); + printf("HSET myhash field2 World: %lld\n", reply->integer); // >>> 1 + // REMOVE_START + if (reply->integer != 1) { + printf("ASSERTION FAILED: Expected 1, got %lld\n", reply->integer); + } + // REMOVE_END + freeReplyObject(reply); + + // Count the fields in the hash + reply = redisCommand(c, "HLEN %s", "myhash"); + printf("HLEN myhash: %lld\n", reply->integer); // >>> 2 + // REMOVE_START + if (reply->type != REDIS_REPLY_INTEGER) { + printf("ASSERTION FAILED: Expected an integer reply for HLEN\n"); + } + if (reply->integer != 2) { + printf("ASSERTION FAILED: Expected 2, got %lld\n", reply->integer); + } + // REMOVE_END + freeReplyObject(reply); + // STEP_END + + // REMOVE_START + redisReply *del_reply = redisCommand(c, "DEL myhash"); + freeReplyObject(del_reply); + // REMOVE_END + // STEP_START disconnect redisFree(c); // STEP_END diff --git a/local_examples/cmds_hash/ioredis/cmds-hash.js b/local_examples/cmds_hash/ioredis/cmds-hash.js index 56c570867f..a220afebff 100644 --- a/local_examples/cmds_hash/ioredis/cmds-hash.js +++ b/local_examples/cmds_hash/ioredis/cmds-hash.js @@ -23,6 +23,24 @@ assert.deepEqual(hmgetResult, ['Hello', 'World', null]); await redis.del('myhash'); // REMOVE_END +// STEP_START hlen +const hlenSet1 = await redis.hset('myhash', 'field1', 'Hello'); +console.log(hlenSet1); // >>> 1 + +const hlenSet2 = await redis.hset('myhash', 'field2', 'World'); +console.log(hlenSet2); // >>> 1 + +const hlenResult = await redis.hlen('myhash'); +console.log(hlenResult); // >>> 2 +// STEP_END + +// REMOVE_START +assert.equal(hlenSet1, 1); +assert.equal(hlenSet2, 1); +assert.equal(hlenResult, 2); +await redis.del('myhash'); +// REMOVE_END + // HIDE_START redis.disconnect(); // HIDE_END diff --git a/local_examples/cmds_hash/jedis/CmdsHashExample.java b/local_examples/cmds_hash/jedis/CmdsHashExample.java index 9d7b686439..8cc5717a4e 100644 --- a/local_examples/cmds_hash/jedis/CmdsHashExample.java +++ b/local_examples/cmds_hash/jedis/CmdsHashExample.java @@ -215,6 +215,26 @@ public void run() { jedis.del("myhash"); // REMOVE_END + // STEP_START hlen + // `hset` returns 1 because `field1` is a new field. + long hLenResult1 = jedis.hset("myhash", "field1", "Hello"); + System.out.println(hLenResult1); // >>> 1 + + // `hset` returns 1 because `field2` is also a new field. + long hLenResult2 = jedis.hset("myhash", "field2", "World"); + System.out.println(hLenResult2); // >>> 1 + + long hLenResult3 = jedis.hlen("myhash"); + System.out.println(hLenResult3); // >>> 2 + // STEP_END + // REMOVE_START + // Tests for 'hlen' step. + assertEquals(1L, hLenResult1); + assertEquals(1L, hLenResult2); + assertEquals(2L, hLenResult3); + jedis.del("myhash"); + // REMOVE_END + // HIDE_START jedis.close(); } diff --git a/local_examples/cmds_hash/lettuce-async/CmdsHashExample.java b/local_examples/cmds_hash/lettuce-async/CmdsHashExample.java index 99256d0873..263f1baed5 100644 --- a/local_examples/cmds_hash/lettuce-async/CmdsHashExample.java +++ b/local_examples/cmds_hash/lettuce-async/CmdsHashExample.java @@ -142,7 +142,7 @@ public void run() { CompletableFuture hmgetExample = asyncCommands.hset("myhash", hmgetExampleParams).thenCompose(res1 -> { return asyncCommands.hmget("myhash", "field1", "field2", "nofield"); }).thenAccept(res2 -> { - System.out.println(res2); // >>> [KeyValue[field1, Hello], KeyValue[field2, World], KeyValue[nofield, null]] + System.out.println(res2); // >>> [KeyValue[field1, Hello], KeyValue[field2, World], KeyValue[nofield].empty] // REMOVE_START assertThat(res2).hasSize(3); // REMOVE_END @@ -250,6 +250,34 @@ public void run() { // REMOVE_START asyncCommands.del("myhash").toCompletableFuture().join(); // REMOVE_END + + // STEP_START hlen + CompletableFuture hLenExample = asyncCommands.hset("myhash", "field1", "Hello").thenCompose(res1 -> { + // `hset` returns true because `field1` is a new field. + System.out.println(res1); // >>> true + // REMOVE_START + assertThat(res1).isEqualTo(true); + // REMOVE_END + return asyncCommands.hset("myhash", "field2", "World"); + }).thenCompose(res2 -> { + // `hset` returns true because `field2` is also a new field. + System.out.println(res2); // >>> true + // REMOVE_START + assertThat(res2).isEqualTo(true); + // REMOVE_END + return asyncCommands.hlen("myhash"); + }).thenAccept(res3 -> { + System.out.println(res3); // >>> 2 + // REMOVE_START + assertThat(res3).isEqualTo(2L); + // REMOVE_END + }).toCompletableFuture(); + // STEP_END + + hLenExample.join(); + // REMOVE_START + asyncCommands.del("myhash").toCompletableFuture().join(); + // REMOVE_END } finally { redisClient.shutdown(); } diff --git a/local_examples/cmds_hash/lettuce-reactive/CmdsHashExample.java b/local_examples/cmds_hash/lettuce-reactive/CmdsHashExample.java index b7ee1dbf84..b57ae1f8ae 100644 --- a/local_examples/cmds_hash/lettuce-reactive/CmdsHashExample.java +++ b/local_examples/cmds_hash/lettuce-reactive/CmdsHashExample.java @@ -181,7 +181,7 @@ public void run() { System.out.println(result); // >>> KeyValue[field1, Hello] // >>> KeyValue[field2, World] - // >>> KeyValue[nofield, null] + // >>> KeyValue[nofield].empty }); // STEP_END @@ -300,6 +300,38 @@ public void run() { // REMOVE_START reactiveCommands.del("myhash").block(); // REMOVE_END + + // STEP_START hlen + Mono hLenExample1 = reactiveCommands.hset("myhash", "field1", "Hello").doOnNext(result -> { + System.out.println(result); // >>> true + // REMOVE_START + assertThat(result).isEqualTo(true); + // REMOVE_END + }); + + hLenExample1.block(); + + Mono hLenExample2 = reactiveCommands.hset("myhash", "field2", "World").doOnNext(result -> { + System.out.println(result); // >>> true + // REMOVE_START + assertThat(result).isEqualTo(true); + // REMOVE_END + }); + + hLenExample2.block(); + + Mono hLenExample3 = reactiveCommands.hlen("myhash").doOnNext(result -> { + System.out.println(result); // >>> 2 + // REMOVE_START + assertThat(result).isEqualTo(2L); + // REMOVE_END + }); + // STEP_END + + hLenExample3.block(); + // REMOVE_START + reactiveCommands.del("myhash").block(); + // REMOVE_END } finally { redisClient.shutdown(); } diff --git a/local_examples/cmds_hash/lettuce-sync/CmdsHashExample.java b/local_examples/cmds_hash/lettuce-sync/CmdsHashExample.java new file mode 100644 index 0000000000..ffb4a63c05 --- /dev/null +++ b/local_examples/cmds_hash/lettuce-sync/CmdsHashExample.java @@ -0,0 +1,228 @@ +// EXAMPLE: cmds_hash +// HIDE_START +package io.redis.examples.sync; + +import io.lettuce.core.*; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.api.StatefulRedisConnection; + +import java.util.*; +// REMOVE_START +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +// REMOVE_END + +public class CmdsHashExample { + + // REMOVE_START + @Test + // REMOVE_END + public void run() { + RedisClient redisClient = RedisClient.create("redis://localhost:6379"); + + try (StatefulRedisConnection connection = redisClient.connect()) { + RedisCommands syncCommands = connection.sync(); + // HIDE_END + + // REMOVE_START + syncCommands.del("myhash"); + // REMOVE_END + + // STEP_START hdel + // `hset` returns true because `field1` is a new field. + boolean res1 = syncCommands.hset("myhash", "field1", "foo"); + System.out.println(res1); // >>> true + + Long res2 = syncCommands.hdel("myhash", "field1"); + System.out.println(res2); // >>> 1 + + // `hdel` returns 0 because `field2` doesn't exist. + Long res3 = syncCommands.hdel("myhash", "field2"); + System.out.println(res3); // >>> 0 + // STEP_END + + // REMOVE_START + assertThat(res1).isTrue(); + assertThat(res2).isEqualTo(1L); + assertThat(res3).isEqualTo(0L); + syncCommands.del("myhash"); + // REMOVE_END + + // STEP_START hset + // `hset` returns true because `field1` is a new field. + boolean res4 = syncCommands.hset("myhash", "field1", "Hello"); + System.out.println(res4); // >>> true + + String res5 = syncCommands.hget("myhash", "field1"); + System.out.println(res5); // >>> Hello + + // The `Map` overload of `hset` returns the number of new fields. + Map newFields = new HashMap<>(); + newFields.put("field2", "Hi"); + newFields.put("field3", "World"); + + Long res6 = syncCommands.hset("myhash", newFields); + System.out.println(res6); // >>> 2 + + String res7 = syncCommands.hget("myhash", "field2"); + System.out.println(res7); // >>> Hi + + String res8 = syncCommands.hget("myhash", "field3"); + System.out.println(res8); // >>> World + + // `hgetall` returns a `Map`, whose iteration order isn't + // guaranteed. Wrap it in a `TreeMap` to sort the fields by name. + Map res9 = syncCommands.hgetall("myhash"); + System.out.println(new TreeMap<>(res9)); + // >>> {field1=Hello, field2=Hi, field3=World} + // STEP_END + + // REMOVE_START + assertThat(res4).isTrue(); + assertThat(res5).isEqualTo("Hello"); + assertThat(res6).isEqualTo(2L); + assertThat(res7).isEqualTo("Hi"); + assertThat(res8).isEqualTo("World"); + assertThat(new TreeMap<>(res9).toString()).isEqualTo("{field1=Hello, field2=Hi, field3=World}"); + syncCommands.del("myhash"); + // REMOVE_END + + // STEP_START hget + // `hset` returns true because `field1` is a new field. + boolean res10 = syncCommands.hset("myhash", "field1", "foo"); + System.out.println(res10); // >>> true + + String res11 = syncCommands.hget("myhash", "field1"); + System.out.println(res11); // >>> foo + + // `hget` returns null because `field2` doesn't exist. + String res12 = syncCommands.hget("myhash", "field2"); + System.out.println(res12); // >>> null + // STEP_END + + // REMOVE_START + assertThat(res10).isTrue(); + assertThat(res11).isEqualTo("foo"); + assertThat(res12).isNull(); + syncCommands.del("myhash"); + // REMOVE_END + + // STEP_START hmget + Map hmgetFields = new HashMap<>(); + hmgetFields.put("field1", "Hello"); + hmgetFields.put("field2", "World"); + + syncCommands.hset("myhash", hmgetFields); + + // `hmget` returns a `KeyValue` for each field you ask for, in the + // order you asked for them. A field that doesn't exist comes back + // as an empty `KeyValue`. + List> res13 = syncCommands.hmget("myhash", "field1", "field2", "nofield"); + System.out.println(res13); + // >>> [KeyValue[field1, Hello], KeyValue[field2, World], KeyValue[nofield].empty] + // STEP_END + + // REMOVE_START + assertThat(res13).hasSize(3); + assertThat(res13.get(0).getValue()).isEqualTo("Hello"); + assertThat(res13.get(1).getValue()).isEqualTo("World"); + assertThat(res13.get(2).hasValue()).isFalse(); + syncCommands.del("myhash"); + // REMOVE_END + + // STEP_START hgetall + Map hGetAllFields = new HashMap<>(); + hGetAllFields.put("field1", "Hello"); + hGetAllFields.put("field2", "World"); + + syncCommands.hset("myhash", hGetAllFields); + + // `hgetall` returns a `Map`, whose iteration order isn't + // guaranteed. Wrap it in a `TreeMap` to sort the fields by name. + Map res14 = syncCommands.hgetall("myhash"); + System.out.println(new TreeMap<>(res14)); + // >>> {field1=Hello, field2=World} + // STEP_END + + // REMOVE_START + assertThat(new TreeMap<>(res14).toString()).isEqualTo("{field1=Hello, field2=World}"); + syncCommands.del("myhash"); + // REMOVE_END + + // STEP_START hvals + Map hValsFields = new HashMap<>(); + hValsFields.put("field1", "Hello"); + hValsFields.put("field2", "World"); + + syncCommands.hset("myhash", hValsFields); + + // The order of the values isn't guaranteed, so sort them before + // printing. + List res15 = syncCommands.hvals("myhash"); + List sortedValues = new ArrayList<>(res15); + Collections.sort(sortedValues); + System.out.println(sortedValues); // >>> [Hello, World] + // STEP_END + + // REMOVE_START + assertThat(sortedValues).containsExactly("Hello", "World"); + syncCommands.del("myhash"); + // REMOVE_END + + // STEP_START hexpire + // Set up a hash with two fields. + Map hExpireFields = new HashMap<>(); + hExpireFields.put("field1", "Hello"); + hExpireFields.put("field2", "World"); + + syncCommands.hset("myhash", hExpireFields); + + // Set the expiration of both fields. `hexpire` returns a status + // code for each field, where 1 means the expiration was set. + List res16 = syncCommands.hexpire("myhash", 10, "field1", "field2"); + System.out.println(res16); // >>> [1, 1] + + // Check the time to live of the fields. + List res17 = syncCommands.httl("myhash", "field1", "field2"); + System.out.println(res17); // >>> [10, 10] + + // Try to set the expiration of a field that doesn't exist. + // The status code -2 means there's no such field. + List res18 = syncCommands.hexpire("myhash", 10, "nonexistent"); + System.out.println(res18); // >>> [-2] + // STEP_END + + // REMOVE_START + assertThat(res16).containsExactly(1L, 1L); + assertThat(res17).hasSize(2); + assertThat(res17.stream().allMatch(ttl -> ttl > 0)).isTrue(); + assertThat(res18).containsExactly(-2L); + syncCommands.del("myhash"); + // REMOVE_END + + // STEP_START hlen + // `hset` returns true because `field1` is a new field. + boolean res19 = syncCommands.hset("myhash", "field1", "Hello"); + System.out.println(res19); // >>> true + + // `hset` returns true because `field2` is also a new field. + boolean res20 = syncCommands.hset("myhash", "field2", "World"); + System.out.println(res20); // >>> true + + Long res21 = syncCommands.hlen("myhash"); + System.out.println(res21); // >>> 2 + // STEP_END + + // REMOVE_START + assertThat(res19).isTrue(); + assertThat(res20).isTrue(); + assertThat(res21).isEqualTo(2L); + syncCommands.del("myhash"); + // REMOVE_END + // HIDE_START + } finally { + redisClient.shutdown(); + } + // HIDE_END + } +} diff --git a/local_examples/cmds_hash/node-redis/cmds-hash.js b/local_examples/cmds_hash/node-redis/cmds-hash.js index a98d8a3acc..26992a33b2 100644 --- a/local_examples/cmds_hash/node-redis/cmds-hash.js +++ b/local_examples/cmds_hash/node-redis/cmds-hash.js @@ -165,6 +165,24 @@ await client.del('myhash') // REMOVE_END // STEP_END +// STEP_START hlen +const res17 = await client.hSet('myhash', 'field1', 'Hello') +console.log(res17) // 1 + +const res18 = await client.hSet('myhash', 'field2', 'World') +console.log(res18) // 1 + +const res19 = await client.hLen('myhash') +console.log(res19) // 2 + +// REMOVE_START +assert.equal(res17, 1); +assert.equal(res18, 1); +assert.equal(res19, 2); +await client.del('myhash') +// REMOVE_END +// STEP_END + // HIDE_START await client.close(); // HIDE_END diff --git a/local_examples/cmds_hash/predis/CmdsHashTest.php b/local_examples/cmds_hash/predis/CmdsHashTest.php index c592aa5c59..d9f110bdb3 100644 --- a/local_examples/cmds_hash/predis/CmdsHashTest.php +++ b/local_examples/cmds_hash/predis/CmdsHashTest.php @@ -171,6 +171,28 @@ public function testCmdsHash(): void $this->assertTrue(array_reduce($hExpireResult3, function($carry, $ttl) { return $carry && $ttl > 0; }, true)); // TTL should be positive $this->assertEquals([-2], $hExpireResult4); // REMOVE_END + + // STEP_START hlen + // REMOVE_START + $this->redis->del('myhash'); + // REMOVE_END + + $hLenResult1 = $this->redis->hset('myhash', 'field1', 'Hello'); + echo "HSET myhash field1 Hello: " . $hLenResult1 . "\n"; // >>> 1 + + $hLenResult2 = $this->redis->hset('myhash', 'field2', 'World'); + echo "HSET myhash field2 World: " . $hLenResult2 . "\n"; // >>> 1 + + $hLenResult3 = $this->redis->hlen('myhash'); + echo "HLEN myhash: " . $hLenResult3 . "\n"; // >>> 2 + // STEP_END + + // REMOVE_START + $this->assertEquals(1, $hLenResult1); + $this->assertEquals(1, $hLenResult2); + $this->assertEquals(2, $hLenResult3); + $this->redis->del('myhash'); + // REMOVE_END } protected function tearDown(): void diff --git a/local_examples/cmds_hash/redis-py/cmds_hash.py b/local_examples/cmds_hash/redis-py/cmds_hash.py index 8664d23c4c..8b70077aa1 100644 --- a/local_examples/cmds_hash/redis-py/cmds_hash.py +++ b/local_examples/cmds_hash/redis-py/cmds_hash.py @@ -141,4 +141,25 @@ assert res14 == [-2] r.delete("myhash") # REMOVE_END +# STEP_END + +# STEP_START hlen +res15 = r.hset("myhash", "field1", "Hello") +print(res15) +# >>> 1 + +res16 = r.hset("myhash", "field2", "World") +print(res16) +# >>> 1 + +res17 = r.hlen("myhash") +print(res17) +# >>> 2 + +# REMOVE_START +assert res15 == 1 +assert res16 == 1 +assert res17 == 2 +r.delete("myhash") +# REMOVE_END # STEP_END \ No newline at end of file diff --git a/local_examples/cmds_hash/ruby/cmds_hash.rb b/local_examples/cmds_hash/ruby/cmds_hash.rb new file mode 100644 index 0000000000..dede870a2d --- /dev/null +++ b/local_examples/cmds_hash/ruby/cmds_hash.rb @@ -0,0 +1,160 @@ +# EXAMPLE: cmds_hash +# HIDE_START +require 'redis' + +r = Redis.new +# HIDE_END + +# REMOVE_START +def assert_equal(expected, actual) + raise "Expected #{expected.inspect}, got #{actual.inspect}" unless actual == expected +end + +r.del('myhash') +# REMOVE_END + +# STEP_START hdel +res1 = r.hset('myhash', 'field1', 'foo') +puts res1 # >>> 1 + +res2 = r.hdel('myhash', 'field1') +puts res2 # >>> 1 + +res3 = r.hdel('myhash', 'field2') +puts res3 # >>> 0 +# STEP_END + +# REMOVE_START +assert_equal(1, res1) +assert_equal(1, res2) +assert_equal(0, res3) +r.del('myhash') +# REMOVE_END + +# STEP_START hset +res4 = r.hset('myhash', 'field1', 'Hello') +puts res4 # >>> 1 + +res5 = r.hget('myhash', 'field1') +puts res5 # >>> Hello + +res6 = r.hset('myhash', { 'field2' => 'Hi', 'field3' => 'World' }) +puts res6 # >>> 2 + +res7 = r.hget('myhash', 'field2') +puts res7 # >>> Hi + +res8 = r.hget('myhash', 'field3') +puts res8 # >>> World + +res9 = r.hgetall('myhash') +puts res9.inspect +# >>> {"field1"=>"Hello", "field2"=>"Hi", "field3"=>"World"} +# STEP_END + +# REMOVE_START +assert_equal(1, res4) +assert_equal('Hello', res5) +assert_equal(2, res6) +assert_equal('Hi', res7) +assert_equal('World', res8) +assert_equal({ 'field1' => 'Hello', 'field2' => 'Hi', 'field3' => 'World' }, res9) +r.del('myhash') +# REMOVE_END + +# STEP_START hget +res10 = r.hset('myhash', 'field1', 'foo') +puts res10 # >>> 1 + +res11 = r.hget('myhash', 'field1') +puts res11 # >>> foo + +res12 = r.hget('myhash', 'field2') +puts res12.inspect # >>> nil +# STEP_END + +# REMOVE_START +assert_equal(1, res10) +assert_equal('foo', res11) +assert_equal(nil, res12) +r.del('myhash') +# REMOVE_END + +# STEP_START hmget +r.hset('myhash', { 'field1' => 'Hello', 'field2' => 'World' }) + +res13 = r.hmget('myhash', 'field1', 'field2', 'nofield') +puts res13.inspect # >>> ["Hello", "World", nil] +# STEP_END + +# REMOVE_START +assert_equal(['Hello', 'World', nil], res13) +r.del('myhash') +# REMOVE_END + +# STEP_START hgetall +r.hset('myhash', { 'field1' => 'Hello', 'field2' => 'World' }) + +res14 = r.hgetall('myhash') +puts res14.inspect # >>> {"field1"=>"Hello", "field2"=>"World"} +# STEP_END + +# REMOVE_START +assert_equal({ 'field1' => 'Hello', 'field2' => 'World' }, res14) +r.del('myhash') +# REMOVE_END + +# STEP_START hvals +r.hset('myhash', { 'field1' => 'Hello', 'field2' => 'World' }) + +res15 = r.hvals('myhash') +puts res15.inspect # >>> ["Hello", "World"] +# STEP_END + +# REMOVE_START +assert_equal(['Hello', 'World'], res15) +r.del('myhash') +# REMOVE_END + +# STEP_START hexpire +# Set up a hash with two fields. +r.hset('myhash', { 'field1' => 'Hello', 'field2' => 'World' }) + +# Set an expiration on both fields. +res16 = r.hexpire('myhash', 10, 'field1', 'field2') +puts res16.inspect # >>> [1, 1] + +# Check the TTL of the fields. +res17 = r.httl('myhash', 'field1', 'field2') +puts res17.inspect # >>> [10, 10] + +# Try to set an expiration on a field that does not exist. +res18 = r.hexpire('myhash', 10, 'nonexistent') +puts res18.inspect # >>> [-2] +# STEP_END + +# REMOVE_START +assert_equal([1, 1], res16) +assert_equal(true, res17.all? { |ttl| ttl > 0 }) +assert_equal([-2], res18) +r.del('myhash') +# REMOVE_END + +# STEP_START hlen +res19 = r.hset('myhash', 'field1', 'Hello') +puts res19 # >>> 1 + +res20 = r.hset('myhash', 'field2', 'World') +puts res20 # >>> 1 + +res21 = r.hlen('myhash') +puts res21 # >>> 2 +# STEP_END + +# REMOVE_START +assert_equal(1, res19) +assert_equal(1, res20) +assert_equal(2, res21) +r.del('myhash') +r.close +# REMOVE_END diff --git a/local_examples/cmds_hash/rust-async/cmds_hash.rs b/local_examples/cmds_hash/rust-async/cmds_hash.rs index a0fb81a965..782182b994 100644 --- a/local_examples/cmds_hash/rust-async/cmds_hash.rs +++ b/local_examples/cmds_hash/rust-async/cmds_hash.rs @@ -439,5 +439,53 @@ mod cmds_hash_tests { let _: Result = r.del("myhash").await; // REMOVE_END // STEP_END + + // STEP_START hlen + match r.hset("myhash", "field1", "Hello").await { + Ok(res15) => { + let res15: i32 = res15; + println!("{res15}"); // >>> 1 + // REMOVE_START + assert_eq!(res15, 1); + // REMOVE_END + }, + Err(e) => { + println!("Error setting hash field: {e}"); + return; + } + } + + match r.hset("myhash", "field2", "World").await { + Ok(res16) => { + let res16: i32 = res16; + println!("{res16}"); // >>> 1 + // REMOVE_START + assert_eq!(res16, 1); + // REMOVE_END + }, + Err(e) => { + println!("Error setting hash field: {e}"); + return; + } + } + + match r.hlen("myhash").await { + Ok(res17) => { + let res17: usize = res17; + println!("{res17}"); // >>> 2 + // REMOVE_START + assert_eq!(res17, 2); + // REMOVE_END + }, + Err(e) => { + println!("Error getting hash length: {e}"); + return; + } + } + + // REMOVE_START + let _: Result = r.del("myhash").await; + // REMOVE_END + // STEP_END } } diff --git a/local_examples/cmds_hash/rust-sync/cmds_hash.rs b/local_examples/cmds_hash/rust-sync/cmds_hash.rs index d905cbc72b..22991cca8b 100644 --- a/local_examples/cmds_hash/rust-sync/cmds_hash.rs +++ b/local_examples/cmds_hash/rust-sync/cmds_hash.rs @@ -439,5 +439,53 @@ mod cmds_hash_tests { let _: Result = r.del("myhash"); // REMOVE_END // STEP_END + + // STEP_START hlen + match r.hset("myhash", "field1", "Hello") { + Ok(hlen1) => { + let hlen1: i32 = hlen1; + println!("{hlen1}"); // >>> 1 + // REMOVE_START + assert_eq!(hlen1, 1); + // REMOVE_END + }, + Err(e) => { + println!("Error setting hash field: {e}"); + return; + } + } + + match r.hset("myhash", "field2", "World") { + Ok(hlen2) => { + let hlen2: i32 = hlen2; + println!("{hlen2}"); // >>> 1 + // REMOVE_START + assert_eq!(hlen2, 1); + // REMOVE_END + }, + Err(e) => { + println!("Error setting hash field: {e}"); + return; + } + } + + match r.hlen("myhash") { + Ok(hlen3) => { + let hlen3: usize = hlen3; + println!("{hlen3}"); // >>> 2 + // REMOVE_START + assert_eq!(hlen3, 2); + // REMOVE_END + }, + Err(e) => { + println!("Error getting hash length: {e}"); + return; + } + } + + // REMOVE_START + let _: Result = r.del("myhash"); + // REMOVE_END + // STEP_END } } diff --git a/local_examples/cmds_string/lettuce-async/CmdsStringExample.java b/local_examples/cmds_string/lettuce-async/CmdsStringExample.java index c1697f74fe..7392c7192e 100644 --- a/local_examples/cmds_string/lettuce-async/CmdsStringExample.java +++ b/local_examples/cmds_string/lettuce-async/CmdsStringExample.java @@ -34,10 +34,10 @@ public void run() { .thenCompose(res2 -> asyncCommands.mget("key1", "key2", "nonexisting")) .thenAccept(res3 -> { System.out.println(res3); - // >>> [KeyValue[key1, Hello], KeyValue[key2, World], KeyValue[nonexisting, null]] + // >>> [KeyValue[key1, Hello], KeyValue[key2, World], KeyValue[nonexisting].empty] // REMOVE_START assertThat(res3.toString()).isEqualTo( - "[KeyValue[key1, Hello], KeyValue[key2, World], KeyValue[nonexisting, null]]"); + "[KeyValue[key1, Hello], KeyValue[key2, World], KeyValue[nonexisting].empty]"); // REMOVE_END }) .toCompletableFuture(); diff --git a/local_examples/cmds_string/lettuce-reactive/CmdsStringExample.java b/local_examples/cmds_string/lettuce-reactive/CmdsStringExample.java index 8f516f9ef5..81f3058f94 100644 --- a/local_examples/cmds_string/lettuce-reactive/CmdsStringExample.java +++ b/local_examples/cmds_string/lettuce-reactive/CmdsStringExample.java @@ -33,10 +33,10 @@ public void run() { .flatMap(res2 -> reactiveCommands.mget("key1", "key2", "nonexisting").collectList()) .doOnNext(res3 -> { System.out.println(res3); - // >>> [KeyValue[key1, Hello], KeyValue[key2, World], KeyValue[nonexisting, null]] + // >>> [KeyValue[key1, Hello], KeyValue[key2, World], KeyValue[nonexisting].empty] // REMOVE_START assertThat(res3.toString()).isEqualTo( - "[KeyValue[key1, Hello], KeyValue[key2, World], KeyValue[nonexisting, null]]"); + "[KeyValue[key1, Hello], KeyValue[key2, World], KeyValue[nonexisting].empty]"); // REMOVE_END }) .then();