Compare commits

...

10 Commits

Author SHA1 Message Date
jaina heartles e4b67ad66b add system include directory
ci/woodpecker/push/woodpecker Pipeline failed Details
2022-12-18 06:25:00 -08:00
jaina heartles d5f8f84480 Enable integration tests in CI
ci/woodpecker/push/woodpecker Pipeline failed Details
2022-12-18 06:21:52 -08:00
jaina heartles 613215efec Fix memory leaks in integration tests 2022-12-18 06:21:04 -08:00
jaina heartles 64bbb7dae9 Fix compile errors in integration tests 2022-12-18 05:58:48 -08:00
jaina heartles 1d65984323 Use non-comptime ints in tests
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-12-18 05:44:13 -08:00
jaina heartles 4ec3a61bb6 Fix deserializer tests 2022-12-18 05:43:36 -08:00
jaina heartles 67855b57c9 Fix http tests 2022-12-18 05:34:16 -08:00
jaina heartles e02c5d99e4 Allow returning default values on base struct 2022-12-18 05:19:17 -08:00
jaina heartles a0b2e22e32 Fix compile errors in tests 2022-12-18 05:18:20 -08:00
jaina heartles 0930eee561 Download zig binary once
ci/woodpecker/push/woodpecker Pipeline failed Details
2022-12-18 05:08:36 -08:00
10 changed files with 167 additions and 111 deletions

View File

@ -6,14 +6,32 @@ clone:
lfs: false lfs: false
pipeline: pipeline:
fmt: prepare-compiler:
image: alpine image: alpine
commands:
- echo "Downloading dependencies..."
- apk add curl xz
- echo "Downloading zig binary..."
- curl https://ziglang.org/download/0.10.0/zig-linux-x86_64-0.10.0.tar.xz | xz -d | tar -x - curl https://ziglang.org/download/0.10.0/zig-linux-x86_64-0.10.0.tar.xz | xz -d | tar -x
format:
image: alpine
commands:
- echo "Checking formatting..."
- ./zig-linux-x86_64-0.10.0/zig fmt --check src/ tests/ build.zig - ./zig-linux-x86_64-0.10.0/zig fmt --check src/ tests/ build.zig
unit-tests: unit-tests:
image: alpine image: alpine
commands: commands:
- apk add sqlite sqlite-dev libpq libpq-dev curl xz - echo "Downloading dependencies..."
- curl https://ziglang.org/download/0.10.0/zig-linux-x86_64-0.10.0.tar.xz | xz -d | tar -x - apk add sqlite sqlite-dev libpq libpq-dev
- echo "Running unit tests..."
- ./zig-linux-x86_64-0.10.0/zig build unit - ./zig-linux-x86_64-0.10.0/zig build unit
integration-tests:
image: alpine
commands:
- echo "Downloading dependencies..."
- apk add sqlite sqlite-dev libpq libpq-dev
- echo "Running integration tests..."
- ./zig-linux-x86_64-0.10.0/zig build integration-tests

View File

@ -137,6 +137,7 @@ pub fn build(b: *std.build.Builder) !void {
api_integration.addPackage(pkgs.main); api_integration.addPackage(pkgs.main);
api_integration.addPackage(pkgs.api); api_integration.addPackage(pkgs.api);
api_integration.linkLibC(); api_integration.linkLibC();
api_integration.addSystemIncludePath("/usr/include/");
if (enable_sqlite) api_integration.linkSystemLibrary("sqlite3"); if (enable_sqlite) api_integration.linkSystemLibrary("sqlite3");
if (enable_postgres) api_integration.linkSystemLibrary("pq"); if (enable_postgres) api_integration.linkSystemLibrary("pq");

View File

@ -322,6 +322,7 @@ fn ApiConn(comptime DbConn: type) type {
pub fn close(self: *Self) void { pub fn close(self: *Self) void {
util.deepFree(self.allocator, self.community); util.deepFree(self.allocator, self.community);
if (self.token_info) |info| util.deepFree(self.allocator, info);
self.db.releaseConnection(); self.db.releaseConnection();
} }
@ -353,11 +354,14 @@ fn ApiConn(comptime DbConn: type) type {
const user = try services.actors.get(self.db, info.user_id, self.allocator); const user = try services.actors.get(self.db, info.user_id, self.allocator);
defer util.deepFree(self.allocator, user); defer util.deepFree(self.allocator, user);
const username = try util.deepClone(self.allocator, user.username);
errdefer util.deepFree(self.allocator, username);
return AuthorizationInfo{ return AuthorizationInfo{
.id = user.id, .id = user.id,
.username = try util.deepClone(self.allocator, user.username), .username = username,
.community_id = self.community.id, .community_id = self.community.id,
.host = self.community.host, .host = try util.deepClone(self.allocator, self.community.host),
.issued_at = info.issued_at, .issued_at = info.issued_at,
}; };

View File

@ -104,7 +104,7 @@ pub fn login(
error.NoRows => error.InvalidLogin, error.NoRows => error.InvalidLogin,
else => error.DatabaseFailure, else => error.DatabaseFailure,
}; };
errdefer util.deepFree(alloc, info); defer alloc.free(info.hash);
try verifyPassword(info.hash, password, alloc); try verifyPassword(info.hash, password, alloc);
@ -162,6 +162,7 @@ pub fn verifyToken(
alloc: std.mem.Allocator, alloc: std.mem.Allocator,
) VerifyTokenError!TokenInfo { ) VerifyTokenError!TokenInfo {
const hash = try hashToken(token, alloc); const hash = try hashToken(token, alloc);
defer alloc.free(hash);
return db.queryRow( return db.queryRow(
TokenInfo, TokenInfo,

View File

@ -163,27 +163,12 @@ test "InjectContextValue" {
.handle(.{}, .{}, .{ .efgh = @as(usize, 10) }, ExpectContext(.{ .abcd = 5, .efgh = 10 }){}); .handle(.{}, .{}, .{ .efgh = @as(usize, 10) }, ExpectContext(.{ .abcd = 5, .efgh = 10 }){});
} }
fn expectDeepEquals(expected: anytype, actual: anytype) !void {
const E = @TypeOf(expected);
const A = @TypeOf(actual);
if (E == void) return std.testing.expect(A == void);
try std.testing.expect(std.meta.fields(E).len == std.meta.fields(A).len);
inline for (std.meta.fields(E)) |f| {
const e = @field(expected, f.name);
const a = @field(actual, f.name);
if (comptime std.meta.trait.isZigString(f.field_type)) {
try std.testing.expectEqualStrings(a, e);
} else {
try std.testing.expectEqual(a, e);
}
}
}
// Helper for testing purposes // Helper for testing purposes
fn ExpectContext(comptime val: anytype) type { fn ExpectContext(comptime val: anytype) type {
return struct { return struct {
pub fn handle(_: @This(), _: anytype, _: anytype, ctx: anytype, _: void) !void { pub fn handle(_: @This(), _: anytype, _: anytype, ctx: anytype, _: void) !void {
try expectDeepEquals(val, ctx); if (@TypeOf(val) == void) return error.TestUnexpectedResult;
try util.testing.expectDeepEqual(@as(@TypeOf(ctx), val), ctx);
} }
}; };
} }
@ -485,7 +470,15 @@ pub fn mount(comptime route: []const u8) Mount(route) {
test "mount" { test "mount" {
const testCase = struct { const testCase = struct {
fn func(comptime base: []const u8, request: []const u8, comptime expected: ?[]const u8) !void { fn func(comptime base: []const u8, request: []const u8, comptime expected: ?[]const u8) !void {
const result = mount(base).handle(.{}, .{}, addField(.{}, "path", request), expectContext(.{ .path = expected orelse "" })); const result = mount(base).handle(
.{},
.{},
addField(.{}, "path", request),
expectContext(.{
.path = expected orelse "",
.mounted_at = std.mem.trim(u8, base, "/"),
}),
);
try if (expected != null) result else std.testing.expectError(error.RouteMismatch, result); try if (expected != null) result else std.testing.expectError(error.RouteMismatch, result);
} }
}.func; }.func;
@ -590,7 +583,8 @@ test "ParsePathArgs" {
expected: @TypeOf(expected), expected: @TypeOf(expected),
path: []const u8, path: []const u8,
fn handle(self: @This(), _: anytype, _: anytype, ctx: anytype, _: void) !void { fn handle(self: @This(), _: anytype, _: anytype, ctx: anytype, _: void) !void {
try expectDeepEquals(self.expected, ctx.args); if (@TypeOf(expected) == @TypeOf(null)) return error.TestUnexpectedResult;
try util.testing.expectDeepEqual(@as(@TypeOf(ctx.args), self.expected), ctx.args);
try std.testing.expectEqualStrings(self.path, ctx.path); try std.testing.expectEqualStrings(self.path, ctx.path);
} }
}{ .expected = expected, .path = path }; }{ .expected = expected, .path = path };
@ -619,10 +613,10 @@ test "ParsePathArgs" {
try testCase("/:foo*", struct { foo: []const u8 }, "/", .{ .foo = "/" }); try testCase("/:foo*", struct { foo: []const u8 }, "/", .{ .foo = "/" });
try testCase("/:foo*", struct { foo: []const u8 }, "", .{ .foo = "" }); try testCase("/:foo*", struct { foo: []const u8 }, "", .{ .foo = "" });
try std.testing.expectError(error.RouteMismatch, testCase("/:id", struct { id: usize }, "/", .{})); try std.testing.expectError(error.RouteMismatch, testCase("/:id", struct { id: usize }, "/", null));
try std.testing.expectError(error.RouteMismatch, testCase("/abcd/:id", struct { id: usize }, "/123", .{})); try std.testing.expectError(error.RouteMismatch, testCase("/abcd/:id", struct { id: usize }, "/123", null));
try std.testing.expectError(error.RouteMismatch, testCase("/:id", struct { id: usize }, "/3/id/blahblah", .{ .id = 3 })); try std.testing.expectError(error.RouteMismatch, testCase("/:id", struct { id: usize }, "/3/id/blahblah", null));
try std.testing.expectError(error.InvalidCharacter, testCase("/:id", struct { id: usize }, "/xyz", .{})); try std.testing.expectError(error.InvalidCharacter, testCase("/:id", struct { id: usize }, "/xyz", null));
} }
const BaseContentType = enum { const BaseContentType = enum {
@ -715,7 +709,7 @@ pub fn ParseBody(comptime Body: type, comptime options: ParseBodyOptions) type {
} }
}; };
} }
pub fn parseBody(comptime Body: type) ParseBody(Body) { pub fn parseBody(comptime Body: type, comptime options: ParseBodyOptions) ParseBody(Body, options) {
return .{}; return .{};
} }
@ -752,7 +746,7 @@ test "parseBody" {
var headers = http.Fields.init(std.testing.allocator); var headers = http.Fields.init(std.testing.allocator);
defer headers.deinit(); defer headers.deinit();
try parseBody(Struct).handle( try parseBody(Struct, .{}).handle(
.{ .body = @as(?std.io.StreamSource, stream), .headers = headers }, .{ .body = @as(?std.io.StreamSource, stream), .headers = headers },
.{}, .{},
.{ .allocator = std.testing.allocator }, .{ .allocator = std.testing.allocator },

View File

@ -357,6 +357,6 @@ test "parseFormData" {
var src = std.io.StreamSource{ .const_buffer = std.io.fixedBufferStream(body) }; var src = std.io.StreamSource{ .const_buffer = std.io.fixedBufferStream(body) };
const val = try parseFormData(struct { const val = try parseFormData(struct {
foo: []const u8, foo: []const u8,
}, "abcd", src.reader(), std.testing.allocator); }, false, "abcd", src.reader(), std.testing.allocator);
util.deepFree(std.testing.allocator, val); util.deepFree(std.testing.allocator, val);
} }

View File

@ -252,49 +252,49 @@ fn formatQuery(comptime prefix: []const u8, params: anytype, writer: anytype) !v
test "parse" { test "parse" {
const testCase = struct { const testCase = struct {
fn case(comptime T: type, expected: T, query_string: []const u8) !void { fn case(allow_unknown_fields: bool, comptime T: type, expected: T, query_string: []const u8) !void {
const result = try parse(std.testing.allocator, T, query_string); const result = try parse(std.testing.allocator, allow_unknown_fields, T, query_string);
defer parseFree(std.testing.allocator, result); defer parseFree(std.testing.allocator, result);
try util.testing.expectDeepEqual(expected, result); try util.testing.expectDeepEqual(expected, result);
} }
}.case; }.case;
try testCase(struct { int: usize = 3 }, .{ .int = 3 }, ""); try testCase(false, struct { int: usize = 3 }, .{ .int = 3 }, "");
try testCase(struct { int: usize = 3 }, .{ .int = 2 }, "int=2"); try testCase(false, struct { int: usize = 3 }, .{ .int = 2 }, "int=2");
try testCase(struct { int: usize = 3 }, .{ .int = 2 }, "int=2&"); try testCase(false, struct { int: usize = 3 }, .{ .int = 2 }, "int=2&");
try testCase(struct { boolean: bool = false }, .{ .boolean = false }, ""); try testCase(false, struct { boolean: bool = false }, .{ .boolean = false }, "");
try testCase(struct { boolean: bool = false }, .{ .boolean = true }, "boolean"); try testCase(false, struct { boolean: bool = false }, .{ .boolean = true }, "boolean");
try testCase(struct { boolean: bool = false }, .{ .boolean = true }, "boolean=true"); try testCase(false, struct { boolean: bool = false }, .{ .boolean = true }, "boolean=true");
try testCase(struct { boolean: bool = false }, .{ .boolean = true }, "boolean=y"); try testCase(false, struct { boolean: bool = false }, .{ .boolean = true }, "boolean=y");
try testCase(struct { boolean: bool = false }, .{ .boolean = false }, "boolean=f"); try testCase(false, struct { boolean: bool = false }, .{ .boolean = false }, "boolean=f");
try testCase(struct { boolean: bool = false }, .{ .boolean = false }, "boolean=no"); try testCase(false, struct { boolean: bool = false }, .{ .boolean = false }, "boolean=no");
try testCase(struct { str_enum: ?enum { foo, bar } = null }, .{ .str_enum = null }, ""); try testCase(false, struct { str_enum: ?enum { foo, bar } = null }, .{ .str_enum = null }, "");
try testCase(struct { str_enum: ?enum { foo, bar } = null }, .{ .str_enum = .foo }, "str_enum=foo"); try testCase(false, struct { str_enum: ?enum { foo, bar } = null }, .{ .str_enum = .foo }, "str_enum=foo");
try testCase(struct { str_enum: ?enum { foo, bar } = null }, .{ .str_enum = .bar }, "str_enum=bar"); try testCase(false, struct { str_enum: ?enum { foo, bar } = null }, .{ .str_enum = .bar }, "str_enum=bar");
try testCase(struct { str_enum: ?enum { foo, bar } = .foo }, .{ .str_enum = .foo }, ""); try testCase(false, struct { str_enum: ?enum { foo, bar } = .foo }, .{ .str_enum = .foo }, "");
try testCase(struct { str_enum: ?enum { foo, bar } = .foo }, .{ .str_enum = null }, "str_enum"); try testCase(false, struct { str_enum: ?enum { foo, bar } = .foo }, .{ .str_enum = null }, "str_enum");
try testCase(struct { n1: usize = 5, n2: usize = 5 }, .{ .n1 = 1, .n2 = 2 }, "n1=1&n2=2"); try testCase(false, struct { n1: usize = 5, n2: usize = 5 }, .{ .n1 = 1, .n2 = 2 }, "n1=1&n2=2");
try testCase(struct { n1: usize = 5, n2: usize = 5 }, .{ .n1 = 1, .n2 = 2 }, "n1=1&n2=2&"); try testCase(false, struct { n1: usize = 5, n2: usize = 5 }, .{ .n1 = 1, .n2 = 2 }, "n1=1&n2=2&");
try testCase(struct { n1: usize = 5, n2: usize = 5 }, .{ .n1 = 1, .n2 = 2 }, "n1=1&&n2=2&"); try testCase(false, struct { n1: usize = 5, n2: usize = 5 }, .{ .n1 = 1, .n2 = 2 }, "n1=1&&n2=2&");
try testCase(struct { str: ?[]const u8 = null }, .{ .str = null }, ""); try testCase(false, struct { str: ?[]const u8 = null }, .{ .str = null }, "");
try testCase(struct { str: ?[]const u8 = null }, .{ .str = null }, "str"); try testCase(false, struct { str: ?[]const u8 = null }, .{ .str = null }, "str");
try testCase(struct { str: ?[]const u8 = null }, .{ .str = null }, "str="); try testCase(false, struct { str: ?[]const u8 = null }, .{ .str = null }, "str=");
try testCase(struct { str: ?[]const u8 = null }, .{ .str = "foo" }, "str=foo"); try testCase(false, struct { str: ?[]const u8 = null }, .{ .str = "foo" }, "str=foo");
try testCase(struct { str: ?[]const u8 = "foo" }, .{ .str = "foo" }, "str=foo"); try testCase(false, struct { str: ?[]const u8 = "foo" }, .{ .str = "foo" }, "str=foo");
try testCase(struct { str: ?[]const u8 = "foo" }, .{ .str = "foo" }, ""); try testCase(false, struct { str: ?[]const u8 = "foo" }, .{ .str = "foo" }, "");
try testCase(struct { str: ?[]const u8 = "foo" }, .{ .str = null }, "str"); try testCase(false, struct { str: ?[]const u8 = "foo" }, .{ .str = null }, "str");
try testCase(struct { str: ?[]const u8 = "foo" }, .{ .str = null }, "str="); try testCase(false, struct { str: ?[]const u8 = "foo" }, .{ .str = null }, "str=");
const rand_uuid = comptime util.Uuid.parse("c1fb6578-4d0c-4eb9-9f67-d56da3ae6f5d") catch unreachable; const rand_uuid = comptime util.Uuid.parse("c1fb6578-4d0c-4eb9-9f67-d56da3ae6f5d") catch unreachable;
try testCase(struct { id: ?util.Uuid = null }, .{ .id = null }, ""); try testCase(false, struct { id: ?util.Uuid = null }, .{ .id = null }, "");
try testCase(struct { id: ?util.Uuid = null }, .{ .id = null }, "id="); try testCase(false, struct { id: ?util.Uuid = null }, .{ .id = null }, "id=");
try testCase(struct { id: ?util.Uuid = null }, .{ .id = null }, "id"); try testCase(false, struct { id: ?util.Uuid = null }, .{ .id = null }, "id");
try testCase(struct { id: ?util.Uuid = null }, .{ .id = rand_uuid }, "id=" ++ rand_uuid.toCharArray()); try testCase(false, struct { id: ?util.Uuid = null }, .{ .id = rand_uuid }, "id=" ++ rand_uuid.toCharArray());
try testCase(struct { id: ?util.Uuid = rand_uuid }, .{ .id = rand_uuid }, ""); try testCase(false, struct { id: ?util.Uuid = rand_uuid }, .{ .id = rand_uuid }, "");
try testCase(struct { id: ?util.Uuid = rand_uuid }, .{ .id = null }, "id="); try testCase(false, struct { id: ?util.Uuid = rand_uuid }, .{ .id = null }, "id=");
try testCase(struct { id: ?util.Uuid = rand_uuid }, .{ .id = null }, "id"); try testCase(false, struct { id: ?util.Uuid = rand_uuid }, .{ .id = null }, "id");
try testCase(struct { id: ?util.Uuid = rand_uuid }, .{ .id = rand_uuid }, "id=" ++ rand_uuid.toCharArray()); try testCase(false, struct { id: ?util.Uuid = rand_uuid }, .{ .id = rand_uuid }, "id=" ++ rand_uuid.toCharArray());
const SubStruct = struct { const SubStruct = struct {
sub: struct { sub: struct {
@ -302,9 +302,9 @@ test "parse" {
bar: usize = 2, bar: usize = 2,
} = .{}, } = .{},
}; };
try testCase(SubStruct, .{ .sub = .{ .foo = 1, .bar = 2 } }, ""); try testCase(false, SubStruct, .{ .sub = .{ .foo = 1, .bar = 2 } }, "");
try testCase(SubStruct, .{ .sub = .{ .foo = 3, .bar = 3 } }, "sub.foo=3&sub.bar=3"); try testCase(false, SubStruct, .{ .sub = .{ .foo = 3, .bar = 3 } }, "sub.foo=3&sub.bar=3");
try testCase(SubStruct, .{ .sub = .{ .foo = 3, .bar = 2 } }, "sub.foo=3"); try testCase(false, SubStruct, .{ .sub = .{ .foo = 3, .bar = 2 } }, "sub.foo=3");
// TODO: Semantics are ill-defined here. What happens if the substruct doesn't have // TODO: Semantics are ill-defined here. What happens if the substruct doesn't have
// default values? // default values?
@ -313,8 +313,8 @@ test "parse" {
// foo: usize = 1, // foo: usize = 1,
// } = null, // } = null,
// }; // };
// try testCase(SubStruct2, .{ .sub = null }, ""); // try testCase(false, SubStruct2, .{ .sub = null }, "");
// try testCase(SubStruct2, .{ .sub = null }, "sub="); // try testCase(false, SubStruct2, .{ .sub = null }, "sub=");
// TODO: also here (semantics are well defined it just breaks tests) // TODO: also here (semantics are well defined it just breaks tests)
// const SubUnion = struct { // const SubUnion = struct {
@ -323,21 +323,24 @@ test "parse" {
// bar: usize, // bar: usize,
// } = null, // } = null,
// }; // };
// try testCase(SubUnion, .{ .sub = null }, ""); // try testCase(false, SubUnion, .{ .sub = null }, "");
// try testCase(SubUnion, .{ .sub = null }, "sub="); // try testCase(false, SubUnion, .{ .sub = null }, "sub=");
const SubUnion2 = struct { const SubUnion2 = struct {
sub: ?struct { sub: ?union(enum) {
foo: usize, bar: struct {
val: union(enum) { foo: usize,
bar: []const u8, bar: []const u8,
},
baz: struct {
foo: usize,
baz: []const u8, baz: []const u8,
}, },
} = null, } = null,
}; };
try testCase(SubUnion2, .{ .sub = null }, ""); try testCase(false, SubUnion2, .{ .sub = null }, "");
try testCase(SubUnion2, .{ .sub = .{ .foo = 1, .val = .{ .bar = "abc" } } }, "sub.foo=1&sub.bar=abc"); try testCase(false, SubUnion2, .{ .sub = .{ .bar = .{ .foo = 1, .bar = "abc" } } }, "sub.foo=1&sub.bar=abc");
try testCase(SubUnion2, .{ .sub = .{ .foo = 1, .val = .{ .baz = "abc" } } }, "sub.foo=1&sub.baz=abc"); try testCase(false, SubUnion2, .{ .sub = .{ .baz = .{ .foo = 1, .baz = "abc" } } }, "sub.foo=1&sub.baz=abc");
} }
test "encodeStruct" { test "encodeStruct" {

View File

@ -1349,7 +1349,7 @@ test "template" {
try testCase("", .{}, ""); try testCase("", .{}, "");
try testCase("abcd", .{}, "abcd"); try testCase("abcd", .{}, "abcd");
try testCase("{.val}", .{ .val = 3 }, "3"); try testCase("{.val}", .{ .val = @as(usize, 3) }, "3");
try testCase("{#if .val}1{/if}", .{ .val = true }, "1"); try testCase("{#if .val}1{/if}", .{ .val = true }, "1");
try testCase("{#for .vals |$v|=} {$v} {=/for}", .{ .vals = [_]u8{ 1, 2, 3 } }, "123"); try testCase("{#for .vals |$v|=} {$v} {=/for}", .{ .vals = [_]u8{ 1, 2, 3 } }, "123");
try testCase("{#for .vals |$val|}{$val}{/for}", .{ .vals = [_]u8{ 1, 2, 3 } }, "123"); try testCase("{#for .vals |$val|}{$val}{/for}", .{ .vals = [_]u8{ 1, 2, 3 } }, "123");

View File

@ -266,7 +266,7 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime
} }
pub fn finish(self: *@This(), allocator: std.mem.Allocator) !Result { pub fn finish(self: *@This(), allocator: std.mem.Allocator) !Result {
return (try self.deserialize(allocator, Result, self.data, &.{})) orelse return (try self.deserialize(allocator, Result, self.data, &.{}, true)) orelse
if (std.meta.fields(Result).len == 0) if (std.meta.fields(Result).len == 0)
return .{} return .{}
else else
@ -290,6 +290,7 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime
comptime T: type, comptime T: type,
intermediary: anytype, intermediary: anytype,
comptime field_ref: FieldRef, comptime field_ref: FieldRef,
allow_default: bool,
) DeserializeError!?T { ) DeserializeError!?T {
if (comptime Context.options.isScalar(T)) { if (comptime Context.options.isScalar(T)) {
const val = @field(intermediary.static, util.comptimeJoin(".", field_ref)); const val = @field(intermediary.static, util.comptimeJoin(".", field_ref));
@ -304,7 +305,7 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime
var partial_match_found: bool = false; var partial_match_found: bool = false;
inline for (info.fields) |field| { inline for (info.fields) |field| {
const F = field.field_type; const F = field.field_type;
const maybe_value = self.deserialize(allocator, F, intermediary, field_ref) catch |err| switch (err) { const maybe_value = self.deserialize(allocator, F, intermediary, field_ref, false) catch |err| switch (err) {
error.MissingField => blk: { error.MissingField => blk: {
partial_match_found = true; partial_match_found = true;
break :blk @as(?F, null); break :blk @as(?F, null);
@ -333,7 +334,7 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime
inline for (info.fields) |field, i| { inline for (info.fields) |field, i| {
const F = field.field_type; const F = field.field_type;
const new_field_ref = field_ref ++ &[_][]const u8{field.name}; const new_field_ref = field_ref ++ &[_][]const u8{field.name};
const maybe_value = try self.deserialize(allocator, F, intermediary, new_field_ref); const maybe_value = try self.deserialize(allocator, F, intermediary, new_field_ref, false);
if (maybe_value) |v| { if (maybe_value) |v| {
@field(result, field.name) = v; @field(result, field.name) = v;
fields_alloced[i] = true; fields_alloced[i] = true;
@ -350,7 +351,7 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime
} }
if (any_missing and any_explicit) return error.MissingField; if (any_missing and any_explicit) return error.MissingField;
if (!any_explicit) { if (!any_explicit and !allow_default) {
inline for (info.fields) |field, i| { inline for (info.fields) |field, i| {
if (fields_alloced[i]) self.deserializeFree(allocator, @field(result, field.name)); if (fields_alloced[i]) self.deserializeFree(allocator, @field(result, field.name));
} }
@ -369,7 +370,7 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime
var count: usize = 0; var count: usize = 0;
errdefer for (result[0..count]) |res| util.deepFree(allocator, res); errdefer for (result[0..count]) |res| util.deepFree(allocator, res);
for (data.items) |sub, i| { for (data.items) |sub, i| {
result[i] = (try self.deserialize(allocator, info.child, sub, &.{})) orelse return error.SparseSlice; result[i] = (try self.deserialize(allocator, info.child, sub, &.{}, false)) orelse return error.SparseSlice;
} }
return result; return result;
@ -378,7 +379,7 @@ pub fn DeserializerContext(comptime Result: type, comptime From: type, comptime
}, },
// Specifically non-scalar optionals // Specifically non-scalar optionals
.Optional => |info| return try self.deserialize(allocator, info.child, intermediary, field_ref), .Optional => |info| return try self.deserialize(allocator, info.child, intermediary, field_ref, allow_default),
else => @compileError("Unsupported type"), else => @compileError("Unsupported type"),
} }
@ -406,7 +407,8 @@ test "Deserializer" {
{ {
const T = struct { foo: []const u8, bar: bool }; const T = struct { foo: []const u8, bar: bool };
var ds = Deserializer(T){}; var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) };
defer ds.deinit();
try ds.setSerializedField("foo", "123"); try ds.setSerializedField("foo", "123");
try ds.setSerializedField("bar", "true"); try ds.setSerializedField("bar", "true");
@ -419,7 +421,8 @@ test "Deserializer" {
{ {
const T = struct { foo: []const u8, bar: bool }; const T = struct { foo: []const u8, bar: bool };
var ds = Deserializer(T){}; var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) };
defer ds.deinit();
try std.testing.expectError(error.UnknownField, ds.setSerializedField("baz", "123")); try std.testing.expectError(error.UnknownField, ds.setSerializedField("baz", "123"));
} }
@ -429,7 +432,8 @@ test "Deserializer" {
foo: struct { bar: bool, baz: bool }, foo: struct { bar: bool, baz: bool },
}; };
var ds = Deserializer(T){}; var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) };
defer ds.deinit();
try ds.setSerializedField("foo.bar", "true"); try ds.setSerializedField("foo.bar", "true");
try ds.setSerializedField("foo.baz", "true"); try ds.setSerializedField("foo.baz", "true");
@ -438,29 +442,45 @@ test "Deserializer" {
try util.testing.expectDeepEqual(T{ .foo = .{ .bar = true, .baz = true } }, val); try util.testing.expectDeepEqual(T{ .foo = .{ .bar = true, .baz = true } }, val);
} }
// Union embedding // Union behavior
{ {
const T = struct { const T = struct {
foo: union(enum) { bar: bool, baz: bool }, foo: union(enum) {
bar: struct {
bar: bool,
},
baz: struct {
baz: bool,
},
},
}; };
var ds = Deserializer(T){}; var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) };
try ds.setSerializedField("bar", "true"); defer ds.deinit();
try ds.setSerializedField("foo.bar", "true");
const val = try ds.finish(std.testing.allocator); const val = try ds.finish(std.testing.allocator);
defer ds.finishFree(std.testing.allocator, val); defer ds.finishFree(std.testing.allocator, val);
try util.testing.expectDeepEqual(T{ .foo = .{ .bar = true } }, val); try util.testing.expectDeepEqual(T{ .foo = .{ .bar = .{ .bar = true } } }, val);
} }
// Returns error if multiple union fields specified // Returns error if multiple union fields specified
{ {
const T = struct { const T = struct {
foo: union(enum) { bar: bool, baz: bool }, foo: union(enum) {
bar: struct {
bar: bool,
},
baz: struct {
baz: bool,
},
},
}; };
var ds = Deserializer(T){}; var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) };
try ds.setSerializedField("bar", "true"); defer ds.deinit();
try ds.setSerializedField("baz", "true"); try ds.setSerializedField("foo.bar", "true");
try ds.setSerializedField("foo.baz", "true");
try std.testing.expectError(error.DuplicateUnionMember, ds.finish(std.testing.allocator)); try std.testing.expectError(error.DuplicateUnionMember, ds.finish(std.testing.allocator));
} }
@ -469,7 +489,8 @@ test "Deserializer" {
{ {
const T = struct { foo: []const u8 = "123", bar: bool = true }; const T = struct { foo: []const u8 = "123", bar: bool = true };
var ds = Deserializer(T){}; var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) };
defer ds.deinit();
const val = try ds.finish(std.testing.allocator); const val = try ds.finish(std.testing.allocator);
defer ds.finishFree(std.testing.allocator, val); defer ds.finishFree(std.testing.allocator, val);
@ -480,7 +501,8 @@ test "Deserializer" {
{ {
const T = struct { foo: []const u8, bar: bool }; const T = struct { foo: []const u8, bar: bool };
var ds = Deserializer(T){}; var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) };
defer ds.deinit();
try ds.setSerializedField("foo", "123"); try ds.setSerializedField("foo", "123");
try std.testing.expectError(error.MissingField, ds.finish(std.testing.allocator)); try std.testing.expectError(error.MissingField, ds.finish(std.testing.allocator));
@ -493,7 +515,8 @@ test "Deserializer" {
qux: ?union(enum) { quux: usize } = null, qux: ?union(enum) { quux: usize } = null,
}; };
var ds = Deserializer(T){}; var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) };
defer ds.deinit();
const val = try ds.finish(std.testing.allocator); const val = try ds.finish(std.testing.allocator);
defer ds.finishFree(std.testing.allocator, val); defer ds.finishFree(std.testing.allocator, val);
@ -506,9 +529,10 @@ test "Deserializer" {
qux: ?union(enum) { quux: usize } = null, qux: ?union(enum) { quux: usize } = null,
}; };
var ds = Deserializer(T){}; var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) };
defer ds.deinit();
try ds.setSerializedField("foo.baz", "3"); try ds.setSerializedField("foo.baz", "3");
try ds.setSerializedField("quux", "3"); try ds.setSerializedField("qux", "3");
const val = try ds.finish(std.testing.allocator); const val = try ds.finish(std.testing.allocator);
defer ds.finishFree(std.testing.allocator, val); defer ds.finishFree(std.testing.allocator, val);
@ -521,9 +545,10 @@ test "Deserializer" {
qux: ?union(enum) { quux: usize } = null, qux: ?union(enum) { quux: usize } = null,
}; };
var ds = Deserializer(T){}; var ds = Deserializer(T){ .arena = std.heap.ArenaAllocator.init(std.testing.allocator) };
defer ds.deinit();
try ds.setSerializedField("foo.bar", "3"); try ds.setSerializedField("foo.bar", "3");
try ds.setSerializedField("quux", "3"); try ds.setSerializedField("qux", "3");
try std.testing.expectError(error.MissingField, ds.finish(std.testing.allocator)); try std.testing.expectError(error.MissingField, ds.finish(std.testing.allocator));
} }

View File

@ -39,7 +39,9 @@ fn connectAndLogin(
var conn = try api_source.connectUnauthorized(host, alloc); var conn = try api_source.connectUnauthorized(host, alloc);
defer conn.close(); defer conn.close();
return try util.deepClone(alloc, try conn.login(username, password)); const result = try conn.login(username, password);
defer util.deepFree(conn.allocator, result);
return try util.deepClone(alloc, result);
} }
test "login as root" { test "login as root" {
@ -54,6 +56,7 @@ test "login as root" {
var conn = try src.connectToken(admin_host, login.token, alloc); var conn = try src.connectToken(admin_host, login.token, alloc);
defer conn.close(); defer conn.close();
const auth = try conn.verifyAuthorization(); const auth = try conn.verifyAuthorization();
defer util.deepFree(conn.allocator, auth);
try std.testing.expectEqual(login.user_id, auth.id); try std.testing.expectEqual(login.user_id, auth.id);
try std.testing.expectEqualStrings(root_user, auth.username); try std.testing.expectEqualStrings(root_user, auth.username);
@ -73,7 +76,8 @@ test "create community" {
defer conn.close(); defer conn.close();
const host = "fedi.example.com"; const host = "fedi.example.com";
const community = try conn.createCommunity("https://" ++ host); const community = try conn.createCommunity("https://" ++ host, null);
defer util.deepFree(alloc, community);
try std.testing.expectEqual(api.Community.Scheme.https, community.scheme); try std.testing.expectEqual(api.Community.Scheme.https, community.scheme);
try std.testing.expectEqual(api.Community.Kind.local, community.kind); try std.testing.expectEqual(api.Community.Kind.local, community.kind);
@ -83,7 +87,10 @@ test "create community" {
} }
test "create community and transfer to new owner" { test "create community and transfer to new owner" {
const alloc = std.testing.allocator; //const alloc = std.testing.allocator;
var gpa = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 16 }){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
var db = try makeDb(alloc); var db = try makeDb(alloc);
defer db.deinit(); defer db.deinit();
var src = try ApiSource.init(&db); var src = try ApiSource.init(&db);
@ -96,8 +103,10 @@ test "create community and transfer to new owner" {
var conn = try src.connectToken(admin_host, root_login.token, alloc); var conn = try src.connectToken(admin_host, root_login.token, alloc);
defer conn.close(); defer conn.close();
const community = try conn.createCommunity("https://" ++ host); const community = try conn.createCommunity("https://" ++ host, null);
defer util.deepFree(conn.allocator, community);
const invite = try conn.createInvite(.{ .to_community = community.id, .kind = .community_owner }); const invite = try conn.createInvite(.{ .to_community = community.id, .kind = .community_owner });
defer util.deepFree(conn.allocator, invite);
break :blk try util.deepClone(alloc, invite); break :blk try util.deepClone(alloc, invite);
}; };
defer util.deepFree(alloc, invite); defer util.deepFree(alloc, invite);
@ -108,7 +117,7 @@ test "create community and transfer to new owner" {
var conn = try src.connectUnauthorized(host, alloc); var conn = try src.connectUnauthorized(host, alloc);
defer conn.close(); defer conn.close();
_ = try conn.register(username, password, .{ .invite_code = invite.code }); util.deepFree(conn.allocator, try conn.register(username, password, .{ .invite_code = invite.code }));
} }
const login = try connectAndLogin(&src, host, username, password, alloc); const login = try connectAndLogin(&src, host, username, password, alloc);
@ -118,6 +127,7 @@ test "create community and transfer to new owner" {
defer conn.close(); defer conn.close();
const auth = try conn.verifyAuthorization(); const auth = try conn.verifyAuthorization();
defer util.deepFree(conn.allocator, auth);
try std.testing.expectEqual(login.user_id, auth.id); try std.testing.expectEqual(login.user_id, auth.id);
try std.testing.expectEqualStrings(username, auth.username); try std.testing.expectEqualStrings(username, auth.username);