mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 22:16:08 +00:00
commit
072fc713c0
5
.github/workflows/CI.yml
vendored
5
.github/workflows/CI.yml
vendored
@ -42,6 +42,11 @@ jobs:
|
||||
- name: Run Tests
|
||||
run: zig build test
|
||||
|
||||
- name: Run App Tests
|
||||
with:
|
||||
path: demo
|
||||
run: zig build jetzig:test
|
||||
|
||||
- name: Build artifacts
|
||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
run: |
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ zig-cache/
|
||||
*.core
|
||||
static/
|
||||
.jetzig
|
||||
.zig-cache/
|
||||
|
@ -40,9 +40,9 @@ If you are interested in _Jetzig_ you will probably find these tools interesting
|
||||
* :white_check_mark: Background jobs.
|
||||
* :white_check_mark: General-purpose cache.
|
||||
* :white_check_mark: Development server auto-reload.
|
||||
* :white_check_mark: Testing helpers for testing HTTP requests/responses.
|
||||
* :white_check_mark: Custom/non-conventional routes.
|
||||
* :x: Environment configurations (development/production/etc.)
|
||||
* :x: Custom/non-conventional routes.
|
||||
* :x: Testing helpers for testing HTTP requests/responses.
|
||||
* :x: Database integration.
|
||||
* :x: Email receipt (via SendGrid/AWS SES/etc.)
|
||||
|
||||
|
38
build.zig
38
build.zig
@ -33,7 +33,6 @@ pub fn build(b: *std.Build) !void {
|
||||
const mime_module = try GenerateMimeTypes.generateMimeModule(b);
|
||||
|
||||
const zig_args_dep = b.dependency("args", .{ .target = target, .optimize = optimize });
|
||||
|
||||
const jetzig_module = b.addModule("jetzig", .{ .root_source_file = b.path("src/jetzig.zig") });
|
||||
jetzig_module.addImport("mime_types", mime_module);
|
||||
lib.root_module.addImport("jetzig", jetzig_module);
|
||||
@ -165,8 +164,10 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
mailers_path,
|
||||
);
|
||||
try generate_routes.generateRoutes();
|
||||
const write_files = b.addWriteFiles();
|
||||
const routes_file = write_files.add("routes.zig", generate_routes.buffer.items);
|
||||
const routes_write_files = b.addWriteFiles();
|
||||
const routes_file = routes_write_files.add("routes.zig", generate_routes.buffer.items);
|
||||
const tests_write_files = b.addWriteFiles();
|
||||
const tests_file = tests_write_files.add("tests.zig", generate_routes.buffer.items);
|
||||
const routes_module = b.createModule(.{ .root_source_file = routes_file });
|
||||
|
||||
var src_dir = try std.fs.openDirAbsolute(b.pathFromRoot("src"), .{ .iterate = true });
|
||||
@ -183,7 +184,8 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
const relpath = try std.fs.path.join(b.allocator, &[_][]const u8{ "src", entry.path });
|
||||
defer b.allocator.free(relpath);
|
||||
|
||||
_ = write_files.add(relpath, src_data);
|
||||
_ = routes_write_files.add(relpath, src_data);
|
||||
_ = tests_write_files.add(relpath, src_data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,12 +198,6 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
|
||||
exe.root_module.addImport("routes", routes_module);
|
||||
|
||||
var it = exe.root_module.import_table.iterator();
|
||||
while (it.next()) |import| {
|
||||
routes_module.addImport(import.key_ptr.*, import.value_ptr.*);
|
||||
exe_static_routes.root_module.addImport(import.key_ptr.*, import.value_ptr.*);
|
||||
}
|
||||
|
||||
exe_static_routes.root_module.addImport("routes", routes_module);
|
||||
exe_static_routes.root_module.addImport("jetzig", jetzig_module);
|
||||
exe_static_routes.root_module.addImport("zmpl", zmpl_module);
|
||||
@ -210,6 +206,28 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
const run_static_routes_cmd = b.addRunArtifact(exe_static_routes);
|
||||
run_static_routes_cmd.expectExitCode(0);
|
||||
exe.step.dependOn(&run_static_routes_cmd.step);
|
||||
|
||||
const exe_unit_tests = b.addTest(.{
|
||||
.root_source_file = tests_file,
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.test_runner = jetzig_dep.path("src/test_runner.zig"),
|
||||
});
|
||||
exe_unit_tests.root_module.addImport("jetzig", jetzig_module);
|
||||
exe_unit_tests.root_module.addImport("__jetzig_project", &exe.root_module);
|
||||
|
||||
var it = exe.root_module.import_table.iterator();
|
||||
while (it.next()) |import| {
|
||||
routes_module.addImport(import.key_ptr.*, import.value_ptr.*);
|
||||
exe_static_routes.root_module.addImport(import.key_ptr.*, import.value_ptr.*);
|
||||
exe_unit_tests.root_module.addImport(import.key_ptr.*, import.value_ptr.*);
|
||||
}
|
||||
|
||||
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
|
||||
|
||||
const test_step = b.step("jetzig:test", "Run tests");
|
||||
test_step.dependOn(&run_exe_unit_tests.step);
|
||||
exe_unit_tests.root_module.addImport("routes", routes_module);
|
||||
}
|
||||
|
||||
fn generateMarkdownFragments(b: *std.Build) ![]const u8 {
|
||||
|
@ -7,8 +7,8 @@
|
||||
.hash = "12203b56c2e17a2fd62ea3d3d9be466f43921a3aef88b381cf58f41251815205fdb5",
|
||||
},
|
||||
.zmpl = .{
|
||||
.url = "https://github.com/jetzig-framework/zmpl/archive/aa4d8ad5b63976d96e3b2c187f5b0b2c693905a1.tar.gz",
|
||||
.hash = "1220b6dfedaf6ad2b464ebcec2aafdc01ba593a07a53885033d56c50a5a04334b517",
|
||||
.url = "https://github.com/jetzig-framework/zmpl/archive/7dac63a9470cf12a2cbaf8e6e3fc3aeb9ea27658.tar.gz",
|
||||
.hash = "1220215354adcea94d36d25c300e291981d3f48c543f7da6e48bb2793a5aed85e768",
|
||||
},
|
||||
.jetkv = .{
|
||||
.url = "https://github.com/jetzig-framework/jetkv/archive/78bcdcc6b0cbd3ca808685c64554a15701f13250.tar.gz",
|
||||
|
12
cli/cli.zig
12
cli/cli.zig
@ -5,6 +5,7 @@ const update = @import("commands/update.zig");
|
||||
const generate = @import("commands/generate.zig");
|
||||
const server = @import("commands/server.zig");
|
||||
const bundle = @import("commands/bundle.zig");
|
||||
const tests = @import("commands/tests.zig");
|
||||
|
||||
const Options = struct {
|
||||
help: bool = false,
|
||||
@ -21,6 +22,7 @@ const Options = struct {
|
||||
.generate = "Generate scaffolding",
|
||||
.server = "Run a development server",
|
||||
.bundle = "Create a deployment bundle",
|
||||
.@"test" = "Run app tests",
|
||||
.help = "Print help and exit",
|
||||
},
|
||||
};
|
||||
@ -32,9 +34,11 @@ const Verb = union(enum) {
|
||||
generate: generate.Options,
|
||||
server: server.Options,
|
||||
bundle: bundle.Options,
|
||||
@"test": tests.Options,
|
||||
g: generate.Options,
|
||||
s: server.Options,
|
||||
b: bundle.Options,
|
||||
t: tests.Options,
|
||||
};
|
||||
|
||||
/// Main entrypoint for `jetzig` executable. Parses command line args and generates a new
|
||||
@ -67,6 +71,7 @@ pub fn main() !void {
|
||||
\\ generate Generate scaffolding.
|
||||
\\ server Run a development server.
|
||||
\\ bundle Create a deployment bundle.
|
||||
\\ test Run app tests.
|
||||
\\
|
||||
\\ Pass --help to any command for more information, e.g. `jetzig init --help`
|
||||
\\
|
||||
@ -112,6 +117,13 @@ fn run(allocator: std.mem.Allocator, options: args.ParseArgsResult(Options, Verb
|
||||
options.positionals,
|
||||
.{ .help = options.options.help },
|
||||
),
|
||||
.t, .@"test" => |opts| tests.run(
|
||||
allocator,
|
||||
opts,
|
||||
writer,
|
||||
options.positionals,
|
||||
.{ .help = options.options.help },
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -68,6 +68,10 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he
|
||||
try writeTemplate(allocator, cwd, args[0], action);
|
||||
}
|
||||
|
||||
for (actions.items) |action| {
|
||||
try writeTest(allocator, writer, args[0], action);
|
||||
}
|
||||
|
||||
var dir = try cwd.openDir("src/app/views", .{});
|
||||
defer dir.close();
|
||||
|
||||
@ -141,6 +145,44 @@ fn writeAction(allocator: std.mem.Allocator, writer: anytype, action: Action) !v
|
||||
try writer.writeAll(function);
|
||||
}
|
||||
|
||||
// Write a view function to the output buffer.
|
||||
fn writeTest(allocator: std.mem.Allocator, writer: anytype, name: []const u8, action: Action) !void {
|
||||
const action_upper = try std.ascii.allocUpperString(allocator, @tagName(action.method));
|
||||
defer allocator.free(action_upper);
|
||||
|
||||
const test_body = try std.fmt.allocPrint(
|
||||
allocator,
|
||||
\\
|
||||
\\test "{s}" {{
|
||||
\\ var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
\\ defer app.deinit();
|
||||
\\
|
||||
\\ const response = try app.request(.{s}, "/{s}{s}", .{{}});
|
||||
\\ try response.expectStatus({s});
|
||||
\\}}
|
||||
\\
|
||||
,
|
||||
.{
|
||||
@tagName(action.method),
|
||||
switch (action.method) {
|
||||
.index, .get => "GET",
|
||||
.put, .patch, .delete, .post => action_upper,
|
||||
},
|
||||
name,
|
||||
switch (action.method) {
|
||||
.index, .post => "",
|
||||
.get, .put, .patch, .delete => "/example-id",
|
||||
},
|
||||
switch (action.method) {
|
||||
.index, .get => ".ok",
|
||||
.post => ".created",
|
||||
.put, .patch, .delete => ".ok",
|
||||
},
|
||||
},
|
||||
);
|
||||
defer allocator.free(test_body);
|
||||
try writer.writeAll(test_body);
|
||||
}
|
||||
// Output static params example. Only invoked if one or more static routes are created.
|
||||
fn writeStaticParams(allocator: std.mem.Allocator, actions: []Action, writer: anytype) !void {
|
||||
try writer.writeAll(
|
||||
|
43
cli/commands/tests.zig
Normal file
43
cli/commands/tests.zig
Normal file
@ -0,0 +1,43 @@
|
||||
const std = @import("std");
|
||||
const util = @import("../util.zig");
|
||||
|
||||
/// Command line options for the `generate` command.
|
||||
pub const Options = struct {
|
||||
@"fail-fast": bool = false,
|
||||
|
||||
pub const shorthands = .{
|
||||
.f = "fail-fast",
|
||||
};
|
||||
|
||||
pub const meta = .{
|
||||
.usage_summary = "[--fail-fast]",
|
||||
.full_text =
|
||||
\\Run app tests.
|
||||
\\
|
||||
\\Execute all tests found in `src/main.zig`
|
||||
\\
|
||||
,
|
||||
.option_docs = .{
|
||||
.path = "Set the output path relative to the current directory (default: current directory)",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/// Run the job generator. Create a job in `src/app/jobs/`
|
||||
pub fn run(
|
||||
allocator: std.mem.Allocator,
|
||||
options: Options,
|
||||
writer: anytype,
|
||||
positionals: [][]const u8,
|
||||
other_options: struct { help: bool },
|
||||
) !void {
|
||||
_ = options;
|
||||
_ = writer;
|
||||
_ = positionals;
|
||||
_ = other_options;
|
||||
try util.execCommand(allocator, &.{
|
||||
"zig",
|
||||
"build",
|
||||
"jetzig:test",
|
||||
});
|
||||
}
|
27
cli/util.zig
27
cli/util.zig
@ -96,6 +96,33 @@ pub fn isCamelCase(input: []const u8) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn execCommand(allocator: std.mem.Allocator, argv: []const []const u8) !void {
|
||||
std.debug.print("[exec]", .{});
|
||||
for (argv) |arg| std.debug.print(" {s}", .{arg});
|
||||
std.debug.print("\n", .{});
|
||||
|
||||
if (std.process.can_execv) {
|
||||
return std.process.execv(allocator, argv);
|
||||
} else {
|
||||
var dir = try detectJetzigProjectDir();
|
||||
defer dir.close();
|
||||
const path = try dir.realpathAlloc(allocator, ".");
|
||||
defer allocator.free(path);
|
||||
try runCommandStreaming(allocator, path, argv);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn runCommandStreaming(allocator: std.mem.Allocator, install_path: []const u8, argv: []const []const u8) !void {
|
||||
var child = std.process.Child.init(argv, allocator);
|
||||
child.stdin_behavior = .Ignore;
|
||||
child.stdout_behavior = .Inherit;
|
||||
child.stderr_behavior = .Inherit;
|
||||
child.cwd = install_path;
|
||||
|
||||
try child.spawn();
|
||||
_ = try child.wait();
|
||||
}
|
||||
|
||||
/// Runs a command as a child process and verifies successful exit code.
|
||||
pub fn runCommand(allocator: std.mem.Allocator, install_path: []const u8, argv: []const []const u8) !void {
|
||||
const result = try std.process.Child.run(.{ .allocator = allocator, .argv = argv, .cwd = install_path });
|
||||
|
@ -22,3 +22,15 @@ pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
test "index" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
_ = try app.request(.POST, "/cache", .{ .params = .{ .message = "test message" } });
|
||||
|
||||
const response = try app.request(.GET, "/cache", .{});
|
||||
try response.expectBodyContains(
|
||||
\\ <span>Cached value: test message</span>
|
||||
);
|
||||
}
|
||||
|
@ -17,12 +17,9 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
|
||||
const params = try request.params();
|
||||
|
||||
const count = if (params.get("iguanas")) |param|
|
||||
try std.fmt.parseInt(usize, param.string.value, 10)
|
||||
else
|
||||
10;
|
||||
const count = params.getT(.integer, "iguanas") orelse 10;
|
||||
|
||||
const iguanas_slice = try iguanas.iguanas(request.allocator, count);
|
||||
const iguanas_slice = try iguanas.iguanas(request.allocator, @intCast(count));
|
||||
|
||||
for (iguanas_slice) |iguana| {
|
||||
try root.append(data.string(iguana));
|
||||
@ -30,3 +27,19 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
test "index" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.GET, "/iguanas", .{ .json = .{ .iguanas = 10 } });
|
||||
try response.expectJson(".1", "iguana");
|
||||
try response.expectJson(".2", "iguana");
|
||||
try response.expectJson(".3", "iguana");
|
||||
try response.expectJson(".4", "iguana");
|
||||
try response.expectJson(".5", "iguana");
|
||||
try response.expectJson(".6", "iguana");
|
||||
try response.expectJson(".7", "iguana");
|
||||
try response.expectJson(".8", "iguana");
|
||||
try response.expectJson(".9", "iguana");
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
/// This example demonstrates usage of Jetzig's KV store.
|
||||
@ -29,3 +30,18 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
test "index" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response1 = try app.request(.GET, "/kvstore.json", .{});
|
||||
try response1.expectStatus(.ok);
|
||||
try response1.expectJson(".stored_string", null);
|
||||
|
||||
const response2 = try app.request(.GET, "/kvstore.json", .{});
|
||||
try response2.expectJson(".stored_string", "example-value");
|
||||
try response2.expectJson(".popped", "hello");
|
||||
try (try app.request(.GET, "/kvstore.json", .{})).expectJson(".popped", "goodbye");
|
||||
try (try app.request(.GET, "/kvstore.json", .{})).expectJson(".popped", "hello again");
|
||||
}
|
||||
|
@ -7,3 +7,10 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
_ = data;
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
test "index" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
const response = try app.request(.GET, "/markdown", .{});
|
||||
try response.expectBodyContains("You can still use <i>Zmpl</i> references, modes, and partials.");
|
||||
}
|
||||
|
@ -40,3 +40,11 @@ fn randomQuote(allocator: std.mem.Allocator) !Quote {
|
||||
const quotes = try std.json.parseFromSlice([]Quote, allocator, json, .{});
|
||||
return quotes.value[std.crypto.random.intRangeLessThan(usize, 0, quotes.value.len)];
|
||||
}
|
||||
|
||||
test "get" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.GET, "/quotes/initial", .{});
|
||||
try response.expectBodyContains("If you can dream it, you can achieve it.");
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
const importedFunction = @import("../lib/example.zig").exampleFunction;
|
||||
@ -18,3 +19,65 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
fn customFunction(a: i32, b: i32, c: i32) i32 {
|
||||
return a + b + c;
|
||||
}
|
||||
|
||||
test "404 Not Found" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.GET, "/foobar", .{});
|
||||
try response.expectStatus(.not_found);
|
||||
}
|
||||
|
||||
test "200 OK" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.GET, "/", .{});
|
||||
try response.expectStatus(.ok);
|
||||
}
|
||||
|
||||
test "response body" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.GET, "/", .{});
|
||||
try response.expectBodyContains("Welcome to Jetzig!");
|
||||
}
|
||||
|
||||
test "header" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.GET, "/", .{});
|
||||
try response.expectHeader("content-type", "text/html");
|
||||
}
|
||||
|
||||
test "json" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.GET, "/.json", .{});
|
||||
try response.expectJson(".message", "Welcome to Jetzig!");
|
||||
try response.expectJson(".custom_number", 600);
|
||||
}
|
||||
|
||||
test "json from header" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(
|
||||
.GET,
|
||||
"/",
|
||||
.{ .headers = &.{.{ .name = "accept", .value = "application/json" }} },
|
||||
);
|
||||
try response.expectJson(".message", "Welcome to Jetzig!");
|
||||
try response.expectJson(".custom_number", 600);
|
||||
}
|
||||
|
||||
test "public file" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(.GET, "/jetzig.png", .{});
|
||||
try response.expectStatus(.ok);
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
///
|
||||
/// ```console
|
||||
/// curl -H "Accept: application/json" \
|
||||
/// --data-bin '{"foo":"hello", "bar":"goodbye"}' \
|
||||
/// --data-binary '{"foo":"hello", "bar":"goodbye"}' \
|
||||
/// --request GET \
|
||||
/// 'http://localhost:8080/static'
|
||||
/// ```
|
||||
@ -43,18 +43,18 @@ pub const static_params = .{
|
||||
};
|
||||
|
||||
pub fn index(request: *jetzig.StaticRequest, data: *jetzig.Data) !jetzig.View {
|
||||
var root = try data.object();
|
||||
var root = try data.root(.object);
|
||||
|
||||
const params = try request.params();
|
||||
|
||||
if (params.get("foo")) |foo| try root.put("foo", foo);
|
||||
if (params.get("bar")) |foo| try root.put("bar", foo);
|
||||
try root.put("foo", params.get("foo"));
|
||||
try root.put("bar", params.get("bar"));
|
||||
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
pub fn get(id: []const u8, request: *jetzig.StaticRequest, data: *jetzig.Data) !jetzig.View {
|
||||
var root = try data.object();
|
||||
var root = try data.root(.object);
|
||||
|
||||
const params = try request.params();
|
||||
|
||||
@ -67,3 +67,59 @@ pub fn get(id: []const u8, request: *jetzig.StaticRequest, data: *jetzig.Data) !
|
||||
|
||||
return request.render(.created);
|
||||
}
|
||||
|
||||
test "index json" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(
|
||||
.GET,
|
||||
"/static.json",
|
||||
.{ .json = .{ .foo = "hello", .bar = "goodbye" } },
|
||||
);
|
||||
|
||||
try response.expectStatus(.ok);
|
||||
try response.expectJson(".foo", "hello");
|
||||
try response.expectJson(".bar", "goodbye");
|
||||
}
|
||||
|
||||
test "get json" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(
|
||||
.GET,
|
||||
"/static/1.json",
|
||||
.{ .json = .{ .foo = "hi", .bar = "bye" } },
|
||||
);
|
||||
|
||||
try response.expectStatus(.ok);
|
||||
try response.expectJson(".id", "id is '1'");
|
||||
}
|
||||
|
||||
test "index html" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(
|
||||
.GET,
|
||||
"/static.html",
|
||||
.{ .params = .{ .foo = "hello", .bar = "goodbye" } },
|
||||
);
|
||||
|
||||
try response.expectStatus(.ok);
|
||||
try response.expectBodyContains("hello");
|
||||
}
|
||||
|
||||
test "get html" {
|
||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||
defer app.deinit();
|
||||
|
||||
const response = try app.request(
|
||||
.GET,
|
||||
"/static/1.html",
|
||||
.{ .params = .{ .foo = "hi", .bar = "bye" } },
|
||||
);
|
||||
|
||||
try response.expectStatus(.ok);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ mailers_path: []const u8,
|
||||
buffer: std.ArrayList(u8),
|
||||
dynamic_routes: std.ArrayList(Function),
|
||||
static_routes: std.ArrayList(Function),
|
||||
module_paths: std.ArrayList([]const u8),
|
||||
data: *jetzig.data.Data,
|
||||
|
||||
const Routes = @This();
|
||||
@ -107,6 +108,7 @@ pub fn init(
|
||||
.buffer = std.ArrayList(u8).init(allocator),
|
||||
.static_routes = std.ArrayList(Function).init(allocator),
|
||||
.dynamic_routes = std.ArrayList(Function).init(allocator),
|
||||
.module_paths = std.ArrayList([]const u8).init(allocator),
|
||||
.data = data,
|
||||
};
|
||||
}
|
||||
@ -157,6 +159,24 @@ pub fn generateRoutes(self: *Routes) !void {
|
||||
\\
|
||||
);
|
||||
|
||||
try writer.writeAll(
|
||||
\\test {
|
||||
\\
|
||||
);
|
||||
|
||||
for (self.module_paths.items) |module_path| {
|
||||
try writer.print(
|
||||
\\ _ = @import("{s}");
|
||||
\\
|
||||
, .{module_path});
|
||||
}
|
||||
|
||||
try writer.writeAll(
|
||||
\\ @import("std").testing.refAllDeclsRecursive(@This());
|
||||
\\}
|
||||
\\
|
||||
);
|
||||
|
||||
// std.debug.print("routes.zig\n{s}\n", .{self.buffer.items});
|
||||
}
|
||||
|
||||
@ -272,6 +292,7 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
|
||||
);
|
||||
|
||||
std.mem.replaceScalar(u8, module_path, '\\', '/');
|
||||
try self.module_paths.append(try self.allocator.dupe(u8, module_path));
|
||||
|
||||
const output = try std.fmt.allocPrint(self.allocator, output_template, .{
|
||||
full_name,
|
||||
@ -325,7 +346,7 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
|
||||
const decl = self.ast.simpleVarDecl(asNodeIndex(index));
|
||||
if (self.isStaticParamsDecl(decl)) {
|
||||
self.data.reset();
|
||||
const params = try self.data.object();
|
||||
const params = try self.data.root(.object);
|
||||
try self.parseStaticParamsDecl(decl, params);
|
||||
static_params = self.data.value;
|
||||
}
|
||||
@ -340,7 +361,7 @@ fn generateRoutesForView(self: *Routes, dir: std.fs.Dir, path: []const u8) !Rout
|
||||
|
||||
if (static_params) |capture| {
|
||||
if (capture.get(static_route.name)) |params| {
|
||||
for (params.array.array.items) |item| { // XXX: Use public interface for Data.Array here ?
|
||||
for (params.items(.array)) |item| {
|
||||
const json = try item.toJson();
|
||||
var encoded_buf = std.ArrayList(u8).init(self.allocator);
|
||||
defer encoded_buf.deinit();
|
||||
@ -399,19 +420,19 @@ fn parseArray(self: *Routes, node: std.zig.Ast.Node.Index, params: *jetzig.data.
|
||||
const main_token = self.ast.nodes.items(.main_token)[node];
|
||||
const field_name = self.ast.tokenSlice(main_token - 3);
|
||||
|
||||
const params_array = try self.data.createArray();
|
||||
const params_array = try self.data.array();
|
||||
try params.put(field_name, params_array);
|
||||
|
||||
for (array.ast.elements) |element| {
|
||||
const elem = self.ast.nodes.items(.tag)[element];
|
||||
switch (elem) {
|
||||
.struct_init_dot, .struct_init_dot_two, .struct_init_dot_two_comma => {
|
||||
const route_params = try self.data.createObject();
|
||||
const route_params = try self.data.object();
|
||||
try params_array.append(route_params);
|
||||
try self.parseStruct(element, route_params);
|
||||
},
|
||||
.array_init_dot, .array_init_dot_two, .array_init_dot_comma, .array_init_dot_two_comma => {
|
||||
const route_params = try self.data.createObject();
|
||||
const route_params = try self.data.object();
|
||||
try params_array.append(route_params);
|
||||
try self.parseField(element, route_params);
|
||||
},
|
||||
@ -420,7 +441,7 @@ fn parseArray(self: *Routes, node: std.zig.Ast.Node.Index, params: *jetzig.data.
|
||||
const string_value = self.ast.tokenSlice(string_token);
|
||||
|
||||
// Strip quotes: `"foo"` -> `foo`
|
||||
try params_array.append(self.data.string(string_value[1 .. string_value.len - 1]));
|
||||
try params_array.append(string_value[1 .. string_value.len - 1]);
|
||||
},
|
||||
.number_literal => {
|
||||
const number_token = self.ast.nodes.items(.main_token)[element];
|
||||
@ -445,7 +466,7 @@ fn parseField(self: *Routes, node: std.zig.Ast.Node.Index, params: *jetzig.data.
|
||||
try self.parseArray(node, params);
|
||||
},
|
||||
.struct_init_dot, .struct_init_dot_two, .struct_init_dot_two_comma => {
|
||||
const nested_params = try self.data.createObject();
|
||||
const nested_params = try self.data.object();
|
||||
const main_token = self.ast.nodes.items(.main_token)[node];
|
||||
const field_name = self.ast.tokenSlice(main_token - 3);
|
||||
try params.put(field_name, nested_params);
|
||||
@ -460,7 +481,7 @@ fn parseField(self: *Routes, node: std.zig.Ast.Node.Index, params: *jetzig.data.
|
||||
try params.put(
|
||||
field_name,
|
||||
// strip outer quotes
|
||||
self.data.string(field_value[1 .. field_value.len - 1]),
|
||||
field_value[1 .. field_value.len - 1],
|
||||
);
|
||||
},
|
||||
.number_literal => {
|
||||
|
@ -16,6 +16,7 @@ pub const markdown = @import("jetzig/markdown.zig");
|
||||
pub const jobs = @import("jetzig/jobs.zig");
|
||||
pub const mail = @import("jetzig/mail.zig");
|
||||
pub const kv = @import("jetzig/kv.zig");
|
||||
pub const testing = @import("jetzig/testing.zig");
|
||||
|
||||
/// The primary interface for a Jetzig application. Create an `App` in your application's
|
||||
/// `src/main.zig` and call `start` to launch the application.
|
||||
|
@ -29,28 +29,8 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void {
|
||||
defer mime_map.deinit();
|
||||
try mime_map.build();
|
||||
|
||||
var routes = std.ArrayList(*jetzig.views.Route).init(self.allocator);
|
||||
|
||||
for (routes_module.routes) |const_route| {
|
||||
var var_route = try self.allocator.create(jetzig.views.Route);
|
||||
var_route.* = .{
|
||||
.name = const_route.name,
|
||||
.action = const_route.action,
|
||||
.view_name = const_route.view_name,
|
||||
.uri_path = const_route.uri_path,
|
||||
.view = const_route.view,
|
||||
.static = const_route.static,
|
||||
.layout = const_route.layout,
|
||||
.template = const_route.template,
|
||||
.json_params = const_route.json_params,
|
||||
};
|
||||
|
||||
try var_route.initParams(self.allocator);
|
||||
try routes.append(var_route);
|
||||
}
|
||||
|
||||
defer routes.deinit();
|
||||
defer for (routes.items) |var_route| {
|
||||
const routes = try createRoutes(self.allocator, &routes_module.routes);
|
||||
defer for (routes) |var_route| {
|
||||
var_route.deinitParams();
|
||||
self.allocator.destroy(var_route);
|
||||
};
|
||||
@ -107,7 +87,7 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void {
|
||||
var server = jetzig.http.Server.init(
|
||||
self.allocator,
|
||||
server_options,
|
||||
routes.items,
|
||||
routes,
|
||||
self.custom_routes.items,
|
||||
&routes_module.jobs,
|
||||
&routes_module.mailers,
|
||||
@ -124,7 +104,7 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void {
|
||||
.{
|
||||
.logger = server.logger,
|
||||
.environment = server.options.environment,
|
||||
.routes = routes.items,
|
||||
.routes = routes,
|
||||
.jobs = &routes_module.jobs,
|
||||
.mailers = &routes_module.mailers,
|
||||
.store = &store,
|
||||
@ -203,3 +183,30 @@ inline fn viewType(path: []const u8) enum { with_id, without_id, with_args } {
|
||||
|
||||
return .without_id;
|
||||
}
|
||||
|
||||
pub fn createRoutes(
|
||||
allocator: std.mem.Allocator,
|
||||
comptime_routes: []const jetzig.views.Route,
|
||||
) ![]*jetzig.views.Route {
|
||||
var routes = std.ArrayList(*jetzig.views.Route).init(allocator);
|
||||
|
||||
for (comptime_routes) |const_route| {
|
||||
var var_route = try allocator.create(jetzig.views.Route);
|
||||
var_route.* = .{
|
||||
.name = const_route.name,
|
||||
.action = const_route.action,
|
||||
.view_name = const_route.view_name,
|
||||
.uri_path = const_route.uri_path,
|
||||
.view = const_route.view,
|
||||
.static = const_route.static,
|
||||
.layout = const_route.layout,
|
||||
.template = const_route.template,
|
||||
.json_params = const_route.json_params,
|
||||
};
|
||||
|
||||
try var_route.initParams(allocator);
|
||||
try routes.append(var_route);
|
||||
}
|
||||
|
||||
return try routes.toOwnedSlice();
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ const Environment = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
pub const EnvironmentName = enum { development, production };
|
||||
pub const EnvironmentName = enum { development, production, testing };
|
||||
|
||||
const Options = struct {
|
||||
help: bool = false,
|
||||
@ -170,6 +170,7 @@ fn getSecret(self: Environment, logger: *jetzig.loggers.Logger, comptime len: u1
|
||||
|
||||
fn resolveLogLevel(level: ?jetzig.loggers.LogLevel, environment: EnvironmentName) jetzig.loggers.LogLevel {
|
||||
return level orelse switch (environment) {
|
||||
.testing => .DEBUG,
|
||||
.development => .DEBUG,
|
||||
.production => .INFO,
|
||||
};
|
||||
|
@ -132,10 +132,14 @@ pub fn init(
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Request) void {
|
||||
self.session.deinit();
|
||||
self.cookies.deinit();
|
||||
self.allocator.destroy(self.cookies);
|
||||
self.allocator.destroy(self.session);
|
||||
if (self._session) |*capture| {
|
||||
capture.*.deinit();
|
||||
self.allocator.destroy(capture.*);
|
||||
}
|
||||
if (self._cookies) |*capture| {
|
||||
capture.*.deinit();
|
||||
self.allocator.destroy(capture.*);
|
||||
}
|
||||
if (self.state != .initial) self.allocator.free(self.body);
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ headers: jetzig.http.Headers,
|
||||
content: []const u8,
|
||||
status_code: http.status_codes.StatusCode,
|
||||
content_type: []const u8,
|
||||
httpz_response: *httpz.Response,
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
@ -19,15 +20,10 @@ pub fn init(
|
||||
) !Self {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.httpz_response = httpz_response,
|
||||
.status_code = .no_content,
|
||||
.content_type = "application/octet-stream",
|
||||
.content = "",
|
||||
.headers = jetzig.http.Headers.init(allocator, &httpz_response.headers),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Self) void {
|
||||
self.headers.deinit();
|
||||
self.allocator.destroy(self.headers);
|
||||
self.std_response.deinit();
|
||||
}
|
||||
|
@ -111,12 +111,12 @@ pub fn errorHandlerFn(self: *Server, request: *httpz.Request, response: *httpz.R
|
||||
|
||||
self.logger.ERROR("Encountered error: {s} {s}", .{ @errorName(err), request.url.raw }) catch {};
|
||||
const stack = @errorReturnTrace();
|
||||
if (stack) |capture| self.logStackTrace(capture, .{ .httpz = request }) catch {};
|
||||
if (stack) |capture| self.logStackTrace(capture, request.arena) catch {};
|
||||
|
||||
response.body = "500 Internal Server Error";
|
||||
}
|
||||
|
||||
fn processNextRequest(
|
||||
pub fn processNextRequest(
|
||||
self: *Server,
|
||||
httpz_request: *httpz.Request,
|
||||
httpz_response: *httpz.Response,
|
||||
@ -442,6 +442,12 @@ fn renderErrorView(
|
||||
_ = route.render(route.*, request) catch |err| {
|
||||
if (isUnhandledError(err)) return err;
|
||||
try self.logger.logError(err);
|
||||
try self.logger.ERROR(
|
||||
"Unexepected error occurred while rendering error page: {s}",
|
||||
.{@errorName(err)},
|
||||
);
|
||||
const stack = @errorReturnTrace();
|
||||
if (stack) |capture| try self.logStackTrace(capture, request.allocator);
|
||||
return try renderDefaultError(request, status_code);
|
||||
};
|
||||
|
||||
@ -508,13 +514,8 @@ fn renderDefaultError(
|
||||
fn logStackTrace(
|
||||
self: Server,
|
||||
stack: *std.builtin.StackTrace,
|
||||
request: union(enum) { jetzig: *const jetzig.http.Request, httpz: *const httpz.Request },
|
||||
allocator: std.mem.Allocator,
|
||||
) !void {
|
||||
const allocator = switch (request) {
|
||||
.jetzig => |capture| capture.allocator,
|
||||
.httpz => |capture| capture.arena,
|
||||
};
|
||||
|
||||
var buf = std.ArrayList(u8).init(allocator);
|
||||
defer buf.deinit();
|
||||
const writer = buf.writer();
|
||||
@ -657,19 +658,12 @@ fn staticPath(request: *jetzig.http.Request, route: jetzig.views.Route) !?[]cons
|
||||
};
|
||||
|
||||
for (route.params.items, 0..) |static_params, index| {
|
||||
const expected_params = try static_params.getValue("params");
|
||||
const expected_params = static_params.get("params");
|
||||
switch (route.action) {
|
||||
.index, .post => {},
|
||||
inline else => {
|
||||
const id = try static_params.getValue("id");
|
||||
if (id == null) return error.JetzigRouteError; // `routes.zig` is incoherent.
|
||||
switch (id.?.*) {
|
||||
.string => |capture| {
|
||||
if (!std.mem.eql(u8, capture.value, request.path.resource_id)) continue;
|
||||
},
|
||||
// `routes.zig` is incoherent.
|
||||
inline else => return error.JetzigRouteError,
|
||||
}
|
||||
const id = static_params.getT(.string, "id") orelse return error.JetzigRouteError;
|
||||
if (!std.mem.eql(u8, id, request.path.resource_id)) continue;
|
||||
},
|
||||
}
|
||||
if (expected_params != null and !expected_params.?.eql(params)) continue;
|
||||
|
@ -2,11 +2,6 @@ const std = @import("std");
|
||||
|
||||
const jetzig = @import("../jetzig.zig");
|
||||
|
||||
pub const StatusCode = enum {
|
||||
ok,
|
||||
not_found,
|
||||
};
|
||||
|
||||
pub fn StatusCodeType(comptime code: []const u8, comptime message: []const u8) type {
|
||||
return struct {
|
||||
code: []const u8 = code,
|
||||
@ -33,16 +28,3 @@ pub fn StatusCodeType(comptime code: []const u8, comptime message: []const u8) t
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const StatusCodeUnion = union(StatusCode) {
|
||||
ok: StatusCode("200", "OK"),
|
||||
not_found: StatusCode("404", "Not Found"),
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn format(self: Self) []const u8 {
|
||||
return switch (self) {
|
||||
inline else => |case| case.format(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,5 @@
|
||||
pub const Job = @import("jobs/Job.zig");
|
||||
pub const JobEnv = Job.JobEnv;
|
||||
pub const JobConfig = Job.JobConfig;
|
||||
pub const JobDefinition = Job.JobDefinition;
|
||||
pub const Pool = @import("jobs/Pool.zig");
|
||||
pub const Worker = @import("jobs/Worker.zig");
|
||||
|
@ -6,6 +6,7 @@ const Self = @This();
|
||||
|
||||
pub const DevelopmentLogger = @import("loggers/DevelopmentLogger.zig");
|
||||
pub const JsonLogger = @import("loggers/JsonLogger.zig");
|
||||
pub const TestLogger = @import("loggers/TestLogger.zig");
|
||||
pub const LogQueue = @import("loggers/LogQueue.zig");
|
||||
|
||||
pub const LogLevel = enum(u4) { TRACE, DEBUG, INFO, WARN, ERROR, FATAL };
|
||||
@ -21,6 +22,7 @@ pub inline fn logTarget(comptime level: LogLevel) LogQueue.Target {
|
||||
pub const Logger = union(enum) {
|
||||
development_logger: DevelopmentLogger,
|
||||
json_logger: JsonLogger,
|
||||
test_logger: TestLogger,
|
||||
|
||||
/// Log a TRACE level message to the configured logger.
|
||||
pub fn TRACE(self: *const Logger, comptime message: []const u8, args: anytype) !void {
|
||||
|
57
src/jetzig/loggers/TestLogger.zig
Normal file
57
src/jetzig/loggers/TestLogger.zig
Normal file
@ -0,0 +1,57 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
const TestLogger = @This();
|
||||
|
||||
enabled: bool = false,
|
||||
|
||||
pub fn TRACE(self: TestLogger, comptime message: []const u8, args: anytype) !void {
|
||||
try self.log(.TRACE, message, args);
|
||||
}
|
||||
|
||||
pub fn DEBUG(self: TestLogger, comptime message: []const u8, args: anytype) !void {
|
||||
try self.log(.DEBUG, message, args);
|
||||
}
|
||||
|
||||
pub fn INFO(self: TestLogger, comptime message: []const u8, args: anytype) !void {
|
||||
try self.log(.INFO, message, args);
|
||||
}
|
||||
|
||||
pub fn WARN(self: TestLogger, comptime message: []const u8, args: anytype) !void {
|
||||
try self.log(.WARN, message, args);
|
||||
}
|
||||
|
||||
pub fn ERROR(self: TestLogger, comptime message: []const u8, args: anytype) !void {
|
||||
try self.log(.ERROR, message, args);
|
||||
}
|
||||
|
||||
pub fn FATAL(self: TestLogger, comptime message: []const u8, args: anytype) !void {
|
||||
try self.log(.FATAL, message, args);
|
||||
}
|
||||
|
||||
pub fn logRequest(self: TestLogger, request: *const jetzig.http.Request) !void {
|
||||
const status = jetzig.http.status_codes.get(request.response.status_code);
|
||||
var buf: [256]u8 = undefined;
|
||||
try self.log(.INFO, "[{s}|{s}|{s}] {s}", .{
|
||||
request.fmtMethod(true),
|
||||
try jetzig.colors.duration(&buf, jetzig.util.duration(request.start_time), true),
|
||||
status.getFormatted(.{ .colorized = true }),
|
||||
request.path.path,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn logError(self: TestLogger, err: anyerror) !void {
|
||||
try self.log(.ERROR, "Encountered error: {s}", .{@errorName(err)});
|
||||
}
|
||||
|
||||
pub fn log(
|
||||
self: TestLogger,
|
||||
comptime level: jetzig.loggers.LogLevel,
|
||||
comptime message: []const u8,
|
||||
args: anytype,
|
||||
) !void {
|
||||
if (self.enabled) {
|
||||
std.debug.print("-- test logger: " ++ @tagName(level) ++ " " ++ message ++ "\n", args);
|
||||
}
|
||||
}
|
249
src/jetzig/testing.zig
Normal file
249
src/jetzig/testing.zig
Normal file
@ -0,0 +1,249 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetzig = @import("../jetzig.zig");
|
||||
const zmpl = @import("zmpl");
|
||||
const httpz = @import("httpz");
|
||||
|
||||
/// An app used for testing. Processes requests and renders responses.
|
||||
pub const App = @import("testing/App.zig");
|
||||
|
||||
const testing = @This();
|
||||
|
||||
/// Pre-built mime map, assigned by Jetzig test runner.
|
||||
pub var mime_map: *jetzig.http.mime.MimeMap = undefined;
|
||||
pub var state: enum { initial, ready } = .initial;
|
||||
|
||||
pub const secret = "secret-bytes-for-use-in-test-environment-only";
|
||||
|
||||
pub const app = App.init;
|
||||
|
||||
pub const TestResponse = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
status: u16,
|
||||
body: []const u8,
|
||||
headers: []const Header,
|
||||
jobs: []Job,
|
||||
|
||||
pub const Header = struct { name: []const u8, value: []const u8 };
|
||||
pub const Job = struct { name: []const u8, params: ?[]const u8 = null };
|
||||
|
||||
pub fn expectStatus(self: TestResponse, comptime expected: jetzig.http.status_codes.StatusCode) !void {
|
||||
try testing.expectStatus(expected, self);
|
||||
}
|
||||
|
||||
pub fn expectBodyContains(self: TestResponse, comptime expected: []const u8) !void {
|
||||
try testing.expectBodyContains(expected, self);
|
||||
}
|
||||
|
||||
pub fn expectJson(self: TestResponse, expected_path: []const u8, expected_value: anytype) !void {
|
||||
try testing.expectJson(expected_path, expected_value, self);
|
||||
}
|
||||
|
||||
pub fn expectHeader(self: TestResponse, expected_name: []const u8, expected_value: ?[]const u8) !void {
|
||||
try testing.expectHeader(expected_name, expected_value, self);
|
||||
}
|
||||
|
||||
pub fn expectRedirect(self: TestResponse, path: []const u8) !void {
|
||||
try testing.expectRedirect(path, self);
|
||||
}
|
||||
|
||||
pub fn expectJob(self: TestResponse, job_name: []const u8, job_params: anytype) !void {
|
||||
try testing.expectJob(job_name, job_params, self);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn expectStatus(comptime expected: jetzig.http.status_codes.StatusCode, response: TestResponse) !void {
|
||||
const expected_code = try jetzig.http.status_codes.get(expected).getCodeInt();
|
||||
|
||||
if (response.status != expected_code) {
|
||||
log("Expected status: `{}`, actual status: `{}`", .{ expected_code, response.status });
|
||||
return error.JetzigExpectStatusError;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expectBodyContains(expected: []const u8, response: TestResponse) !void {
|
||||
if (!std.mem.containsAtLeast(u8, response.body, 1, expected)) {
|
||||
log(
|
||||
"Expected content:\n========\n{s}\n========\n\nActual content:\n========\n{s}\n========",
|
||||
.{ expected, response.body },
|
||||
);
|
||||
return error.JetzigExpectBodyContainsError;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expectHeader(expected_name: []const u8, expected_value: ?[]const u8, response: TestResponse) !void {
|
||||
for (response.headers) |header| {
|
||||
if (!std.ascii.eqlIgnoreCase(header.name, expected_name)) continue;
|
||||
if (expected_value) |value| {
|
||||
if (std.mem.eql(u8, header.value, value)) return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return error.JetzigExpectHeaderError;
|
||||
}
|
||||
|
||||
pub fn expectRedirect(path: []const u8, response: TestResponse) !void {
|
||||
if (response.status != 301 or response.status != 302) return error.JetzigExpectRedirectError;
|
||||
|
||||
try expectHeader("location", path, response);
|
||||
}
|
||||
|
||||
pub fn expectJson(expected_path: []const u8, expected_value: anytype, response: TestResponse) !void {
|
||||
var data = zmpl.Data.init(response.allocator);
|
||||
data.fromJson(response.body) catch |err| {
|
||||
switch (err) {
|
||||
error.SyntaxError => {
|
||||
log("Expected JSON, encountered parser error.", .{});
|
||||
return error.JetzigExpectJsonError;
|
||||
},
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
|
||||
const json_banner = "\n======|json|======\n{s}\n======|/json|=====\n";
|
||||
|
||||
if (try data.getValue(std.mem.trimLeft(u8, expected_path, &.{'.'}))) |value| {
|
||||
switch (value.*) {
|
||||
.string => |string| switch (@typeInfo(@TypeOf(expected_value))) {
|
||||
.Pointer, .Array => {
|
||||
if (std.mem.eql(u8, string.value, expected_value)) return;
|
||||
},
|
||||
.Null => {
|
||||
log(
|
||||
"Expected null/non-existent value for `{s}`, found: `{s}`",
|
||||
.{ expected_path, string.value },
|
||||
);
|
||||
return error.JetzigExpectJsonError;
|
||||
},
|
||||
else => unreachable,
|
||||
},
|
||||
.integer => |integer| switch (@typeInfo(@TypeOf(expected_value))) {
|
||||
.Int, .ComptimeInt => {
|
||||
if (integer.value == expected_value) return;
|
||||
},
|
||||
.Null => {
|
||||
log(
|
||||
"Expected null/non-existent value for `{s}`, found: `{}`",
|
||||
.{ expected_path, integer.value },
|
||||
);
|
||||
return error.JetzigExpectJsonError;
|
||||
},
|
||||
else => {},
|
||||
},
|
||||
.float => |float| switch (@typeInfo(@TypeOf(expected_value))) {
|
||||
.Float, .ComptimeFloat => {
|
||||
if (float.value == expected_value) return;
|
||||
},
|
||||
.Null => {
|
||||
log(
|
||||
"Expected null/non-existent value for `{s}`, found: `{}`",
|
||||
.{ expected_path, float.value },
|
||||
);
|
||||
return error.JetzigExpectJsonError;
|
||||
},
|
||||
else => {},
|
||||
},
|
||||
.boolean => |boolean| switch (@typeInfo(@TypeOf(expected_value))) {
|
||||
.Bool => {
|
||||
if (boolean.value == expected_value) return;
|
||||
},
|
||||
.Null => {
|
||||
log(
|
||||
"Expected null/non-existent value for `{s}`, found: `{}`",
|
||||
.{ expected_path, boolean.value },
|
||||
);
|
||||
return error.JetzigExpectJsonError;
|
||||
},
|
||||
else => {},
|
||||
},
|
||||
.Null => switch (@typeInfo(@TypeOf(expected_value))) {
|
||||
.Optional => {
|
||||
if (expected_value == null) return;
|
||||
},
|
||||
.Null => {
|
||||
return;
|
||||
},
|
||||
else => {},
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
switch (value.*) {
|
||||
.string => |string| {
|
||||
switch (@typeInfo(@TypeOf(expected_value))) {
|
||||
.Pointer, .Array => {
|
||||
log(
|
||||
"Expected `{s}` in `{s}`, found `{s}` in JSON:" ++ json_banner,
|
||||
.{ expected_value, expected_path, string.value, response.body },
|
||||
);
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
},
|
||||
.integer,
|
||||
=> |integer| {
|
||||
switch (@typeInfo(@TypeOf(expected_value))) {
|
||||
.Int, .ComptimeInt => {
|
||||
log(
|
||||
"Expected `{}` in `{s}`, found `{}` in JSON:" ++ json_banner,
|
||||
.{ expected_value, expected_path, integer.value, response.body },
|
||||
);
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
},
|
||||
.float => |float| {
|
||||
switch (@typeInfo(@TypeOf(expected_value))) {
|
||||
.Float, .ComptimeFloat => {
|
||||
log(
|
||||
"Expected `{}` in `{s}`, found `{}` in JSON:" ++ json_banner,
|
||||
.{ expected_value, expected_path, float.value, response.body },
|
||||
);
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
},
|
||||
.boolean => |boolean| {
|
||||
switch (@typeInfo(@TypeOf(expected_value))) {
|
||||
.Bool => {
|
||||
log(
|
||||
"Expected `{}` in `{s}`, found `{}` in JSON:" ++ json_banner,
|
||||
.{ expected_value, expected_path, boolean.value, response.body },
|
||||
);
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
},
|
||||
.Null => {
|
||||
log(
|
||||
"Expected value in `{s}`, found `null` in JSON:" ++ json_banner,
|
||||
.{ expected_path, response.body },
|
||||
);
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
} else {
|
||||
log(
|
||||
"Path not found: `{s}` in JSON: " ++ json_banner,
|
||||
.{ expected_path, response.body },
|
||||
);
|
||||
}
|
||||
return error.JetzigExpectJsonError;
|
||||
}
|
||||
|
||||
pub fn expectJob(job_name: []const u8, job_params: anytype, response: TestResponse) !void {
|
||||
for (response.jobs) |job| {
|
||||
comptime var has_args = false;
|
||||
inline for (@typeInfo(@TypeOf(job_params)).Struct.fields) |field| {
|
||||
has_args = true;
|
||||
_ = field;
|
||||
}
|
||||
if (!has_args and std.mem.eql(u8, job_name, job.name)) return;
|
||||
}
|
||||
return error.JetzigExpectJobError;
|
||||
}
|
||||
|
||||
fn log(comptime message: []const u8, args: anytype) void {
|
||||
std.debug.print("[jetzig.testing] " ++ message ++ "\n", args);
|
||||
}
|
234
src/jetzig/testing/App.zig
Normal file
234
src/jetzig/testing/App.zig
Normal file
@ -0,0 +1,234 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
const httpz = @import("httpz");
|
||||
|
||||
const App = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
routes: []const jetzig.views.Route,
|
||||
arena: *std.heap.ArenaAllocator,
|
||||
store: *jetzig.kv.Store,
|
||||
cache: *jetzig.kv.Store,
|
||||
job_queue: *jetzig.kv.Store,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, routes_module: type) !App {
|
||||
switch (jetzig.testing.state) {
|
||||
.ready => {},
|
||||
.initial => {
|
||||
std.log.err(
|
||||
"Unexpected state. Use Jetzig test runner: `zig build jetzig:test` or `jetzig test`",
|
||||
.{},
|
||||
);
|
||||
std.process.exit(1);
|
||||
},
|
||||
}
|
||||
|
||||
const arena = try allocator.create(std.heap.ArenaAllocator);
|
||||
arena.* = std.heap.ArenaAllocator.init(allocator);
|
||||
|
||||
return .{
|
||||
.arena = arena,
|
||||
.allocator = allocator,
|
||||
.routes = &routes_module.routes,
|
||||
.store = try createStore(arena.allocator()),
|
||||
.cache = try createStore(arena.allocator()),
|
||||
.job_queue = try createStore(arena.allocator()),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App) void {
|
||||
self.arena.deinit();
|
||||
self.allocator.destroy(self.arena);
|
||||
}
|
||||
|
||||
const RequestOptions = struct {
|
||||
headers: []const jetzig.testing.TestResponse.Header = &.{},
|
||||
json: ?[]const u8 = null,
|
||||
params: ?[]Param = null,
|
||||
};
|
||||
|
||||
const Param = struct {
|
||||
key: []const u8,
|
||||
value: ?[]const u8,
|
||||
};
|
||||
|
||||
pub fn request(
|
||||
self: *App,
|
||||
comptime method: jetzig.http.Request.Method,
|
||||
comptime path: []const u8,
|
||||
args: anytype,
|
||||
) !jetzig.testing.TestResponse {
|
||||
const options = buildOptions(self, args);
|
||||
|
||||
const allocator = self.arena.allocator();
|
||||
const routes = try jetzig.App.createRoutes(allocator, self.routes);
|
||||
|
||||
const logger = jetzig.loggers.Logger{ .test_logger = jetzig.loggers.TestLogger{} };
|
||||
var log_queue = jetzig.loggers.LogQueue.init(allocator);
|
||||
var server = jetzig.http.Server{
|
||||
.allocator = allocator,
|
||||
.logger = logger,
|
||||
.options = .{
|
||||
.logger = logger,
|
||||
.bind = undefined,
|
||||
.port = undefined,
|
||||
.detach = false,
|
||||
.environment = .testing,
|
||||
.log_queue = &log_queue,
|
||||
.secret = jetzig.testing.secret,
|
||||
},
|
||||
.routes = routes,
|
||||
.custom_routes = &.{},
|
||||
.mailer_definitions = &.{},
|
||||
.job_definitions = &.{},
|
||||
.mime_map = jetzig.testing.mime_map,
|
||||
.store = self.store,
|
||||
.cache = self.cache,
|
||||
.job_queue = self.job_queue,
|
||||
};
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
var httpz_request = try stubbedRequest(allocator, &buf, method, path, options);
|
||||
var httpz_response = try stubbedResponse(allocator);
|
||||
try server.processNextRequest(&httpz_request, &httpz_response);
|
||||
var headers = std.ArrayList(jetzig.testing.TestResponse.Header).init(self.arena.allocator());
|
||||
for (0..httpz_response.headers.len) |index| {
|
||||
try headers.append(.{
|
||||
.name = try self.arena.allocator().dupe(u8, httpz_response.headers.keys[index]),
|
||||
.value = try self.arena.allocator().dupe(u8, httpz_response.headers.values[index]),
|
||||
});
|
||||
}
|
||||
var data = jetzig.data.Data.init(allocator);
|
||||
defer data.deinit();
|
||||
|
||||
var jobs = std.ArrayList(jetzig.testing.TestResponse.Job).init(self.arena.allocator());
|
||||
while (try self.job_queue.popFirst(&data, "__jetzig_jobs")) |value| {
|
||||
if (value.getT(.string, "__jetzig_job_name")) |job_name| try jobs.append(.{
|
||||
.name = try self.arena.allocator().dupe(u8, job_name),
|
||||
});
|
||||
}
|
||||
|
||||
return .{
|
||||
.allocator = self.arena.allocator(),
|
||||
.status = httpz_response.status,
|
||||
.body = try self.arena.allocator().dupe(u8, httpz_response.body orelse ""),
|
||||
.headers = try headers.toOwnedSlice(),
|
||||
.jobs = try jobs.toOwnedSlice(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn params(self: App, args: anytype) []Param {
|
||||
const allocator = self.arena.allocator();
|
||||
var array = std.ArrayList(Param).init(allocator);
|
||||
inline for (@typeInfo(@TypeOf(args)).Struct.fields) |field| {
|
||||
array.append(.{ .key = field.name, .value = @field(args, field.name) }) catch @panic("OOM");
|
||||
}
|
||||
return array.toOwnedSlice() catch @panic("OOM");
|
||||
}
|
||||
|
||||
pub fn json(self: App, args: anytype) []const u8 {
|
||||
const allocator = self.arena.allocator();
|
||||
return std.json.stringifyAlloc(allocator, args, .{}) catch @panic("OOM");
|
||||
}
|
||||
|
||||
fn stubbedRequest(
|
||||
allocator: std.mem.Allocator,
|
||||
buf: []u8,
|
||||
comptime method: jetzig.http.Request.Method,
|
||||
comptime path: []const u8,
|
||||
options: RequestOptions,
|
||||
) !httpz.Request {
|
||||
var request_headers = try keyValue(allocator, 32);
|
||||
for (options.headers) |header| request_headers.add(header.name, header.value);
|
||||
if (options.json != null) {
|
||||
request_headers.add("accept", "application/json");
|
||||
request_headers.add("content-type", "application/json");
|
||||
}
|
||||
|
||||
var params_buf = std.ArrayList([]const u8).init(allocator);
|
||||
if (options.params) |array| {
|
||||
for (array) |param| {
|
||||
try params_buf.append(
|
||||
try std.fmt.allocPrint(allocator, "{s}{s}{s}", .{
|
||||
param.key,
|
||||
if (param.value != null) "=" else "",
|
||||
param.value orelse "",
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
const query = try std.mem.join(allocator, "&", try params_buf.toOwnedSlice());
|
||||
return .{
|
||||
.url = .{
|
||||
.raw = try std.mem.concat(allocator, u8, &.{ path, if (query.len > 0) "?" else "", query }),
|
||||
.path = path,
|
||||
.query = query,
|
||||
},
|
||||
.address = undefined,
|
||||
.method = std.enums.nameCast(httpz.Method, @tagName(method)),
|
||||
.protocol = .HTTP11,
|
||||
.params = undefined,
|
||||
.headers = request_headers,
|
||||
.body_buffer = if (options.json) |capture| .{ .data = @constCast(capture), .type = .static } else null,
|
||||
.body_len = if (options.json) |capture| capture.len else 0,
|
||||
.qs = try keyValue(allocator, 32),
|
||||
.fd = try keyValue(allocator, 32),
|
||||
.mfd = try multiFormKeyValue(allocator, 32),
|
||||
.spare = buf,
|
||||
.arena = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
fn stubbedResponse(allocator: std.mem.Allocator) !httpz.Response {
|
||||
return .{
|
||||
.conn = undefined,
|
||||
.pos = 0,
|
||||
.status = 200,
|
||||
.headers = try keyValue(allocator, 32),
|
||||
.content_type = null,
|
||||
.arena = allocator,
|
||||
.written = false,
|
||||
.chunked = false,
|
||||
.disowned = false,
|
||||
.keepalive = false,
|
||||
.body = null,
|
||||
};
|
||||
}
|
||||
|
||||
fn keyValue(allocator: std.mem.Allocator, max: usize) !httpz.key_value.KeyValue {
|
||||
return try httpz.key_value.KeyValue.init(allocator, max);
|
||||
}
|
||||
|
||||
fn multiFormKeyValue(allocator: std.mem.Allocator, max: usize) !httpz.key_value.MultiFormKeyValue {
|
||||
return try httpz.key_value.MultiFormKeyValue.init(allocator, max);
|
||||
}
|
||||
|
||||
fn createStore(allocator: std.mem.Allocator) !*jetzig.kv.Store {
|
||||
const store = try allocator.create(jetzig.kv.Store);
|
||||
store.* = try jetzig.kv.Store.init(allocator, .{});
|
||||
return store;
|
||||
}
|
||||
|
||||
fn buildOptions(app: *const App, args: anytype) RequestOptions {
|
||||
const fields = switch (@typeInfo(@TypeOf(args))) {
|
||||
.Struct => |info| info.fields,
|
||||
else => @compileError("Expected struct, found `" ++ @tagName(@typeInfo(@TypeOf(args))) ++ "`"),
|
||||
};
|
||||
|
||||
inline for (fields) |field| {
|
||||
comptime {
|
||||
if (std.mem.eql(u8, field.name, "headers")) continue;
|
||||
if (std.mem.eql(u8, field.name, "json")) continue;
|
||||
if (std.mem.eql(u8, field.name, "params")) continue;
|
||||
}
|
||||
|
||||
@compileError("Unrecognized request option: " ++ field.name);
|
||||
}
|
||||
|
||||
return .{
|
||||
.headers = if (@hasField(@TypeOf(args), "headers")) args.headers else &.{},
|
||||
.json = if (@hasField(@TypeOf(args), "json")) app.json(args.json) else null,
|
||||
.params = if (@hasField(@TypeOf(args), "params")) app.params(args.params) else null,
|
||||
};
|
||||
}
|
@ -70,3 +70,18 @@ pub fn generateSecret(allocator: std.mem.Allocator, comptime len: u10) ![]const
|
||||
pub fn duration(start_time: i128) i64 {
|
||||
return @intCast(std.time.nanoTimestamp() - start_time);
|
||||
}
|
||||
|
||||
/// Generate a random variable name with enough entropy to be considered unique.
|
||||
pub fn generateVariableName(buf: *[32]u8) []const u8 {
|
||||
const first_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const any_chars = "0123456789" ++ first_chars;
|
||||
|
||||
for (0..3) |index| {
|
||||
buf[index] = first_chars[std.crypto.random.intRangeAtMost(u8, 0, first_chars.len - 1)];
|
||||
}
|
||||
|
||||
for (3..32) |index| {
|
||||
buf[index] = any_chars[std.crypto.random.intRangeAtMost(u8, 0, any_chars.len - 1)];
|
||||
}
|
||||
return buf[0..32];
|
||||
}
|
||||
|
202
src/test_runner.zig
Normal file
202
src/test_runner.zig
Normal file
@ -0,0 +1,202 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
const Test = struct {
|
||||
name: []const u8,
|
||||
function: TestFn,
|
||||
module: ?[]const u8 = null,
|
||||
leaked: bool = false,
|
||||
result: Result = .success,
|
||||
stack_trace_buf: [4096]u8 = undefined,
|
||||
duration: usize = 0,
|
||||
|
||||
pub const TestFn = *const fn () anyerror!void;
|
||||
pub const Result = union(enum) {
|
||||
success: void,
|
||||
failure: Failure,
|
||||
skipped: void,
|
||||
};
|
||||
|
||||
const Failure = struct {
|
||||
err: anyerror,
|
||||
trace: ?[]const u8,
|
||||
};
|
||||
|
||||
const name_template = jetzig.colors.blue("{s}") ++ ":" ++ jetzig.colors.cyan("{s}") ++ " ";
|
||||
|
||||
pub fn init(test_fn: std.builtin.TestFn) Test {
|
||||
return if (std.mem.indexOf(u8, test_fn.name, ".test.")) |index|
|
||||
.{
|
||||
.function = test_fn.func,
|
||||
.module = test_fn.name[0..index],
|
||||
.name = test_fn.name[index + ".test.".len ..],
|
||||
}
|
||||
else
|
||||
.{ .function = test_fn.func, .name = test_fn.name };
|
||||
}
|
||||
|
||||
pub fn run(self: *Test) !void {
|
||||
std.testing.allocator_instance = .{};
|
||||
const start = std.time.nanoTimestamp();
|
||||
|
||||
self.function() catch |err| {
|
||||
switch (err) {
|
||||
error.SkipZigTest => self.result = .skipped,
|
||||
else => self.result = .{ .failure = .{
|
||||
.err = err,
|
||||
.trace = try self.formatStackTrace(@errorReturnTrace()),
|
||||
} },
|
||||
}
|
||||
};
|
||||
|
||||
self.duration = @intCast(std.time.nanoTimestamp() - start);
|
||||
|
||||
if (std.testing.allocator_instance.deinit() == .leak) self.leaked = true;
|
||||
}
|
||||
|
||||
fn formatStackTrace(self: *Test, maybe_trace: ?*std.builtin.StackTrace) !?[]const u8 {
|
||||
return if (maybe_trace) |trace| blk: {
|
||||
var stream = std.io.fixedBufferStream(&self.stack_trace_buf);
|
||||
const writer = stream.writer();
|
||||
try trace.format("", .{}, writer);
|
||||
break :blk stream.getWritten();
|
||||
} else null;
|
||||
}
|
||||
|
||||
pub fn print(self: Test, stream: anytype) !void {
|
||||
const writer = stream.writer();
|
||||
|
||||
switch (self.result) {
|
||||
.success => try self.printPassed(writer),
|
||||
.failure => |failure| try self.printFailure(failure, writer),
|
||||
.skipped => try self.printSkipped(writer),
|
||||
}
|
||||
try self.printDuration(writer);
|
||||
|
||||
if (self.leaked) try self.printLeaked(writer);
|
||||
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
|
||||
fn printPassed(self: Test, writer: anytype) !void {
|
||||
try writer.print(
|
||||
jetzig.colors.green("[PASS] ") ++ name_template,
|
||||
.{ self.module orelse "tests", self.name },
|
||||
);
|
||||
}
|
||||
|
||||
fn printFailure(self: Test, failure: Failure, writer: anytype) !void {
|
||||
try writer.print(
|
||||
jetzig.colors.red("[FAIL] ") ++ name_template ++ jetzig.colors.yellow("({s})"),
|
||||
.{ self.module orelse "tests", self.name, @errorName(failure.err) },
|
||||
);
|
||||
|
||||
if (failure.trace) |trace| {
|
||||
try writer.print("{s}", .{trace});
|
||||
}
|
||||
}
|
||||
|
||||
fn printSkipped(self: Test, writer: anytype) !void {
|
||||
try writer.print(
|
||||
jetzig.colors.yellow("[SKIP]") ++ name_template,
|
||||
.{ self.module orelse "tests", self.name },
|
||||
);
|
||||
}
|
||||
|
||||
fn printLeaked(self: Test, writer: anytype) !void {
|
||||
_ = self;
|
||||
try writer.print(jetzig.colors.red(" [LEAKED]"), .{});
|
||||
}
|
||||
|
||||
fn printDuration(self: Test, writer: anytype) !void {
|
||||
var buf: [256]u8 = undefined;
|
||||
try writer.print(
|
||||
"[" ++ jetzig.colors.cyan("{s}") ++ "]",
|
||||
.{try jetzig.colors.duration(&buf, @intCast(self.duration), true)},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
const start = std.time.nanoTimestamp();
|
||||
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const allocator = gpa.allocator();
|
||||
var tests = std.ArrayList(Test).init(allocator);
|
||||
defer tests.deinit();
|
||||
|
||||
var mime_map = jetzig.http.mime.MimeMap.init(allocator);
|
||||
try mime_map.build();
|
||||
jetzig.testing.mime_map = &mime_map;
|
||||
|
||||
try std.io.getStdErr().writer().writeAll("\n[jetzig] Launching Test Runner...\n\n");
|
||||
|
||||
jetzig.testing.state = .ready;
|
||||
|
||||
for (builtin.test_functions) |test_function| {
|
||||
var t = Test.init(test_function);
|
||||
try t.run();
|
||||
try t.print(std.io.getStdErr());
|
||||
try tests.append(t);
|
||||
}
|
||||
|
||||
try printSummary(tests.items, start);
|
||||
}
|
||||
|
||||
fn printSummary(tests: []const Test, start: i128) !void {
|
||||
var success: usize = 0;
|
||||
var failure: usize = 0;
|
||||
var leaked: usize = 0;
|
||||
var skipped: usize = 0;
|
||||
|
||||
for (tests) |t| {
|
||||
switch (t.result) {
|
||||
.success => success += 1,
|
||||
.failure => failure += 1,
|
||||
.skipped => skipped += 1,
|
||||
}
|
||||
if (t.leaked) leaked += 1;
|
||||
}
|
||||
const tick = jetzig.colors.green("✔");
|
||||
const cross = jetzig.colors.red("✗");
|
||||
|
||||
const writer = std.io.getStdErr().writer();
|
||||
|
||||
var total_duration_buf: [256]u8 = undefined;
|
||||
const total_duration = try jetzig.colors.duration(
|
||||
&total_duration_buf,
|
||||
@intCast(std.time.nanoTimestamp() - start),
|
||||
false,
|
||||
);
|
||||
|
||||
try writer.print(
|
||||
"\n {s}{s}{}" ++
|
||||
"\n {s}{s}{}" ++
|
||||
"\n {s}{}" ++
|
||||
"\n " ++ jetzig.colors.cyan(" tests ") ++ "{}" ++
|
||||
"\n " ++ jetzig.colors.cyan(" duration ") ++ "{s}" ++ "\n\n",
|
||||
.{
|
||||
if (failure == 0) tick else cross,
|
||||
if (failure == 0) jetzig.colors.blue(" failed ") else jetzig.colors.red(" failed "),
|
||||
failure,
|
||||
if (leaked == 0) tick else cross,
|
||||
if (leaked == 0) jetzig.colors.blue(" leaked ") else jetzig.colors.red(" leaked "),
|
||||
leaked,
|
||||
if (skipped == 0) jetzig.colors.blue(" skipped ") else jetzig.colors.yellow(" skipped "),
|
||||
skipped,
|
||||
success + failure,
|
||||
total_duration,
|
||||
},
|
||||
);
|
||||
|
||||
if (failure == 0 and leaked == 0) {
|
||||
try writer.print(jetzig.colors.green(" PASS ") ++ "\n", .{});
|
||||
try writer.print(jetzig.colors.green(" ▔▔▔▔") ++ "\n", .{});
|
||||
std.process.exit(0);
|
||||
} else {
|
||||
try writer.print(jetzig.colors.red(" FAIL ") ++ "\n", .{});
|
||||
try writer.print(jetzig.colors.red(" ▔▔▔▔") ++ "\n", .{});
|
||||
std.process.exit(1);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user