diff --git a/build.zig b/build.zig index bdae688..2feb6c6 100644 --- a/build.zig +++ b/build.zig @@ -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; +} diff --git a/build.zig.zon b/build.zig.zon index 8f9565e..b4dc192 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -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", diff --git a/cli/commands/database.zig b/cli/commands/database.zig index bad2e94..38989ee 100644 --- a/cli/commands/database.zig +++ b/cli/commands/database.zig @@ -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), }; }; } diff --git a/cli/commands/database/dump.zig b/cli/commands/database/reflect.zig similarity index 91% rename from cli/commands/database/dump.zig rename to cli/commands/database/reflect.zig index 167d68e..fecdd40 100644 --- a/cli/commands/database/dump.zig +++ b/cli/commands/database/reflect.zig @@ -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", }); } diff --git a/cli/commands/init.zig b/cli/commands/init.zig index efb78fb..215acb1 100644 --- a/cli/commands/init.zig +++ b/cli/commands/init.zig @@ -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, diff --git a/cli/commands/tests.zig b/cli/commands/tests.zig index 49257ad..1654da4 100644 --- a/cli/commands/tests.zig +++ b/cli/commands/tests.zig @@ -37,8 +37,7 @@ pub fn run( try util.execCommand(allocator, &.{ "zig", "build", - "-e", - "testing", + "-Denvironment=testing", "jetzig:test", }); } diff --git a/demo/.gitignore b/demo/.gitignore index b08979b..1ddcd8a 100644 --- a/demo/.gitignore +++ b/demo/.gitignore @@ -3,3 +3,4 @@ zig-out/ static/ src/app/views/**/.*.zig .DS_Store +log/ diff --git a/demo/src/app/views/session.zig b/demo/src/app/views/session.zig index 3108823..cec1fdb 100644 --- a/demo/src/app/views/session.zig +++ b/demo/src/app/views/session.zig @@ -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")); diff --git a/demo/src/config/database.zig b/demo/src/config/database.zig new file mode 100644 index 0000000..676b606 --- /dev/null +++ b/demo/src/config/database.zig @@ -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", + // }, +}; diff --git a/src/commands/database.zig b/src/commands/database.zig index af911cb..8607185 100644 --- a/src/commands/database.zig +++ b/src/commands/database.zig @@ -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, }, diff --git a/src/jetzig/config.zig b/src/jetzig/config.zig index b81029d..cce32c3 100644 --- a/src/jetzig/config.zig +++ b/src/jetzig/config.zig @@ -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); diff --git a/src/jetzig/database.zig b/src/jetzig/database.zig index c30df34..b1c4283 100644 --- a/src/jetzig/database.zig +++ b/src/jetzig/database.zig @@ -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}); diff --git a/src/jetzig/http/Session.zig b/src/jetzig/http/Session.zig index 40607e3..1c96a60 100644 --- a/src/jetzig/http/Session.zig +++ b/src/jetzig/http/Session.zig @@ -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()); } diff --git a/src/jetzig/loggers/TestLogger.zig b/src/jetzig/loggers/TestLogger.zig index 6ffe5ff..9683451 100644 --- a/src/jetzig/loggers/TestLogger.zig +++ b/src/jetzig/loggers/TestLogger.zig @@ -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 => {}, } } diff --git a/src/jetzig/middleware.zig b/src/jetzig/middleware.zig index 9c01558..e3e5b20 100644 --- a/src/jetzig/middleware.zig +++ b/src/jetzig/middleware.zig @@ -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, diff --git a/src/jetzig/middleware/AuthMiddleware.zig b/src/jetzig/middleware/AuthMiddleware.zig index 122273b..22f8b35 100644 --- a/src/jetzig/middleware/AuthMiddleware.zig +++ b/src/jetzig/middleware/AuthMiddleware.zig @@ -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(); diff --git a/src/jetzig/testing/App.zig b/src/jetzig/testing/App.zig index b58e14f..7230b67 100644 --- a/src/jetzig/testing/App.zig +++ b/src/jetzig/testing/App.zig @@ -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. + }; +} diff --git a/src/test_runner.zig b/src/test_runner.zig index a936d37..0982b82 100644 --- a/src/test_runner.zig +++ b/src/test_runner.zig @@ -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); } }