Compare commits

...

4 Commits

Author SHA1 Message Date
cba76ae724
tests.message: clean up a little bit 2023-08-27 18:11:34 -07:00
1c3e5ff538
tests: add basic connection test
I created a utility class to spawn a NATS server. This assumes it is
installed and in PATH, which should be easy enough to accomplish for a
CI environment. The current approach will not work with parallel
tests, but that's not a practical reality, so this route should be
fine for the time being. It might be nice to spawn a default server at
the beginning of testing and tear it down at the end, to save waiting
for the startup/shutdown time in many individual tests. This makes me
wonder: if I initialize a server in the beginning of the `test` block
in main that imports the other modules, does that scope stay live
while the "child" tests are running? My default guess would be
probably not, but that would be very convenient, so I might try it out
and see.
2023-08-27 18:11:34 -07:00
d3d5849f55
build test: install unit tests binary
I've gotten tired of trying to find this in the cache dir, since the
build system does not rerun it once it is cached. Also sometimes the
build runner does weird things to the output.
2023-08-27 18:11:34 -07:00
4cf5049882
tests: add top level function tests
These are mostly just tests to make sure the wrappers function as
written, rather than testing for any specific output. This commit
demonstrates the simple value of actually referencing the various
wrapper functions, as a variety of really basic compilation errors
were caught and addressed.
2023-08-27 18:11:34 -07:00
8 changed files with 241 additions and 37 deletions

View File

@ -19,6 +19,7 @@ pub fn build(b: *std.Build) void {
});
const tests = b.addTest(.{
.name = "nats-zig-unit-tests",
.root_source_file = .{ .path = "tests/main.zig" },
.target = target,
.optimize = optimize,
@ -29,6 +30,7 @@ pub fn build(b: *std.Build) void {
const run_main_tests = b.addRunArtifact(tests);
const test_step = b.step("test", "Run tests");
test_step.dependOn(&b.addInstallArtifact(tests, .{}).step);
test_step.dependOn(&run_main_tests.step);
add_examples(b, .{

View File

@ -12,7 +12,7 @@ pub const ErrorInfo = struct {
};
pub fn getLastError() ErrorInfo {
const status: c_uint = 0;
var status: c_uint = 0;
const desc = nats_c.nats_GetLastError(&status);
return .{
@ -21,8 +21,8 @@ pub fn getLastError() ErrorInfo {
};
}
pub fn getLastErrorStack(buffer: []u8) Error!void {
const status = Status.fromInt(nats_c.getLastErrorStack(buffer.ptr, buffer.len));
pub fn getLastErrorStack(buffer: *[]u8) Error!void {
const status = Status.fromInt(nats_c.nats_GetLastErrorStack(buffer.ptr, buffer.len));
return status.raise();
}

View File

@ -35,6 +35,9 @@ pub const Message = msg_.Message;
pub const Statistics = sta_.Statistics;
pub const StatsCounts = sta_.StatsCounts;
pub const ErrorInfo = err_.ErrorInfo;
pub const getLastError = err_.getLastError;
pub const getLastErrorStack = err_.getLastErrorStack;
pub const Status = err_.Status;
pub const Error = err_.Error;
@ -59,7 +62,7 @@ pub fn now() i64 {
return nats_c.nats_Now();
}
pub fn nowInNanoSeconds() i64 {
pub fn nowInNanoseconds() i64 {
return nats_c.nats_NowInNanoSeconds();
}
@ -76,6 +79,8 @@ pub fn releaseThreadMemory() void {
return nats_c.nats_ReleaseThreadMemory();
}
pub const default_spin_count: i64 = -1;
pub fn init(lock_spin_count: i64) Error!void {
const status = Status.fromInt(nats_c.nats_Open(lock_spin_count));
return status.raise();
@ -85,40 +90,52 @@ pub fn deinit() void {
return nats_c.nats_Close();
}
// the result of this requires manual deallocation unless it is used to provide the
// signature out-parameter in the natsSignatureHandler callback. Calling it outside of
// that context seems unlikely, but we should probably provide a deinit function so the
// user doesn't have to dig around for libc free to deallocate it.
pub fn sign(encoded_seed: [:0]const u8, input: [:0]const u8) Error![]const u8 {
var result: [*]u8 = undefined;
var length: c_int = 0;
const status = Status.fromInt(nats_c.nats_Sign(encoded_seed.ptr, &input, &length));
return status.toError() orelse result[0..@intCast(length)];
}
pub fn deinitWait(timeout: i64) Error!void {
const status = Status.fromInt(nats_c.nats_CloseAndWait(timeout));
return status.raise();
}
// This appears to be a jetstream API, but these two endpoints are trivial, so, whoops.
// I have no clue what this does, since there's basically no
pub const Inbox = opaque {
pub fn create() Error!*Inbox {
var self: *Inbox = undefined;
const status = Status.fromInt(nats_c.natsInbox_Create(@ptrCast(&self)));
// the result of this requires manual deallocation unless it is used to provide the
// signature out-parameter in the natsSignatureHandler callback. Calling it outside of
// that context seems unlikely, but we should probably provide a deinit function so the
// user doesn't have to dig around for libc free to deallocate it.
pub fn sign(encoded_seed: [:0]const u8, input: [:0]const u8) Error![]const u8 {
var result: [*c]u8 = undefined;
var length: c_int = 0;
const status = Status.fromInt(nats_c.nats_Sign(
encoded_seed.ptr,
input.ptr,
&result,
&length,
));
return status.toError() orelse self;
}
return status.toError() orelse result[0..@intCast(length)];
}
pub fn destroy(self: *Inbox) void {
nats_c.natsInbox_Destroy(@ptrCast(self));
}
};
// Note: an "Inbox" is actually just a string. This API creates a random (unique)
// string suitable for passing as the `reply` field to Message.create or
// Connection.publishRequest. The string is owned by the caller and should be freed
// using `destroyInbox`.
pub fn createInbox() Error![:0]u8 {
var self: [*c]u8 = undefined;
const status = Status.fromInt(nats_c.natsInbox_Create(@ptrCast(&self)));
return status.toError() orelse std.mem.sliceTo(self, 0);
}
pub fn destroyInbox(inbox: [:0]u8) void {
nats_c.natsInbox_Destroy(@ptrCast(inbox.ptr));
}
// I think this is also a jetstream API. This function sure does not seem at all useful
// by itself.
// by itself. Note: for some reason, most of the jetstream data structures are all
// public, instead of following the opaque handle style that the rest of the library
// does.
// typedef struct natsMsgList {
// natsMsg **Msgs;
// int Count;
// } natsMsgList;
pub const MessageList = opaque {
pub fn destroy(self: *MessageList) void {
nats_c.natsMsgList_Destroy(@ptrCast(self));

18
tests/connection.zig Normal file
View File

@ -0,0 +1,18 @@
const std = @import("std");
const nats = @import("nats");
const util = @import("./util.zig");
test "nats.Connection.connectTo" {
var server = try util.TestServer.launch(.{});
defer server.stop();
{
try nats.init(nats.default_spin_count);
defer nats.deinit();
const connection = try nats.Connection.connectTo(nats.default_server_url);
defer connection.destroy();
}
}

View File

@ -1,5 +1,5 @@
const std = @import("std");
test {
_ = @import("./nats.zig");
_ = @import("./connection.zig");
_ = @import("./message.zig");
}

View File

@ -1,8 +1,6 @@
const std = @import("std");
const nats = @import("nats");
// const nats = @import("../src/nats.zig");
// const message = @import("../src/message.zig");
const nats = @import("nats");
test "message: create message" {
const subject = "hello";
@ -11,7 +9,7 @@ test "message: create message" {
// have to initialize the library so the reference counter can correctly destroy
// objects, otherwise we segfault on trying to free the memory.
try nats.init(-1);
try nats.init(nats.default_spin_count);
defer nats.deinit();
const message = try nats.Message.create(subject, reply, data);
@ -28,7 +26,7 @@ test "message: create message" {
}
test "message: get subject" {
try nats.init(-1);
try nats.init(nats.default_spin_count);
defer nats.deinit();
const subject = "hello";
@ -40,7 +38,7 @@ test "message: get subject" {
}
test "message: get reply" {
try nats.init(-1);
try nats.init(nats.default_spin_count);
defer nats.deinit();
const subject = "hello";

96
tests/nats.zig Normal file
View File

@ -0,0 +1,96 @@
const std = @import("std");
const nats = @import("nats");
test "version" {
const version = nats.getVersion();
const vernum = nats.getVersionNumber();
try std.testing.expectEqualStrings("3.6.1", version);
try std.testing.expectEqual(@as(u32, 0x03_06_01), vernum);
try std.testing.expect(nats.checkCompatibility());
}
test "time" {
const now = nats.now();
const nownano = nats.nowInNanoseconds();
nats.sleep(1);
const later = nats.now();
const laternano = nats.nowInNanoseconds();
try std.testing.expect(later >= now);
try std.testing.expect(laternano >= nownano);
}
test "init" {
{
try nats.init(nats.default_spin_count);
defer nats.deinit();
}
{
// a completely random number
try nats.init(900_142_069);
nats.deinit();
}
{
try nats.init(0);
try nats.deinitWait(1000);
}
}
test "misc" {
{
try nats.init(nats.default_spin_count);
defer nats.deinit();
try nats.setMessageDeliveryPoolSize(500);
}
{
try nats.init(nats.default_spin_count);
defer nats.deinit();
// just test that the function is wrapped properly
nats.releaseThreadMemory();
}
blk: {
try nats.init(nats.default_spin_count);
defer nats.deinit();
// this is a mess of a test that is designed to fail because actually we're
// testing out the error reporting functions instead of signing. Nice bait
// and switch.
const signed = nats.sign("12345678", "12345678") catch {
const err = nats.getLastError();
std.debug.print("signing failed: {s}\n", .{err.desc});
var stackmem = [_]u8{0} ** 512;
var stackbuf: []u8 = &stackmem;
nats.getLastErrorStack(&stackbuf) catch {
std.debug.print("Actually, the error stack was too big\n", .{});
break :blk;
};
std.debug.print("stack: {s}\n", .{stackbuf});
break :blk;
};
std.heap.raw_c_allocator.free(signed);
}
}
test "inbox" {
try nats.init(nats.default_spin_count);
defer nats.deinit();
const inbox = try nats.createInbox();
defer nats.destroyInbox(inbox);
std.debug.print("inbox: {s}\n", .{inbox});
}

73
tests/util.zig Normal file
View File

@ -0,0 +1,73 @@
const std = @import("std");
const TestLaunchError = error{
NoLaunchStringFound,
};
pub const TestServer = struct {
process: std.ChildProcess,
pub const LaunchOptions = struct {
executable: []const u8 = "nats-server",
port: u16 = 4222,
auth: union(enum) {
none: void,
token: []const u8,
password: struct { user: []const u8, pass: []const u8 },
} = .none,
allocator: std.mem.Allocator = std.testing.allocator,
fn argLen(self: LaunchOptions) usize {
// executable, -a, 127.0.0.1, -p, 4222
const base_len: usize = 5;
return base_len + switch (self.auth) {
.none => @as(usize, 0),
.token => @as(usize, 2),
.password => @as(usize, 4),
};
}
};
pub fn launch(options: LaunchOptions) !TestServer {
// const allocator = std.testing.allocator;
var portbuf = [_]u8{0} ** 5;
const strport = try std.fmt.bufPrint(&portbuf, "{d}", .{options.port});
const argsbuf: [9][]const u8 = blk: {
const executable: [1][]const u8 = .{options.executable};
const listen: [2][]const u8 = .{ "-a", "127.0.0.1" };
const port: [2][]const u8 = .{ "-p", strport };
const auth: [4][]const u8 = switch (options.auth) {
.none => .{""} ** 4,
.token => |tok| .{ "--auth", tok, "", "" },
.password => |auth| .{ "--user", auth.user, "--password", auth.pass },
};
break :blk executable ++ listen ++ port ++ auth;
};
const args = argsbuf[0..options.argLen()];
var child = std.ChildProcess.init(args, options.allocator);
child.stdin_behavior = .Ignore;
child.stdout_behavior = .Pipe;
child.stderr_behavior = .Pipe;
try child.spawn();
var poller = std.io.poll(options.allocator, enum { stderr }, .{ .stderr = child.stderr.? });
defer poller.deinit();
while (try poller.poll()) {
if (std.mem.indexOf(u8, poller.fifo(.stderr).buf, "[INF] Server is ready")) |_| {
return .{ .process = child };
}
}
_ = try child.kill();
return error.NoLaunchStringFound;
}
pub fn stop(self: *TestServer) void {
_ = self.process.kill() catch return;
}
};