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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,44 @@ int main(int argc, char **argv) {

## Key Patterns

### 0. CHECK_REPLY — mandatory, and the reason C examples are testable at all

**A C example has no assertion framework, so the harness can only see the process exit
code.** hiredis returns an *error reply* (a valid `redisReply` with
`type == REDIS_REPLY_ERROR`) for a malformed command rather than failing the call, and it
does not abort. Without a guard, an example can send a broken command, print a wrong value
and still exit 0 — a green run that proves nothing. That happened for real in DOC-6968:
`redisCommand(c, "SET mykey \"Hello World\"")` split on whitespace (hiredis does **not**
honour CLI-style quoting — bind a value containing spaces with `%s`), the server answered
`ERR syntax error`, and the sweep still reported PASS.

So every C example defines this macro in a `REMOVE` block after the includes, and calls it
after **every** `redisCommand`, also inside a `REMOVE` block, so the published snippet stays
plain `redisCommand`:

```c
// REMOVE_START
#define CHECK_REPLY(r) do { \
if ((r) == NULL || (r)->type == REDIS_REPLY_ERROR) { \
printf("REDIS ERROR: %s\n", (r) ? (r)->str : "no reply from server"); \
return 1; \
} \
} while (0)
// REMOVE_END

reply = redisCommand(c, "SET mykey %s", "Hello World");
// REMOVE_START
CHECK_REPLY(reply);
// REMOVE_END
```

Note why a cheaper check is not enough: scanning the program's output for `ERR` misses the
common case, because `printf("%lld", reply->integer)` on an error reply prints **`0`**, not
the error text. The guard has to sit at the call site.

Value assertions in these files must also `return 1`, not merely `printf("ASSERTION
FAILED")` — otherwise they cannot fail the harness either.

### 1. Includes
```c
// STEP_START includes
Expand Down
19 changes: 19 additions & 0 deletions local_examples/client-specific/c/landing.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@

#include <hiredis/hiredis.h>

// REMOVE_START
// Fail loudly on a NULL or error reply. hiredis returns an error REPLY (not a
// connection error) for things like a bad command, and the examples would
// otherwise print a wrong value and still exit 0 — a green harness run that
// proves nothing. Kept in a REMOVE block so the published example stays plain.
#define CHECK_REPLY(r) do { \
if ((r) == NULL || (r)->type == REDIS_REPLY_ERROR) { \
printf("REDIS ERROR: %s\n", (r) ? (r)->str : "no reply from server"); \
return 1; \
} \
} while (0)
// REMOVE_END

int main() {
// The `redisContext` type represents the connection
// to the Redis server. Here, we connect to the
Expand All @@ -30,11 +43,17 @@ int main() {

// Set a string key.
redisReply *reply = redisCommand(c, "SET foo bar");
// REMOVE_START
CHECK_REPLY(reply);
// REMOVE_END
printf("Reply: %s\n", reply->str);
freeReplyObject(reply);

// Get the key we have just stored.
reply = redisCommand(c, "GET foo");
// REMOVE_START
CHECK_REPLY(reply);
// REMOVE_END
printf("Reply: %s\n", reply->str);
freeReplyObject(reply);

Expand Down
208 changes: 208 additions & 0 deletions local_examples/cmds_generic/go-redis/cmds_generic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,211 @@ func ExampleClient_ttl_cmd() {
// OK
// 10
}

func ExampleClient_scan1_cmd() {
ctx := context.Background()

rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})

// REMOVE_START
rdb.FlushDB(ctx)
// REMOVE_END

// STEP_START scan1
scan1Result1, err := rdb.SAdd(ctx, "myset", "1", "2", "3", "foo", "foobar", "feelsgood").Result()

if err != nil {
panic(err)
}

fmt.Println(scan1Result1) // >>> 6

scan1Result2, _, err := rdb.SScan(ctx, "myset", 0, "f*", 0).Result()

if err != nil {
panic(err)
}

sort.Strings(scan1Result2)
fmt.Println(scan1Result2) // >>> [feelsgood foo foobar]
// STEP_END

// Output:
// 6
// [feelsgood foo foobar]
}

func ExampleClient_scan2_cmd() {
ctx := context.Background()

rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})

// REMOVE_START
rdb.FlushDB(ctx)

for i := 1; i <= 1000; i++ {
rdb.Set(ctx, fmt.Sprintf("key:%d", i), i, 0)
}
// REMOVE_END

// STEP_START scan2
// MATCH is applied after elements are fetched, so with the default COUNT most
// iterations return few keys or none at all.
var scan2Cursor uint64
var scan2Keys []string
var err error

for i := 0; i < 4; i++ {
scan2Keys, scan2Cursor, err = rdb.Scan(ctx, scan2Cursor, "*11*", 0).Result()

if err != nil {
panic(err)
}
}

// A larger COUNT forces more scanning in a single iteration, so the remaining
// matches arrive together. This continues from the cursor reached above.
scan2Keys, _, err = rdb.Scan(ctx, scan2Cursor, "*11*", 1000).Result()

if err != nil {
panic(err)
}

fmt.Println(len(scan2Keys)) // >>> 18
// STEP_END

// REMOVE_START
rdb.FlushDB(ctx)
// REMOVE_END

// Output:
// 18
}

func ExampleClient_scan3_cmd() {
ctx := context.Background()

rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})

// REMOVE_START
rdb.FlushDB(ctx)
// REMOVE_END

// STEP_START scan3
scan3Result1, err := rdb.GeoAdd(ctx, "geokey", &redis.GeoLocation{
Longitude: 0, Latitude: 0, Name: "value",
}).Result()

if err != nil {
panic(err)
}

fmt.Println(scan3Result1) // >>> 1

scan3Result2, err := rdb.ZAdd(ctx, "zkey", redis.Z{Score: 1000, Member: "value"}).Result()

if err != nil {
panic(err)
}

fmt.Println(scan3Result2) // >>> 1

scan3Result3, err := rdb.Type(ctx, "geokey").Result()

if err != nil {
panic(err)
}

fmt.Println(scan3Result3) // >>> zset

scan3Result4, err := rdb.Type(ctx, "zkey").Result()

if err != nil {
panic(err)
}

fmt.Println(scan3Result4) // >>> zset

scan3Result5, _, err := rdb.ScanType(ctx, 0, "", 0, "zset").Result()

if err != nil {
panic(err)
}

sort.Strings(scan3Result5)
fmt.Println(scan3Result5) // >>> [geokey zkey]
// STEP_END

// Output:
// 1
// 1
// zset
// zset
// [geokey zkey]
}

func ExampleClient_scan4_cmd() {
ctx := context.Background()

rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password docs
DB: 0, // use default DB
})

// REMOVE_START
rdb.FlushDB(ctx)
// REMOVE_END

// STEP_START scan4
scan4Result1, err := rdb.HSet(ctx, "myhash", "a", 1, "b", 2).Result()

if err != nil {
panic(err)
}

fmt.Println(scan4Result1) // >>> 2

scan4Result2, _, err := rdb.HScan(ctx, "myhash", 0, "", 0).Result()

if err != nil {
panic(err)
}

// HSCAN returns field and value interleaved. Redis does not promise an order, so
// collect the pairs into a map: fmt prints map keys sorted, whatever order they arrived in.
scan4Fields := map[string]string{}

for i := 0; i < len(scan4Result2); i += 2 {
scan4Fields[scan4Result2[i]] = scan4Result2[i+1]
}

fmt.Println(scan4Fields) // >>> map[a:1 b:2]

scan4Result3, _, err := rdb.HScanNoValues(ctx, "myhash", 0, "", 0).Result()

if err != nil {
panic(err)
}

sort.Strings(scan4Result3)
fmt.Println(scan4Result3) // >>> [a b]
// STEP_END

// Output:
// 2
// map[a:1 b:2]
// [a b]
Comment thread
cursor[bot] marked this conversation as resolved.
}
Loading
Loading