This commit is contained in:
Bob Farrell 2024-11-09 17:12:16 +00:00
parent 4210aa5e83
commit d27907a1c5
18 changed files with 202 additions and 61 deletions

View File

@ -4,6 +4,7 @@ pub const Routes = @import("src/Routes.zig");
pub const GenerateMimeTypes = @import("src/GenerateMimeTypes.zig");
const zmpl_build = @import("zmpl");
const Environment = enum { development, testing, production };
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
@ -106,7 +107,10 @@ pub fn build(b: *std.Build) !void {
main_tests.root_module.addImport("jetcommon", jetcommon_dep.module("jetcommon"));
main_tests.root_module.addImport("httpz", httpz_dep.module("httpz"));
main_tests.root_module.addImport("smtp", smtp_client_dep.module("smtp_client"));
const test_build_options = b.addOptions();
test_build_options.addOption(Environment, "environment", .testing);
const run_main_tests = b.addRunArtifact(main_tests);
main_tests.root_module.addOptions("build_options", test_build_options);
const test_step = b.step("test", "Run library tests");
test_step.dependOn(&run_main_tests.step);
@ -123,12 +127,13 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
return error.ZmplVersionNotSupported;
}
_ = b.option([]const u8, "seed", "Internal test seed");
const target = b.host;
const optimize = exe.root_module.optimize orelse .Debug;
if (optimize != .Debug) exe.linkLibC();
const Environment = enum { development, testing, production };
const environment = b.option(
Environment,
"environment",
@ -207,7 +212,6 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
exe_routes_file.root_module.addImport("zmpl", zmpl_module);
const run_routes_file_cmd = b.addRunArtifact(exe_routes_file);
run_routes_file_cmd.has_side_effects = true; // FIXME
const routes_file_path = run_routes_file_cmd.addOutputFileArg("routes.zig");
run_routes_file_cmd.addArgs(&.{
root_path,
@ -217,6 +221,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
jobs_path,
mailers_path,
});
const routes_module = b.createModule(.{ .root_source_file = routes_file_path });
routes_module.addImport("jetzig", jetzig_module);
exe.root_module.addImport("routes", routes_module);
@ -262,7 +267,10 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
run_static_routes_cmd.expectExitCode(0);
const run_tests_file_cmd = b.addRunArtifact(exe_routes_file);
run_tests_file_cmd.step.dependOn(&run_routes_file_cmd.step);
const tests_file_path = run_tests_file_cmd.addOutputFileArg("tests.zig");
run_tests_file_cmd.addArgs(&.{
root_path,
b.pathFromRoot("src"),
@ -270,8 +278,15 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
views_path,
jobs_path,
mailers_path,
try randomSeed(b), // Hack to force cache skip - let's find a better way.
});
for (try scanSourceFiles(b)) |sub_path| {
// XXX: We don't use these args, but they help Zig's build system to cache/bust steps.
run_routes_file_cmd.addFileArg(.{ .src_path = .{ .owner = b, .sub_path = sub_path } });
run_tests_file_cmd.addFileArg(.{ .src_path = .{ .owner = b, .sub_path = sub_path } });
}
const exe_unit_tests = b.addTest(.{
.root_source_file = tests_file_path,
.target = target,
@ -280,6 +295,8 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
});
exe_unit_tests.root_module.addImport("jetzig", jetzig_module);
exe_unit_tests.root_module.addImport("static", static_module);
exe_unit_tests.root_module.addImport("routes", routes_module);
exe_unit_tests.root_module.addImport("main", main_module);
var it = exe.root_module.import_table.iterator();
while (it.next()) |import| {
@ -303,12 +320,10 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
}
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
const test_step = b.step("jetzig:test", "Run tests");
test_step.dependOn(&run_static_routes_cmd.step);
test_step.dependOn(&run_exe_unit_tests.step);
test_step.dependOn(&run_tests_file_cmd.step);
exe_unit_tests.root_module.addImport("routes", routes_module);
run_exe_unit_tests.step.dependOn(&run_static_routes_cmd.step);
run_exe_unit_tests.step.dependOn(&run_routes_file_cmd.step);
const routes_step = b.step("jetzig:routes", "List all routes in your app");
const exe_routes = b.addExecutable(.{
@ -346,7 +361,7 @@ fn registerDatabaseSteps(b: *std.Build, exe_database: *std.Build.Step.Compile) v
.{ "rollback", "Roll back a migration in your Jetzig app's database." },
.{ "create", "Create a database for your Jetzig app." },
.{ "drop", "Drop your Jetzig app's database." },
.{ "dump", "Read your app's database and generate a JetQuery schema." },
.{ "reflect", "Read your app's database and generate a JetQuery schema." },
};
inline for (commands) |command| {
@ -411,3 +426,30 @@ fn isSourceFile(b: *std.Build, path: []const u8) !bool {
};
return stat.kind == .file;
}
fn scanSourceFiles(b: *std.Build) ![]const []const u8 {
var buf = std.ArrayList([]const u8).init(b.allocator);
var src_dir = try std.fs.openDirAbsolute(b.pathFromRoot("src"), .{ .iterate = true });
defer src_dir.close();
var walker = try src_dir.walk(b.allocator);
defer walker.deinit();
while (try walker.next()) |entry| {
if (entry.kind == .file) try buf.append(
try std.fs.path.join(b.allocator, &.{ "src", entry.path }),
);
}
return try buf.toOwnedSlice();
}
fn randomSeed(b: *std.Build) ![]const u8 {
const seed = try b.allocator.alloc(u8, 32);
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
for (0..32) |index| {
seed[index] = chars[std.crypto.random.intRangeAtMost(u8, 0, chars.len - 1)];
}
return seed;
}

View File

@ -7,14 +7,16 @@
.hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163",
},
.zmpl = .{
.path = "../zmpl",
.url = "https://github.com/jetzig-framework/zmpl/archive/517b453f333c328ff5ebafa49020b1040e54e880.tar.gz",
.hash = "1220798a4647e3b0766aad653830a2601e11c567ba6bfe83e526eb91d04a6c45f7d8",
},
.jetkv = .{
.url = "https://github.com/jetzig-framework/jetkv/archive/2b1130a48979ea2871c8cf6ca89c38b1e7062839.tar.gz",
.hash = "12201d75d73aad5e1c996de4d5ae87a00e58479c8d469bc2eeb5fdeeac8857bc09af",
},
.jetquery = .{
.path = "../jetquery",
.url = "https://github.com/jetzig-framework/jetquery/archive/8a5c1660504bb6f235e0fd2add7b7aca493a855b.tar.gz",
.hash = "1220f5daafc820790e66a3f509289d3575c8e7841748b72620f0ba687823fa2db4e4",
},
.jetcommon = .{
.url = "https://github.com/jetzig-framework/jetcommon/archive/a248776ba56d6cc2b160d593ac3305756adcd26e.tar.gz",
@ -25,13 +27,13 @@
.hash = "1220411a8c46d95bbf3b6e2059854bcb3c5159d428814099df5294232b9980517e9c",
},
.pg = .{
// .url = "https://github.com/karlseguin/pg.zig/archive/1491270ac43c7eba91992bb06b3758254c36e39a.tar.gz",
// .hash = "1220bcc68967188de7ad5d520a4629c0d1e169c111d87e6978a3c128de5ec2b6bdd0",
.path = "../pg.zig",
.url = "https://github.com/karlseguin/pg.zig/archive/9226a0a256cd000eee2f9652c8c03af8dcbed79b.tar.gz",
.hash = "12202b30ebf018ca398f665e51cae6d000fdfe2d08d5ec369e3110f762c548b154a4",
},
.smtp_client = .{
.url = "https://github.com/karlseguin/smtp_client.zig/archive/8fcfad9ca2d9e446612c79f4e54050cfbe81b38d.tar.gz",
.hash = "1220cebfcf6c63295819df92ec54abe62aad91b1d16666781194c29a7874bb7bbbda",
// Pending https://github.com/karlseguin/smtp_client.zig/pull/9 before using mainline
.url = "https://github.com/bobf/smtp_client.zig/archive/945554f22f025ba8a1efd01e56bda427bb4e22ca.tar.gz",
.hash = "1220de146446d0cae4396e346cb8283dd5e086491f8577ddbd5e03ad0928111d8bc6",
},
.httpz = .{
.url = "https://github.com/karlseguin/http.zig/archive/da9e944de0be6e5c67ca711dd238ce82d81558b4.tar.gz",

View File

@ -10,13 +10,13 @@ const migrate = @import("database/migrate.zig");
const rollback = @import("database/rollback.zig");
const create = @import("database/create.zig");
const drop = @import("database/drop.zig");
const dump = @import("database/dump.zig");
const reflect = @import("database/reflect.zig");
pub const confirm_drop_env = "JETZIG_DROP_PRODUCTION_DATABASE";
/// Command line options for the `database` command.
pub const Options = struct {
pub const meta = .{
.usage_summary = "[migrate|rollback|create|drop|dump]",
.usage_summary = "[migrate|rollback|create|drop|reflect]",
.full_text =
\\Manage the application's database.
\\
@ -40,13 +40,13 @@ pub fn run(
defer arena.deinit();
const alloc = arena.allocator();
const Action = enum { migrate, rollback, create, drop, dump };
const Action = enum { migrate, rollback, create, drop, reflect };
const map = std.StaticStringMap(Action).initComptime(.{
.{ "migrate", .migrate },
.{ "rollback", .rollback },
.{ "create", .create },
.{ "drop", .drop },
.{ "dump", .dump },
.{ "reflect", .reflect },
});
const action = if (main_options.positionals.len > 0)
@ -74,7 +74,7 @@ pub fn run(
.rollback => rollback.run(alloc, cwd, sub_args, options, T, main_options),
.create => create.run(alloc, cwd, sub_args, options, T, main_options),
.drop => drop.run(alloc, cwd, sub_args, options, T, main_options),
.dump => dump.run(alloc, cwd, sub_args, options, T, main_options),
.reflect => reflect.run(alloc, cwd, sub_args, options, T, main_options),
};
};
}

View File

@ -19,7 +19,7 @@ pub fn run(
\\
\\Example:
\\
\\ jetzig database dump
\\ jetzig database reflect
\\
, .{});
@ -30,6 +30,6 @@ pub fn run(
"zig",
"build",
util.environmentBuildOption(main_options.options.environment),
"jetzig:database:dump",
"jetzig:database:reflect",
});
}

View File

@ -104,6 +104,14 @@ pub fn run(
&[_]Replace{.{ .from = "jetzig-demo", .to = project_name }},
);
try copySourceFile(
allocator,
install_dir,
"demo/config/database.zig",
"config/database.zig",
null,
);
try copySourceFile(
allocator,
install_dir,

View File

@ -37,8 +37,7 @@ pub fn run(
try util.execCommand(allocator, &.{
"zig",
"build",
"-e",
"testing",
"-Denvironment=testing",
"jetzig:test",
});
}

1
demo/.gitignore vendored
View File

@ -3,3 +3,4 @@ zig-out/
static/
src/app/views/**/.*.zig
.DS_Store
log/

View File

@ -6,7 +6,7 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
const session = try request.session();
if (try session.get("message")) |message| {
if (session.get("message")) |message| {
try root.put("message", message);
} else {
try root.put("message", data.string("No message saved yet"));

View File

@ -0,0 +1,48 @@
pub const database = .{
// Null adapter fails when a database call is invoked.
.development = .{
.adapter = .null,
},
.testing = .{
.adapter = .null,
},
.production = .{
.adapter = .null,
},
// PostgreSQL adapter configuration.
//
// All options except `adapter` can be configured using environment variables:
//
// * JETQUERY_HOSTNAME
// * JETQUERY_PORT
// * JETQUERY_USERNAME
// * JETQUERY_PASSWORD
// * JETQUERY_DATABASE
//
// .testing = .{
// .adapter = .postgresql,
// .hostname = "localhost",
// .port = 5432,
// .username = "postgres",
// .password = "password",
// .database = "myapp_testing",
// },
//
// .development = .{
// .adapter = .postgresql,
// .hostname = "localhost",
// .port = 5432,
// .username = "postgres",
// .password = "password",
// .database = "myapp_development",
// },
//
// .production = .{
// .adapter = .postgresql,
// .hostname = "localhost",
// .port = 5432,
// .username = "postgres",
// .password = "password",
// .database = "myapp_production",
// },
};

View File

@ -14,7 +14,7 @@ const production_drop_failure_message = "To drop a production database, " ++
const environment = jetzig.build_options.environment;
const config = @field(jetquery.config.database, @tagName(environment));
const Action = enum { migrate, rollback, create, drop, dump };
const Action = enum { migrate, rollback, create, drop, reflect };
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
@ -35,7 +35,7 @@ pub fn main() !void {
.{ "rollback", .rollback },
.{ "create", .create },
.{ "drop", .drop },
.{ "dump", .dump },
.{ "reflect", .reflect },
});
const action = map.get(args[1]) orelse return error.JetzigUnrecognizedDatabaseArgument;
@ -80,7 +80,7 @@ pub fn main() !void {
try repo.dropDatabase(config.database, .{});
}
},
.dump => {
.reflect => {
var cwd = try jetzig.util.detectJetzigProjectDir();
defer cwd.close();
@ -99,12 +99,12 @@ pub fn main() !void {
,
},
);
const dump = try reflect.generateSchema();
const schema = try reflect.generateSchema();
const path = try cwd.realpathAlloc(
allocator,
try std.fs.path.join(allocator, &.{ "src", "app", "database", "Schema.zig" }),
);
try jetzig.util.createFile(path, dump);
try jetzig.util.createFile(path, schema);
std.log.info("Database schema written to `{s}`.", .{path});
},
}
@ -119,7 +119,7 @@ fn migrationsRepo(action: Action, allocator: std.mem.Allocator) !MigrationsRepo
.admin = switch (action) {
.migrate, .rollback => false,
.create, .drop => true,
.dump => unreachable, // We use a separate repo for schema reflection.
.reflect => unreachable, // We use a separate repo for schema reflection.
},
.context = .migration,
},

View File

@ -97,10 +97,8 @@ pub const job_worker_threads: usize = 1;
/// milliseconds.
pub const job_worker_sleep_interval_ms: usize = 10;
/// Database Schema.
pub const Schema: type = struct {
pub const _null = struct {}; // https://github.com/ziglang/zig/pull/21331
};
/// Database Schema. Set to `@import("Schema")` to load `src/app/database/Schema.zig`.
pub const Schema: type = struct {};
/// Key-value store options. Set backend to `.file` to use a file-based store.
/// When using `.file` backend, you must also set `.file_options`.
@ -166,7 +164,7 @@ pub const force_development_email_delivery = false;
/// Reconciles a configuration value from user-defined values and defaults provided by Jetzig.
pub fn get(T: type, comptime key: []const u8) T {
const self = @This();
if (!@hasDecl(self, key)) @panic("Unknown config option: " ++ key);
if (!@hasDecl(self, key)) @compileError("Unknown config option: " ++ key);
if (@hasDecl(root, "jetzig_options") and @hasDecl(root.jetzig_options, key)) {
return @field(root.jetzig_options, key);

View File

@ -10,10 +10,10 @@ pub fn Query(comptime model: anytype) type {
return jetzig.jetquery.Query(adapter, Schema, model);
}
pub fn repo(allocator: std.mem.Allocator, app: *const jetzig.App) !Repo {
pub fn repo(allocator: std.mem.Allocator, app: anytype) !Repo {
// XXX: Is this terrible ?
const Callback = struct {
var jetzig_app: *const jetzig.App = undefined;
var jetzig_app: @TypeOf(app) = undefined;
pub fn callbackFn(event: jetzig.jetquery.events.Event) !void {
try eventCallback(event, jetzig_app);
}
@ -30,7 +30,7 @@ pub fn repo(allocator: std.mem.Allocator, app: *const jetzig.App) !Repo {
);
}
fn eventCallback(event: jetzig.jetquery.events.Event, app: *const jetzig.App) !void {
fn eventCallback(event: jetzig.jetquery.events.Event, app: anytype) !void {
try app.server.logger.logSql(event);
if (event.err) |err| {
try app.server.logger.ERROR("[database] {?s}", .{err.message});

View File

@ -47,8 +47,6 @@ pub fn reset(self: *Self) !void {
/// Free allocated memory.
pub fn deinit(self: *Self) void {
if (self.state != .parsed) return;
self.data.deinit();
}
@ -78,7 +76,7 @@ pub fn getT(
/// Put a value into the session.
pub fn put(self: *Self, key: []const u8, value: anytype) !void {
if (self.state != .parsed) return error.UnparsedSessionCookie;
std.debug.assert(self.state == .parsed);
switch (self.data.value.?.*) {
.object => |*object| {
@ -90,10 +88,9 @@ pub fn put(self: *Self, key: []const u8, value: anytype) !void {
try self.save();
}
// removes true if a value was removed
// and false otherwise
// Returns `true` if a value was removed and `false` otherwise.
pub fn remove(self: *Self, key: []const u8) !bool {
if (self.state != .parsed) return error.UnparsedSessionCookie;
std.debug.assert(self.state == .parsed);
// copied from `get()`
const result = switch (self.data.value.?.*) {
@ -191,7 +188,7 @@ test "put and get session key/value" {
try session.parse();
try session.put("foo", data.string("bar"));
var value = (try session.get("foo")).?;
var value = (session.get("foo")).?;
try std.testing.expectEqualStrings(try value.toString(), "bar");
}
@ -210,11 +207,11 @@ test "remove session key/value" {
try session.parse();
try session.put("foo", data.string("bar"));
var value = (try session.get("foo")).?;
var value = (session.get("foo")).?;
try std.testing.expectEqualStrings(try value.toString(), "bar");
try std.testing.expectEqual(true, try session.remove("foo"));
try std.testing.expectEqual(null, try session.get("foo"));
try std.testing.expectEqual(null, session.get("foo"));
}
test "get value from parsed/decrypted cookie" {
@ -231,7 +228,7 @@ test "get value from parsed/decrypted cookie" {
defer session.deinit();
try session.parse();
var value = (try session.get("foo")).?;
var value = (session.get("foo")).?;
try std.testing.expectEqualStrings("bar", try value.toString());
}

View File

@ -4,7 +4,8 @@ const jetzig = @import("../../jetzig.zig");
const TestLogger = @This();
enabled: bool = false,
mode: enum { stream, file, disable },
file: ?std.fs.File = null,
pub fn TRACE(self: TestLogger, comptime message: []const u8, args: anytype) !void {
try self.log(.TRACE, message, args);
@ -55,7 +56,10 @@ pub fn log(
comptime message: []const u8,
args: anytype,
) !void {
if (self.enabled) {
std.debug.print("-- test logger: " ++ @tagName(level) ++ " " ++ message ++ "\n", args);
const template = "-- test logger: " ++ @tagName(level) ++ " " ++ message ++ "\n";
switch (self.mode) {
.stream => std.debug.print(template, args),
.file => try self.file.?.writer().print(template, args),
.disable => {},
}
}

View File

@ -3,6 +3,7 @@ const jetzig = @import("../jetzig.zig");
pub const HtmxMiddleware = @import("middleware/HtmxMiddleware.zig");
pub const CompressionMiddleware = @import("middleware/CompressionMiddleware.zig");
pub const AuthMiddleware = @import("middleware/AuthMiddleware.zig");
const RouteOptions = struct {
content: ?[]const u8 = null,

View File

@ -1,5 +1,5 @@
const std = @import("std");
const jetzig = @import("jetzig");
const jetzig = @import("../../jetzig.zig");
pub const middleware_name = "auth";
@ -9,7 +9,7 @@ const user_model = jetzig.config.get(jetzig.auth.AuthOptions, "auth").user_model
/// Define any custom data fields you want to store here. Assigning to these fields in the `init`
/// function allows you to access them in the `beforeRequest` and `afterRequest` functions, where
/// they can also be modified.
user: ?@TypeOf(jetzig.database.Query(user_model).find(0)).ResultType(),
user: ?@TypeOf(jetzig.database.Query(user_model).find(0)).ResultType,
const Self = @This();

View File

@ -12,6 +12,11 @@ store: *jetzig.kv.Store,
cache: *jetzig.kv.Store,
job_queue: *jetzig.kv.Store,
multipart_boundary: ?[]const u8 = null,
logger: jetzig.loggers.Logger,
server: Server,
repo: *jetzig.database.Repo,
const Server = struct { logger: jetzig.loggers.Logger };
const initHook = jetzig.root.initHook;
@ -31,20 +36,39 @@ pub fn init(allocator: std.mem.Allocator, routes_module: type) !App {
const arena = try allocator.create(std.heap.ArenaAllocator);
arena.* = std.heap.ArenaAllocator.init(allocator);
return .{
var dir = try std.fs.cwd().makeOpenPath("log", .{});
const file = try dir.createFile("test.log", .{ .exclusive = false, .truncate = false });
const logger = jetzig.loggers.Logger{
.test_logger = jetzig.loggers.TestLogger{ .mode = .file, .file = file },
};
const alloc = arena.allocator();
const app = try alloc.create(App);
const repo = try alloc.create(jetzig.database.Repo);
app.* = App{
.arena = arena,
.allocator = allocator,
.routes = &routes_module.routes,
.store = try createStore(arena.allocator()),
.cache = try createStore(arena.allocator()),
.job_queue = try createStore(arena.allocator()),
.logger = logger,
.server = .{ .logger = logger },
.repo = repo,
};
repo.* = try jetzig.database.repo(alloc, app.*);
return app.*;
}
/// Free allocated resources for test app.
pub fn deinit(self: *App) void {
self.arena.deinit();
self.allocator.destroy(self.arena);
if (self.logger.test_logger.file) |file| file.close();
}
const RequestOptions = struct {
@ -85,19 +109,21 @@ pub fn request(
const options = try buildOptions(allocator, self, args);
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);
// We init the `std.process.EnvMap` directly here (instead of calling `std.process.getEnvMap`
// to ensure that tests run in a clean environment. Users can manually add items to the
// environment within a test if required.
const vars = jetzig.Environment.Vars{ .env_map = std.process.EnvMap.init(allocator) };
var server = jetzig.http.Server{
.allocator = allocator,
.logger = logger,
.logger = self.logger,
.env = .{
.parent_allocator = undefined,
.arena = undefined,
.allocator = allocator,
.vars = vars,
.logger = logger,
.logger = self.logger,
.bind = undefined,
.port = undefined,
.detach = false,
@ -114,7 +140,7 @@ pub fn request(
.cache = self.cache,
.job_queue = self.job_queue,
.global = undefined,
.repo = undefined, // TODO - database test helpers
.repo = self.repo,
};
try server.decodeStaticParams();
@ -154,7 +180,8 @@ 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");
const value = coerceString(allocator, @field(args, field.name));
array.append(.{ .key = field.name, .value = value }) catch @panic("OOM");
}
return array.toOwnedSlice() catch @panic("OOM");
}
@ -333,3 +360,14 @@ fn buildHeaders(allocator: std.mem.Allocator, args: anytype) ![]const jetzig.tes
}
return try headers.toOwnedSlice();
}
fn coerceString(allocator: std.mem.Allocator, value: anytype) []const u8 {
return switch (@typeInfo(@TypeOf(value))) {
.int,
.float,
.comptime_int,
.comptime_float,
=> std.fmt.allocPrint(allocator, "{d}", .{value}) catch @panic("OOM"),
else => value, // TODO: Handle more complex types - arrays, objects, etc.
};
}

View File

@ -7,6 +7,8 @@ pub const std_options = std.Options{
.logFn = log,
};
pub const jetzig_options = @import("main").jetzig_options;
pub fn log(
comptime message_level: std.log.Level,
comptime scope: @Type(.enum_literal),
@ -144,14 +146,14 @@ const Test = struct {
fn printSkipped(self: Test, writer: anytype) !void {
try writer.print(
jetzig.colors.yellow("[SKIP]") ++ name_template,
"[" ++ 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]"), .{});
try writer.print("[" ++ jetzig.colors.red("LEAKED") ++ "] ", .{});
}
fn printDuration(self: Test, writer: anytype) !void {
@ -251,6 +253,7 @@ fn printSummary(tests: []const Test, start: i128) !void {
} else {
try writer.print(jetzig.colors.red(" FAIL ") ++ "\n", .{});
try writer.print(jetzig.colors.red(" ▔▔▔▔") ++ "\n", .{});
try writer.print("Server logs: " ++ jetzig.colors.cyan("log/test.log") ++ "\n\n", .{});
std.process.exit(1);
}
}