Testing cstrings returned from C code

Hey everyone,

I just started looking into Zig as an interesting alternative for programming in C/Go/rust. In order to learn more about the language I started dabbling around with it. And since I found the high C-interoperability highly interesting, and with C lacking sound and built-in support for testing, I started with unit testing C code in zig. And maybe my question has been asked to death already…

I have a bunch of C code that I try to cover with zig tests instead of having my tests written in a custom C unit test framework. I have all the basics (calling C from zig, passing arguments, getting stuff back) figured out. The only thing I can’t wrap my head around is how to test cstrings returned from my C code in a good way.

Here’s an example of what I fight with. I have a C function with this signature:

const char* kv_store_get(kv_store* store, const char* key);

A fairly simple function that, as the name suggests, retrieves the value that matches the key stored in an inmemory key-value-store. Nothing fancy. It returns a char* with a copy of the data from the key-value-store.

I already have a working zig test that checks if a key and value can be stored and retrieved correctly. It looks like this:

test "kvstore: put and get a value" {
    const store = ckvstore.create_kv_store(1);
    try std.testing.expect(store != null);

    const key  = "key";
    const value: [*:0]const u8 = "value";

    const result = ckvstore.kv_store_put(store, key, value);
    try std.testing.expect(result == 0);

    const retrieved_value = ckvstore.kv_store_get(store, key);
    // this here is my workaround:
    try std.testing.expect(C.strcmp(retrieved_value, value) == 0);

    ckvstore.free_kv_store(store);
}

What bothers me is that I could not manage checking the returned string and comparing it with the initially stored value by using zig-native functions. Eventually I resorted to using strcmp from native C.

I tried working with std.mem.eql in different variations of

try std.mem.eql(u8, retrieved_value, value);

but I could not match the datatype of the cstring correctly and always get stuck at a variation of this error:

test\server_test.zig:24:25: error: expected type '[]const u8', found '[*c]const u8'
    try std.mem.eql(u8, retrieved_value, value);

Any help and explanation of which concepts I (obviously) got wrong is highly appriciated. Thanks!

Maybe you need to use std.mem.span to convert your C string into a proper Zig slice first? You can then use all the Zig goodies to work with the slice.

3 Likes

std.mem.span is the best way of converting a C string into a Zig slice for general tasks, but for comparing two C strings specifically, you can use std.mem.orderZ, which works a lot like strcmp:

try std.testing.expect(std.mem.orderZ(u8, retrieved_value, value) == .eq);

I should also mention that for testing two strings for equality in a unit test, there’s std.testing.expectEqualStrings, which takes two []const u8 slices (so you would need to use std.mem.span beforehand) but provides the added benefit of logging the strings as an error if they don’t match.

3 Likes

Amazing. Thanks, @castholm and @gonzo! The combination of std.mem.span and std.testing.expectEqualStrings works perfectly and as expected. I was sure I was just missing something kind of obvious.

Thats my solution now:

const retrieved_value = std.mem.span(ckvstore.kv_store_get(store, key));
try std.testing.expectEqualStrings(std.mem.span(value), retrieved_value);

Some points:

  1. Use defer to clean up.
  2. Use expectEqual instead of expect, it gives much better error messages displaying the expected and the actual value. The expected value is the first parameter and the actual is the second (last) parameter.
  3. Use slices to hold strings in zig. Slices are fat pointers with pointers and length. Use zero terminated slices when you want to pass the slices to C functions.
  4. Use .ptr to get the pointer from the slice and std.mem.span to convert a zero terminated pointer to a slice (see: Convert []const u8 to [*:0]const u8 - #2 by dimdin)
  5. Use expectEqualStrings to compare the contents of two slices.
test "kvstore: put and get a value" {
    const store = ckvstore.create_kv_store(1);
    try std.testing.expect(store != null);
    defer ckvstore.free_kv_store(store);

    const key  = "key";
    const value: [:0]const u8 = "value";

    const result = ckvstore.kv_store_put(store, key, value.ptr);
    try std.testing.expectEqual(0, result);

    const retrieved_value = ckvstore.kv_store_get(store, key);
    try std.testing.expectEqualStrings(value, std.mem.span(retrieved_value));
}

Extra points for replacing the store C API with a zig API:

  • Use slices instead of C strings.
  • create_kv_store must not return a nullable store. If there is a need to represent failure you can return an error union of store !Store and call using const store = try ckvstore.create_kv_store(1);
  • kv_store_get must return an optional value to denote that a key might not be in the store. Call using:
    if (ckvstore.kv_store_get(store, key)) |retrieved_value| {
        try std.testing.expectEqualStrings(value, retrieved_value);
    }
    
1 Like