├── tests
├── empty
├── fs
│ ├── single_char
│ ├── sub-1
│ │ └── file-1
│ ├── sub-2
│ │ └── file-2
│ ├── test_struct.json
│ ├── lines
│ └── long_line
└── large
├── .gitignore
├── docs
├── .gitignore
├── src
│ ├── assets
│ │ └── favicon.png
│ ├── _data
│ │ └── env.js
│ ├── json_string.html
│ ├── index.njk
│ ├── sort.html
│ ├── readme.njk
│ ├── fs
│ │ ├── readjson.html
│ │ └── readdir.html
│ ├── thread_pool.html
│ ├── uuid.html
│ ├── pool.html
│ ├── command_line_args.html
│ ├── _includes
│ │ └── site.njk
│ ├── scheduler.html
│ ├── benchmark.html
│ ├── testing.html
│ ├── datetime.html
│ └── http
│ │ └── client.html
├── package.json
└── .eleventy.js
├── Makefile
├── LICENSE
├── src
├── zul.zig
├── sort.zig
├── benchmark.zig
├── thread_pool.zig
├── pool.zig
├── testing.zig
├── scheduler.zig
├── fs.zig
├── command_line_args.zig
├── arc.zig
├── uuid.zig
├── ulid.zig
└── context.zig
├── test_runner.zig
└── readme.md
/tests/empty:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/fs/single_char:
--------------------------------------------------------------------------------
1 | l
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | zig-out/
2 | .zig-cache/
3 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | _site
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/tests/fs/sub-1/file-1:
--------------------------------------------------------------------------------
1 | a file for testing recursive fs iterator
2 |
--------------------------------------------------------------------------------
/tests/fs/sub-2/file-2:
--------------------------------------------------------------------------------
1 | a file for testing recursive fs iterator
2 |
--------------------------------------------------------------------------------
/docs/src/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlseguin/zul/HEAD/docs/src/assets/favicon.png
--------------------------------------------------------------------------------
/tests/fs/test_struct.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 9001,
3 | "name": "Goku",
4 | "tags": ["a", "b", "c"]
5 | }
6 |
--------------------------------------------------------------------------------
/docs/src/_data/env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | prod: process.env.ENV == 'prod',
3 | baseURL: process.env.BASE_URL || '',
4 | }
5 |
--------------------------------------------------------------------------------
/tests/fs/lines:
--------------------------------------------------------------------------------
1 | Consider Phlebas
2 | Old Man's War
3 | Hyperion
4 | Under Heaven
5 | Project Hail Mary
6 | Roadside Picnic
7 | The Fifth Season
8 | Sundiver
9 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | F=
2 |
3 | .PHONY: t
4 | t:
5 | TEST_FILTER="${F}" zig build test --summary all -freference-trace
6 |
7 | .phony: d
8 | d:
9 | cd docs && npx @11ty/eleventy --serve --port 5300
10 |
--------------------------------------------------------------------------------
/tests/fs/long_line:
--------------------------------------------------------------------------------
1 | Accountability without giving control isn't real. When that comes with cognitive overload, you get burnout. When it stems from arbitrary timelines and schedules made up by people who have the control but not the accountability, you get resentment exit interviews.
2 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zul html documentation",
3 | "version": "1.0.0",
4 | "description": "",
5 | "keywords": [],
6 | "author": "",
7 | "devDependencies": {
8 | "@11ty/eleventy": "^2.0.1",
9 | "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0",
10 | "@grimlink/eleventy-plugin-sass": "^1.0.3",
11 | "cheerio": "^1.0.0-rc.12",
12 | "markdown-it-anchor": "^8.6.7",
13 | "markdown-it-attrs": "^4.1.6",
14 | "sass": "^1.69.5"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/docs/src/json_string.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: site.njk
3 | title: zul.JsonString
4 | ---
5 |
6 |
7 | Allows the embedding of already-encoded JSON strings into objects in order to avoid double encoded values.
8 |
9 |
10 | {% highlight zig %}
11 | const an_encoded_json_value = "{\"over\": 9000}";
12 | const str = try std.json.stringifyAlloc(allocator, .{
13 | .name = "goku",
14 | .power = zul.jsonString(an_encoded_json_value),
15 | }, .{});
16 | {% endhighlight %}
17 |
18 |
19 | zul.JsonString wraps a []const u8 and implements jsonStringify to write the value as-is. This prevents double-encoding of already-encoded values.
20 |
21 | Use the zul.jsonString(str) helper to create a JsonString.
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (Expat)
2 |
3 | Copyright (c) Karl Seguin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/.eleventy.js:
--------------------------------------------------------------------------------
1 | const sass = require('sass');
2 | const cheerio = require('cheerio');
3 | const pluginSass = require('@grimlink/eleventy-plugin-sass');
4 | const syntaxHighlight = require('@11ty/eleventy-plugin-syntaxhighlight');
5 |
6 | const env = require('./src/_data/env.js');
7 | const package = require('./package.json');
8 |
9 | module.exports = function(config) {
10 | config.addPassthroughCopy('src/assets/docs.js');
11 | config.addPassthroughCopy('src/assets/favicon.png');
12 | config.setTemplateFormats(['html', 'njk']);
13 |
14 | config.addCollection("sorted", function(collectionApi) {
15 | return collectionApi.getAll().sort(function(a, b) {
16 | return a.url.localeCompare(b.url);
17 | });
18 | });
19 |
20 | config.addPlugin(syntaxHighlight);
21 | config.addPlugin(pluginSass, {
22 | sass: sass,
23 | outputPath: '/assets/',
24 | outputStyle: (env.prod) ? 'compressed' : 'expanded',
25 | });
26 |
27 | config.addNunjucksGlobal('postMeta', function(post) {
28 | const $ = cheerio.load(post.content);
29 | const example = $('pre').eq(0);
30 | return {
31 | desc: $('p').eq(0).text(),
32 | example: {raw: example.text(), html: example.prop('outerHTML')},
33 | };
34 | });
35 |
36 | config.addAsyncFilter('asset_url', async function(url) {
37 | return env.baseURL + '/assets/' + url + '?v=' + package.version;
38 | });
39 |
40 | return {
41 | dir: {
42 | input: 'src'
43 | }
44 | };
45 | };
46 |
--------------------------------------------------------------------------------
/docs/src/index.njk:
--------------------------------------------------------------------------------
1 | ---
2 | id: home
3 | layout: site.njk
4 | title: Zig Utility Library
5 | eleventyExcludeFromCollections: true
6 | ---
7 | {% block head %}
8 |
16 | {% endblock %}
17 | Zig Utility Library
18 | The purpose of this library is to enhance Zig's standard library. Much of zul wraps Zig's std to provide simpler APIs for common tasks (e.g. reading lines from a file). In other cases, new functionality has been added (e.g. a UUID type).
19 |
20 | Besides Zig's standard library, there are no dependencies. Most functionality is contained within its own file and can easily be copy and pasted into an existing library or project.
21 |
22 | {%- for post in collections.sorted %}
23 | {%- set meta = postMeta(post) -%}
24 |
25 |
26 |
{{ post.data.title }}
27 |
{{ meta.desc }}
28 |
29 |
example
30 |
docs
31 |
32 | {{ meta.example.html | safe }}
33 | {%- endfor -%}
34 |
35 |
46 |
--------------------------------------------------------------------------------
/docs/src/sort.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: site.njk
3 | title: zul.sort
4 | ---
5 |
6 |
7 | Helpers for sorting strings and integers
8 |
9 |
10 | {% highlight zig %}
11 | // sorting strings based on their bytes
12 | var values = [_][]const u8{"ABC", "abc", "Dog", "Cat", "horse", "chicken"};
13 | zul.sort.strings(&values, .asc);
14 |
15 | // sort ASCII strings, ignoring case
16 | zul.sort.asciiIgnoreCase(&values, .desc);
17 |
18 | // sort integers or floats
19 | var numbers = [_]i32{10, -20, 33, 0, 2, 6};
20 | zul.sort.numbers(i32, &numbers, .asc);
21 | {% endhighlight %}
22 |
23 |
24 |
25 |
26 |
27 |
sort.strings(values: [][]const u8, direction: Direction) void
28 |
29 |
Sorts the values in-place using byte-comparison. The sort is unstable (i.e. there is no guarantee about how duplicate values are ordered with respect to each other). Direction can be .asc or .desc
30 |
31 |
32 |
33 |
sort.asciiIgnoreCase(values: [][]const u8, direction: Direction) void
34 |
35 |
Sorts the values in-place, ignoring ASCII casing. The sort is unstable (i.e. there is no guarantee about how duplicate values are ordered with respect to each other). Direction can be .asc or .desc
36 |
37 |
38 |
39 |
sort.number(comptime T: type, values: []T, direction: Direction) void
40 |
41 |
Sorts the values in-place. The sort is unstable (i.e. there is no guarantee about how duplicate values are ordered with respect to each other). Direction can be .asc or .desc. T must be an integer or float type (i.e. i32, f64).
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/docs/src/readme.njk:
--------------------------------------------------------------------------------
1 | ---
2 | title: Zig Utility Library
3 | eleventyExcludeFromCollections: true
4 | permalink: "../readme.md"
5 | permalinkBypassOutputDir: true
6 | ---
7 | # Zig Utility Library
8 | The purpose of this library is to enhance Zig's standard library. Much of zul wraps Zig's std to provide simpler APIs for common tasks (e.g. reading lines from a file). In other cases, new functionality has been added (e.g. a UUID type).
9 |
10 | Besides Zig's standard library, there are no dependencies. Most functionality is contained within its own file and can be copy and pasted into an existing library or project.
11 |
12 | Full documentation is available at: [https://www.goblgobl.com/zul/](https://www.goblgobl.com/zul/).
13 |
14 | (This readme is auto-generated from [docs/src/readme.njk](https://github.com/karlseguin/zul/blob/master/docs/src/readme.njk))
15 |
16 | ## Usage
17 | In your build.zig.zon add a reference to Zul:
18 |
19 | ```zig
20 | .{
21 | .name = "my-app",
22 | .paths = .{""},
23 | .version = "0.0.0",
24 | .dependencies = .{
25 | .zul = .{
26 | .url = "https://github.com/karlseguin/zul/archive/master.tar.gz",
27 | .hash = "$INSERT_HASH_HERE"
28 | },
29 | },
30 | }
31 | ```
32 |
33 | To get the hash, run:
34 |
35 | ```bash
36 | zig fetch https://github.com/karlseguin/zul/archive/master.tar.gz
37 | ```
38 |
39 | Instead of `master` you can use a specific commit/tag.
40 |
41 | Next, in your `build.zig`, you should already have an executable, something like:
42 |
43 | ```zig
44 | const exe = b.addExecutable(.{
45 | .name = "my-app",
46 | .root_source_file = b.path("src/main.zig"),
47 | .target = target,
48 | .optimize = optimize,
49 | });
50 | ```
51 |
52 | Add the following line:
53 |
54 | ```zig
55 | exe.root_module.addImport("zul", b.dependency("zul", .{}).module("zul"));
56 | ```
57 |
58 | You can now `const zul = @import("zul");` in your project.
59 |
60 | {% for post in collections.sorted %}
61 | {%- set meta = postMeta(post) -%}
62 | ## [{{ post.data.title }}](https://www.goblgobl.com/zul{{post.url}})
63 | {{ meta.desc }}
64 |
65 | ```zig
66 | {{meta.example.raw | safe}}
67 | ```
68 |
69 | {% endfor %}
70 |
--------------------------------------------------------------------------------
/src/zul.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | pub const fs = @import("fs.zig");
4 | pub const sort = @import("sort.zig");
5 | pub const http = @import("http.zig");
6 | pub const pool = @import("pool.zig");
7 | pub const testing = @import("testing.zig");
8 | pub const benchmark = @import("benchmark.zig");
9 |
10 | pub const UUID = @import("uuid.zig").UUID;
11 | pub const Scheduler = @import("scheduler.zig").Scheduler;
12 | pub const ThreadPool = @import("thread_pool.zig").ThreadPool;
13 | pub const StringBuilder = @import("string_builder.zig").StringBuilder;
14 | pub const CommandLineArgs = @import("command_line_args.zig").CommandLineArgs;
15 | pub const LockRefArc = @import("arc.zig").LockRefArc;
16 | pub const LockRefArenaArc = @import("arc.zig").LockRefArenaArc;
17 |
18 | const datetime = @import("datetime.zig");
19 | pub const Date = datetime.Date;
20 | pub const Time = datetime.Time;
21 | pub const DateTime = datetime.DateTime;
22 |
23 | pub fn Managed(comptime T: type) type {
24 | return struct {
25 | value: T,
26 | arena: *std.heap.ArenaAllocator,
27 |
28 | const Self = @This();
29 |
30 | pub fn fromJson(parsed: std.json.Parsed(T)) Self {
31 | return .{
32 | .arena = parsed.arena,
33 | .value = parsed.value,
34 | };
35 | }
36 |
37 | pub fn deinit(self: Self) void {
38 | const arena = self.arena;
39 | const allocator = arena.child_allocator;
40 | arena.deinit();
41 | allocator.destroy(arena);
42 | }
43 | };
44 | }
45 |
46 | pub fn jsonString(raw: []const u8) JsonString {
47 | return .{ .raw = raw };
48 | }
49 |
50 | pub const JsonString = struct {
51 | raw: []const u8,
52 |
53 | pub fn jsonStringify(self: JsonString, jws: anytype) !void {
54 | return jws.print("{s}", .{self.raw});
55 | }
56 | };
57 |
58 | test {
59 | @import("std").testing.refAllDecls(@This());
60 | }
61 |
62 | const t = testing;
63 | test "JsonString" {
64 | const str = try std.json.Stringify.valueAlloc(t.allocator, .{
65 | .data = jsonString("{\"over\": 9000}"),
66 | }, .{});
67 | defer t.allocator.free(str);
68 | try t.expectEqual("{\"data\":{\"over\": 9000}}", str);
69 | }
70 |
--------------------------------------------------------------------------------
/docs/src/fs/readjson.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: site.njk
3 | title: zul.fs.readJson
4 | ---
5 |
6 |
7 | Reads and parses a JSON file.
8 |
9 |
10 | {% highlight zig %}
11 | // Parameters:
12 | // 1- The type to parse the JSON data into
13 | // 2- An allocator
14 | // 3- Absolute or relative path
15 | // 4- std.json.ParseOptions
16 | const managed_user = try zul.fs.readJson(User, allocator, "/tmp/data.json", .{});
17 |
18 | // readJson returns a zul.Managed(T)
19 | // managed_user.value is valid until managed_user.deinit() is called
20 | defer managed_user.deinit();
21 | const user = managed_user.value;
22 | {% endhighlight %}
23 |
24 |
25 |
26 |
27 |
comptime T: type
28 |
29 |
30 |
31 |
allocator: std.mem.Allocator
32 |
The allocator to use for any memory allocations needed to parse the JSON or create T.
33 |
34 |
35 |
path: []const u8
36 |
Absolute or relative path to the file.
37 |
38 |
39 |
opts: std.json.ParseOptions
40 |
41 |
Options that control the parsing. The allocate field will be forced to alloc_always.
42 |
43 |
44 |
45 |
46 |
47 | On success, readJson(T, ...) returns a zul.Managed(T) which exposes a deinit() method as well as the the parsed value in the value: T field.
48 |
49 | Parsing JSON and creating T likely requires memory allocations. These allocations are done within an std.heap.ArenaAllocator. Thus, the parsed value: T has a lifetime tied to the arena. When zul.Manage(T).deinit() is called, the arena is cleared and freed.
50 |
51 | zul.Manage is a renamed std.json.Parsed(T) (I dislike the name std.json.Parsed(T) because it represents data and behavior that has nothing to with with JSON or parsing).
52 |
--------------------------------------------------------------------------------
/docs/src/thread_pool.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: site.njk
3 | title: zul.ThreadPool
4 | ---
5 |
6 |
7 | Lightweight thread pool with back-pressure and zero allocations after initialization.
8 |
9 | The standard library's std.Thread.Pool is designed for large jobs. As such, each scheduled job has non-trivial overhead.
10 |
11 |
12 | {% highlight zig %}
13 | var tp = try zul.ThreadPool(someTask).init(allocator, .{.count = 4, .backlog = 500});
14 | defer tp.deinit(allocator);
15 |
16 | // This will block if the threadpool has 500 pending jobs
17 | // where 500 is the configured backlog
18 | tp.spawn(.{1, true});
19 |
20 |
21 | fn someTask(i: i32, allow: bool) void {
22 | // process
23 | }
24 | {% endhighlight %}
25 |
26 |
27 | zul.ThreadPool(comptime Fn: type) is a simple and memory efficient way to have pre-initialized threads ready to process incoming work. The ThreadPool is a generic and takes the function to execute as a parameter.
28 |
29 |
30 |
31 |
32 |
init(...) !*Self
33 | {% highlight zig %}
34 | fn init(
35 | // Allocator is used to create the thread pool, no allocations occur after `init` returns.
36 | allocator: std.mem.Allocator,
37 |
38 | opts: .{
39 | // number of threads
40 | .count: u32 = 1,
41 |
42 | // The number of pending jobs to allow before callers are blocked.
43 | // The library will allocate an array of this size to hold all pending
44 | // parameters.
45 | .backlog: u32 = 500,
46 |
47 | }
48 | ) !*ThreadPool(Fn)
49 | {% endhighlight %}
50 |
51 |
Creates a zul.ThreadPool(Fn).
52 |
53 |
54 |
60 |
66 |
67 |
--------------------------------------------------------------------------------
/src/sort.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | pub const Direction = enum {
4 | asc,
5 | desc,
6 |
7 | pub fn toOrder(self: Direction) std.math.Order {
8 | return switch (self) {
9 | .asc => .lt,
10 | .desc => .gt,
11 | };
12 | }
13 | };
14 |
15 | pub fn strings(values: [][]const u8, direction: Direction) void {
16 | std.mem.sortUnstable([]const u8, values, direction.toOrder(), struct {
17 | fn order(d: std.math.Order, lhs: []const u8, rhs: []const u8) bool {
18 | return std.mem.order(u8, lhs, rhs) == d;
19 | }
20 | }.order);
21 | }
22 |
23 | pub fn asciiIgnoreCase(values: [][]const u8, direction: Direction) void {
24 | std.mem.sortUnstable([]const u8, values, direction.toOrder(), struct {
25 | fn order(d: std.math.Order, lhs: []const u8, rhs: []const u8) bool {
26 | return std.ascii.orderIgnoreCase(lhs, rhs) == d;
27 | }
28 | }.order);
29 | }
30 |
31 | pub fn numbers(comptime T: type, values: []T, direction: Direction) void {
32 | switch (direction) {
33 | .asc => std.mem.sortUnstable(T, values, {}, std.sort.asc(T)),
34 | .desc => std.mem.sortUnstable(T, values, {}, std.sort.desc(T)),
35 | }
36 | }
37 |
38 | const t = @import("zul.zig").testing;
39 | test "sort: string / ascii" {
40 | var values = [_][]const u8{ "ABC", "abc", "Dog", "Cat", "horse", "chicken" };
41 | strings(&values, .asc);
42 | try t.expectEqual("ABC", values[0]);
43 | try t.expectEqual("Cat", values[1]);
44 | try t.expectEqual("Dog", values[2]);
45 | try t.expectEqual("abc", values[3]);
46 | try t.expectEqual("chicken", values[4]);
47 | try t.expectEqual("horse", values[5]);
48 |
49 | strings(&values, .desc);
50 | try t.expectEqual("ABC", values[5]);
51 | try t.expectEqual("Cat", values[4]);
52 | try t.expectEqual("Dog", values[3]);
53 | try t.expectEqual("abc", values[2]);
54 | try t.expectEqual("chicken", values[1]);
55 | try t.expectEqual("horse", values[0]);
56 |
57 | asciiIgnoreCase(&values, .asc);
58 | try t.expectEqual("abc", values[0]);
59 | try t.expectEqual("ABC", values[1]);
60 | try t.expectEqual("Cat", values[2]);
61 | try t.expectEqual("chicken", values[3]);
62 | try t.expectEqual("Dog", values[4]);
63 | try t.expectEqual("horse", values[5]);
64 |
65 | asciiIgnoreCase(&values, .desc);
66 | try t.expectEqual("ABC", values[5]);
67 | try t.expectEqual("abc", values[4]);
68 | try t.expectEqual("Cat", values[3]);
69 | try t.expectEqual("chicken", values[2]);
70 | try t.expectEqual("Dog", values[1]);
71 | try t.expectEqual("horse", values[0]);
72 | }
73 |
74 | test "sort: numbers" {
75 | var values = [_]i32{ 10, -20, 33, 0, 2, 6 };
76 | numbers(i32, &values, .asc);
77 | try t.expectEqual(-20, values[0]);
78 | try t.expectEqual(0, values[1]);
79 | try t.expectEqual(2, values[2]);
80 | try t.expectEqual(6, values[3]);
81 | try t.expectEqual(10, values[4]);
82 | try t.expectEqual(33, values[5]);
83 |
84 | numbers(i32, &values, .desc);
85 | try t.expectEqual(-20, values[5]);
86 | try t.expectEqual(0, values[4]);
87 | try t.expectEqual(2, values[3]);
88 | try t.expectEqual(6, values[2]);
89 | try t.expectEqual(10, values[1]);
90 | try t.expectEqual(33, values[0]);
91 | }
92 |
--------------------------------------------------------------------------------
/docs/src/uuid.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: site.njk
3 | title: zul.UUID
4 | ---
5 |
6 |
7 | Parse and generate version 4 and version 7 UUIDs.
8 |
9 |
10 | {% highlight zig %}
11 | // v4() returns a zul.UUID
12 | const uuid1 = zul.UUID.v4();
13 |
14 | // toHex() returns a [36]u8
15 | const hex = uuid1.toHex(.lower);
16 |
17 | // returns a zul.UUID (or an error)
18 | const uuid2 = try zul.UUID.parse("761e3a9d-4f92-4e0d-9d67-054425c2b5c3");
19 | std.debug.print("{any}\n", uuid1.eql(uuid2));
20 |
21 | // create a UUIDv7
22 | const uuid3 = zul.UUID.v7();
23 |
24 | // zul.UUID can be JSON serialized
25 | try std.json.stringify(.{.id = uuid3}, .{}, writer);
26 | {% endhighlight %}
27 |
28 |
29 | zul.UUID is a thin wrapper around a [16]u8. Its main purpose is to generate a hex-encoded version of the UUID.
30 |
31 |
32 |
33 |
34 |
bin: [16]u8
35 |
36 |
The binary representation of the UUID.
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
UUID.v4() UUID
45 |
Generate a version 4 (random) UUID.
46 |
47 |
48 |
UUID.v7() UUID
49 |
Generate a version 7 UUID.
50 |
51 |
52 |
UUID.random() UUID
53 |
54 |
Non-compliant pseudo-UUID. Does not have a version or variant. Use UUID.v4() or UUID.v7() unless you have specific reasons not to.
55 |
56 |
57 |
58 |
UUID.parse(hex: []const u8) !UUID
59 |
60 |
Attempts to parse a hex-encoded UUID. Returns error.InvalidUUID is the UUID is not valid.
61 |
62 |
63 |
64 |
UUID.bin2Hex(bin: []const u8, case: Case) ![36]iu8
65 |
66 |
Hex encodes a 16 byte binary UUID.
67 |
68 |
69 |
70 |
71 |
72 |
toHex(uuid: UUID, case: Case) [36]u8
73 |
74 |
Hex-encodes the UUID. The case parameter must be .lower or .upper and controls whether lowercase or uppercase hexadecimal is used.
75 |
This method should be preferred over the other toHex* variants.
76 |
77 |
78 |
84 |
90 |
--------------------------------------------------------------------------------
/src/benchmark.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const Timer = std.time.Timer;
4 | const Allocator = std.mem.Allocator;
5 |
6 | pub const Opts = struct {
7 | samples: u32 = 10_000,
8 | runtime: usize = 3 * std.time.ms_per_s,
9 | };
10 |
11 | pub fn Result(comptime SAMPLE_COUNT: usize) type {
12 | return struct {
13 | total: u64,
14 | iterations: u64,
15 | requested_bytes: usize,
16 | // sorted, use samples()
17 | _samples: [SAMPLE_COUNT]u64,
18 |
19 | const Self = @This();
20 |
21 | pub fn print(self: *const Self, name: []const u8) void {
22 | std.debug.print("{s}\n", .{name});
23 | std.debug.print(" {d} iterations\t{d:.2}ns per iterations\n", .{ self.iterations, self.mean() });
24 | std.debug.print(" {d:.2} bytes per iteration\n", .{self.requested_bytes / self.iterations});
25 | std.debug.print(" worst: {d}ns\tmedian: {d:.2}ns\tstddev: {d:.2}ns\n\n", .{ self.worst(), self.median(), self.stdDev() });
26 | }
27 |
28 | pub fn samples(self: *const Self) []const u64 {
29 | return self._samples[0..@min(self.iterations, SAMPLE_COUNT)];
30 | }
31 |
32 | pub fn worst(self: *const Self) u64 {
33 | const s = self.samples();
34 | return s[s.len - 1];
35 | }
36 |
37 | pub fn mean(self: *const Self) f64 {
38 | const s = self.samples();
39 |
40 | var total: u64 = 0;
41 | for (s) |value| {
42 | total += value;
43 | }
44 | return @as(f64, @floatFromInt(total)) / @as(f64, @floatFromInt(s.len));
45 | }
46 |
47 | pub fn median(self: *const Self) u64 {
48 | const s = self.samples();
49 | return s[s.len / 2];
50 | }
51 |
52 | pub fn stdDev(self: *const Self) f64 {
53 | const m = self.mean();
54 | const s = self.samples();
55 |
56 | var total: f64 = 0.0;
57 | for (s) |value| {
58 | const t = @as(f64, @floatFromInt(value)) - m;
59 | total += t * t;
60 | }
61 | const variance = total / @as(f64, @floatFromInt(s.len - 1));
62 | return std.math.sqrt(variance);
63 | }
64 | };
65 | }
66 |
67 | pub fn run(func: TypeOfBenchmark(void), comptime opts: Opts) !Result(opts.samples) {
68 | return runC({}, func, opts);
69 | }
70 |
71 | pub fn runC(context: anytype, func: TypeOfBenchmark(@TypeOf(context)), comptime opts: Opts) !Result(opts.samples) {
72 | var gpa = std.heap.GeneralPurposeAllocator(.{ .enable_memory_limit = true }){};
73 | const allocator = gpa.allocator();
74 |
75 | const sample_count = opts.samples;
76 | const run_time = opts.runtime * std.time.ns_per_ms;
77 |
78 | var total: u64 = 0;
79 | var iterations: usize = 0;
80 | var timer = try Timer.start();
81 | var samples = std.mem.zeroes([sample_count]u64);
82 |
83 | while (true) {
84 | iterations += 1;
85 | timer.reset();
86 |
87 | if (@TypeOf(context) == void) {
88 | try func(allocator, &timer);
89 | } else {
90 | try func(context, allocator, &timer);
91 | }
92 | const elapsed = timer.lap();
93 |
94 | total += elapsed;
95 | samples[@mod(iterations, sample_count)] = elapsed;
96 | if (total > run_time) break;
97 | }
98 |
99 | std.sort.heap(u64, samples[0..@min(sample_count, iterations)], {}, resultLessThan);
100 |
101 | return .{
102 | .total = total,
103 | ._samples = samples,
104 | .iterations = iterations,
105 | .requested_bytes = gpa.total_requested_bytes,
106 | };
107 | }
108 |
109 | fn TypeOfBenchmark(comptime C: type) type {
110 | return switch (C) {
111 | void => *const fn (Allocator, *Timer) anyerror!void,
112 | else => *const fn (C, Allocator, *Timer) anyerror!void,
113 | };
114 | }
115 |
116 | fn resultLessThan(context: void, lhs: u64, rhs: u64) bool {
117 | _ = context;
118 | return lhs < rhs;
119 | }
120 |
--------------------------------------------------------------------------------
/docs/src/fs/readdir.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: site.njk
3 | title: zul.fs.readDir
4 | ---
5 |
6 |
7 | Iterates, non-recursively, through a directory.
8 |
9 | This is a thin abstraction over the standard libraries IterableDir.iterate() behavior. The main benefit of zul.fs.readDir is the ability collect all entries, sorted or not, in a slice.
10 |
11 |
12 | {% highlight zig %}
13 | // Parameters:
14 | // 1- Absolute or relative directory path
15 | var it = try zul.fs.readDir("/tmp/dir");
16 | defer it.deinit();
17 |
18 | // can iterate through the files
19 | while (try it.next()) |entry| {
20 | std.debug.print("{s} {any}\n", .{entry.name, entry.kind});
21 | }
22 |
23 | // reset the iterator
24 | it.reset();
25 |
26 | // or can collect them into a slice, optionally sorted:
27 | const sorted_entries = try it.all(allocator, .dir_first);
28 | for (sorted_entries) |entry| {
29 | std.debug.print("{s} {any}\n", .{entry.name, entry.kind});
30 | }
31 | {% endhighlight %}
32 |
33 |
34 |
35 |
36 |
path: []const u8
37 |
Absolute or relative path to the directory.
38 |
39 |
40 |
41 |
42 | On success, readDir returns an Iterator.
43 |
44 |
45 |
46 |
52 |
53 |
all(self: *Iterator, allocator: Allocator, sort: Sort) ![]Entry
54 |
55 |
Gets all remaining directory entries. sort can be one of four values:
56 |
57 | none - no sorting.
58 | alphabetic - sorted alphabetically.
59 | dir_first - sorted alphabetically with directories first.
60 | dir_last - sorted alphabetically with directories last.
61 |
62 |
63 |
Normally, the entry.name is only valid up until the next iteration. In order to collect all entries, this function clones the names. Internally this, along with the std.ArrayList used to collect the entries, are managed by an ArenaAllocator, which is cleared on deinit . Compared to simply iterating through the entries one at a time, this adds considerable overhead. But, if you need all entries, sorted or otherwise, this cloning is necessary. If you don't, prefer using the standard libraries std.fs.IterableDir directly.
64 |
65 |
66 |
67 |
next(self: *Iterator) !?Entry
68 |
69 |
Returns the next directory entry, or null if there are no more entries.
70 |
71 |
The returned entry is only valid until the next call to next, deinit or reset.
72 |
73 |
The order of iteration depends on the file system, but generally no guarantee is made. Whether or not entries added/removed during iteration are seen by the iterator depends also depends on the file system.
74 |
75 |
76 |
77 |
reset(self: *Iterator) void
78 |
79 |
Resets the iterator. Once reset, the iterator can be iterated again from the start.
80 |
81 |
82 |
83 |
84 |
85 | Entry is an std.fs.IterableDir.Entry which has two fields: name: []const u8 and kind: std.fs.File.Kind.
86 |
--------------------------------------------------------------------------------
/docs/src/pool.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: site.njk
3 | title: zul.pool
4 | ---
5 |
6 |
7 | A thread-safe object pool which will dynamically grow when empty and revert to the configured size.
8 |
9 |
10 | {% highlight zig %}
11 | // create a pool for our Expensive class.
12 | // Our Expensive class takes a special initializing context, here an usize which
13 | // we set to 10_000. This is just to pass data from the pool into Expensive.init
14 | var pool = try zul.pool.Growing(Expensive, usize).init(allocator, 10_000, .{.count = 100});
15 | defer pool.deinit();
16 |
17 | // acquire will either pick an item from the pool
18 | // if the pool is empty, it'll create a new one (hence, "Growing")
19 | var exp1 = try pool.acquire();
20 | defer pool.release(exp1);
21 |
22 | ...
23 |
24 | // pooled object must have 3 functions
25 | const Expensive = struct {
26 | // an init function
27 | pub fn init(allocator: Allocator, size: usize) !Expensive {
28 | return .{
29 | // ...
30 | };
31 | }
32 |
33 | // a deinit method
34 | pub fn deinit(self: *Expensive) void {
35 | // ...
36 | }
37 |
38 | // a reset method, called when the item is released back to the pool
39 | pub fn reset(self: *Expensive) void {
40 | // ...
41 | }
42 | };
43 | {% endhighlight %}
44 |
45 | fn Growing(comptime T: type, comptime C: type) Growing(T, C)
46 | The Growing pool is a generic function that takes two parameters. T is the type of object being pool. C is the type of data to pass into T.init. In many cases, C will be void, in which case T.init will not receive the value:
47 |
48 | {% highlight zig %}
49 | var pool = try zul.pool.Growing(Expensive, void).init(allocator, {}, .{.count = 100});
50 | defer pool.deinit();
51 |
52 | ...
53 |
54 | const Expensive = struct {
55 | // Because the context was defined as `void`, the `init` function does not
56 | // take a 2nd paremeter.
57 | pub fn init(allocator: Allocator) !Expensive {
58 | return .{
59 | // ...
60 | };
61 | }
62 |
63 | // ...
64 | };
65 | {% endhighlight %}
66 |
67 | T must have an init(allocator: Allocator, ctx: C) !T function. It must also have the following two methods: deinit(self: *T) void and reset(self: *T) void. Because the pool will dynamically create T when empty, deinit will be called when items are released back into a full pool (as well as when pool.deinit is called). reset is called whenever an item is released back into the pool.
68 |
69 |
70 |
71 |
72 |
73 |
init(...) !Growing
74 | {% highlight zig %}
75 | fn init(
76 | // Allocator is used to create the pool, create the pooled items, and is passed
77 | // to the T.init
78 | allocator: std.mem.Allocator,
79 |
80 | // An arbitrary context to passed to T.init
81 | ctx: C
82 |
83 | opts: .{
84 | // number of items to keep in the pool
85 | .count: usize,
86 | }
87 | ) !Growing(T, C)
88 | {% endhighlight %}
89 |
90 |
Creates a pool.Growing.
91 |
92 |
93 |
99 |
100 |
acquire(self: *Growing(T, C)) !*T
101 |
102 |
Returns an *T. When available, *T will be retrieved from the pooled objects. When the pool is empty, a new *T is created.
103 |
104 |
105 |
111 |
112 |
--------------------------------------------------------------------------------
/docs/src/command_line_args.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: site.njk
3 | title: zul.CommandLineArgs
4 | ---
5 |
6 |
7 | A simple command line parser.
8 |
9 |
10 | {% highlight zig %}
11 | var args = try zul.CommandLineArgs.parse(allocator);
12 | defer args.deinit();
13 |
14 | if (args.contains("version")) {
15 | //todo: print the version
16 | os.exit(0);
17 | }
18 |
19 | // Values retrieved from args.get are valid until args.deinit()
20 | // is called. Dupe the value if needed.
21 | const host = args.get("host") orelse "127.0.0.1";
22 | ...
23 | {% endhighlight %}
24 |
25 |
26 | zul.CommandLineArgs is a thin wrapper around std.process.argsWithAllocator which applies simple logic to parse key=value pairs into a StringHashMap([]const u8.
27 |
28 | 7 argument types are supported:
29 |
30 |
31 | --key value
32 | -k value
33 | --key=value
34 | -k=value
35 | --key
36 | -k
37 | -xvf value
38 |
39 |
40 | A key without a value, such as ./run --version or ./run -v will be given a empty string value ("").
41 |
42 | Parsing is simple. Keys begin with one or two dashes. If the parser sees ./run --key1 --key2 value2, it will load "key1" with a value of "", and "key2" with a value of "value2".
43 |
44 | Single-dash keys can be grouped together, with the last key being given the value, so parameters like ./run -xvf file.tar.gz work like you (probably) expect.
45 |
46 | Once the parser runs into a non key+value combo, all following arguments are treated as a "tail", which is a list of []const u8
47 |
48 |
49 |
50 |
51 |
exe: []const u8
52 |
53 |
The first command line argument is the path to the running executable.
54 |
55 |
56 |
57 |
tail: [][]const u8
58 |
59 |
A list of arguments starting from the first non key+value pair.
60 |
61 |
62 |
63 |
list: [][]const u8
64 |
65 |
A list of arguments. The first argument in the list is the path to the running executable. This is an exact collection of std.process.argsWithAllocator.
66 |
67 |
68 |
69 |
70 |
71 |
72 |
78 |
79 |
deinit(self: CommandLineArgs) void
80 |
81 |
Releases all memory related to the parsing. This includes any string returned by get or in the list or tail slice.
82 |
83 |
84 |
90 |
96 |
97 |
count(self: *const CommandLineArgs) u32
98 |
99 |
The number of key value pairs. args.count() + args.tail.len + 1 == args.list.len. The + 1 is for the path to the running executable which is included in list.
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/src/thread_pool.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const Thread = std.Thread;
4 | const Allocator = std.mem.Allocator;
5 |
6 | pub const Opts = struct {
7 | count: u32 = 1,
8 | backlog: u32 = 128,
9 | };
10 |
11 | pub fn ThreadPool(comptime F: anytype) type {
12 | const Args = std.meta.ArgsTuple(@TypeOf(F));
13 | return struct {
14 | stop: bool,
15 | push: usize,
16 | pull: usize,
17 | pending: usize,
18 | queue: []Args,
19 | threads: []Thread,
20 | mutex: Thread.Mutex,
21 | sem: Thread.Semaphore,
22 | cond: Thread.Condition,
23 | queue_end: usize,
24 |
25 | const Self = @This();
26 |
27 | pub fn init(allocator: Allocator, opts: Opts) !*Self {
28 | const queue = try allocator.alloc(Args, opts.backlog);
29 | errdefer allocator.free(queue);
30 |
31 | const threads = try allocator.alloc(Thread, opts.count);
32 | errdefer allocator.free(threads);
33 |
34 | const thread_pool = try allocator.create(Self);
35 | errdefer allocator.destroy(thread_pool);
36 |
37 | thread_pool.* = .{
38 | .pull = 0,
39 | .push = 0,
40 | .pending = 0,
41 | .cond = .{},
42 | .mutex = .{},
43 | .stop = false,
44 | .threads = threads,
45 | .queue = queue,
46 | .queue_end = queue.len - 1,
47 | .sem = .{ .permits = queue.len },
48 | };
49 |
50 | var started: usize = 0;
51 | errdefer {
52 | thread_pool.stop = true;
53 | thread_pool.cond.broadcast();
54 | for (0..started) |i| {
55 | threads[i].join();
56 | }
57 | }
58 |
59 | for (0..threads.len) |i| {
60 | threads[i] = try Thread.spawn(.{}, Self.worker, .{thread_pool});
61 | started += 1;
62 | }
63 |
64 | return thread_pool;
65 | }
66 |
67 | pub fn deinit(self: *Self, allocator: Allocator) void {
68 | self.mutex.lock();
69 | self.stop = true;
70 | self.mutex.unlock();
71 |
72 | self.cond.broadcast();
73 | for (self.threads) |thrd| {
74 | thrd.join();
75 | }
76 | allocator.free(self.threads);
77 | allocator.free(self.queue);
78 |
79 | allocator.destroy(self);
80 | }
81 |
82 | pub fn empty(self: *Self) bool {
83 | self.mutex.lock();
84 | defer self.mutex.unlock();
85 | return self.pull == self.push;
86 | }
87 |
88 | pub fn spawn(self: *Self, args: Args) !void {
89 | self.sem.wait();
90 | self.mutex.lock();
91 | const push = self.push;
92 | self.queue[push] = args;
93 | self.push = if (push == self.queue_end) 0 else push + 1;
94 | self.pending += 1;
95 | self.mutex.unlock();
96 | self.cond.signal();
97 | }
98 |
99 | fn worker(self: *Self) void {
100 | while (true) {
101 | self.mutex.lock();
102 | while (self.pending == 0) {
103 | if (self.stop) {
104 | self.mutex.unlock();
105 | return;
106 | }
107 | self.cond.wait(&self.mutex);
108 | }
109 | const pull = self.pull;
110 | const args = self.queue[pull];
111 | self.pull = if (pull == self.queue_end) 0 else pull + 1;
112 | self.pending -= 1;
113 | self.mutex.unlock();
114 | self.sem.post();
115 | @call(.auto, F, args);
116 | }
117 | }
118 | };
119 | }
120 |
121 | const t = @import("zul.zig").testing;
122 | test "ThreadPool: small fuzz" {
123 | testSum = 0; // global defined near the end of this file
124 | var tp = try ThreadPool(testIncr).init(t.allocator, .{ .count = 3, .backlog = 3 });
125 |
126 | for (0..50_000) |_| {
127 | try tp.spawn(.{1});
128 | }
129 | while (tp.empty() == false) {
130 | std.Thread.sleep(std.time.ns_per_ms);
131 | }
132 | tp.deinit(t.allocator);
133 | try t.expectEqual(50_000, testSum);
134 | }
135 |
136 | test "ThreadPool: large fuzz" {
137 | testSum = 0; // global defined near the end of this file
138 | var tp = try ThreadPool(testIncr).init(t.allocator, .{ .count = 50, .backlog = 1000 });
139 |
140 | for (0..50_000) |_| {
141 | try tp.spawn(.{1});
142 | }
143 | while (tp.empty() == false) {
144 | std.Thread.sleep(std.time.ns_per_ms);
145 | }
146 | tp.deinit(t.allocator);
147 | try t.expectEqual(50_000, testSum);
148 | }
149 |
150 | var testSum: u64 = 0;
151 | fn testIncr(c: u64) void {
152 | _ = @atomicRmw(u64, &testSum, .Add, c, .monotonic);
153 | }
154 |
--------------------------------------------------------------------------------
/docs/src/_includes/site.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ title }}
5 |
6 |
60 | {% block header %}{% endblock %}
61 |
67 |
68 | {{ content | safe }}
69 |
--------------------------------------------------------------------------------
/src/pool.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const Thread = std.Thread;
4 | const Allocator = std.mem.Allocator;
5 |
6 | pub const GrowingOpts = struct {
7 | count: usize,
8 | };
9 |
10 | pub fn Growing(comptime T: type, comptime C: type) type {
11 | return struct {
12 | _ctx: C,
13 | _items: []*T,
14 | _available: usize,
15 | _mutex: Thread.Mutex,
16 | _allocator: Allocator,
17 |
18 | const Self = @This();
19 |
20 | pub fn init(allocator: Allocator, ctx: C, opts: GrowingOpts) !Self {
21 | const count = opts.count;
22 |
23 | const items = try allocator.alloc(*T, count);
24 | errdefer allocator.free(items);
25 |
26 | var initialized: usize = 0;
27 | errdefer {
28 | for (0..initialized) |i| {
29 | items[i].deinit();
30 | allocator.destroy(items[i]);
31 | }
32 | }
33 |
34 | for (0..count) |i| {
35 | items[i] = try allocator.create(T);
36 | errdefer allocator.destroy(items[i]);
37 | items[i].* = if (C == void) try T.init(allocator) else try T.init(allocator, ctx);
38 | initialized += 1;
39 | }
40 |
41 | return .{
42 | ._ctx = ctx,
43 | ._mutex = .{},
44 | ._items = items,
45 | ._available = count,
46 | ._allocator = allocator,
47 | };
48 | }
49 |
50 | pub fn deinit(self: *Self) void {
51 | const allocator = self._allocator;
52 | for (self._items) |item| {
53 | item.deinit();
54 | allocator.destroy(item);
55 | }
56 | allocator.free(self._items);
57 | }
58 |
59 | pub fn acquire(self: *Self) !*T {
60 | const items = self._items;
61 |
62 | self._mutex.lock();
63 | const available = self._available;
64 | if (available == 0) {
65 | // dont hold the lock over factory
66 | self._mutex.unlock();
67 |
68 | const allocator = self._allocator;
69 | const item = try allocator.create(T);
70 | item.* = if (C == void) try T.init(allocator) else try T.init(allocator, self._ctx);
71 | return item;
72 | }
73 |
74 | const index = available - 1;
75 | const item = items[index];
76 | self._available = index;
77 | self._mutex.unlock();
78 | return item;
79 | }
80 |
81 | pub fn release(self: *Self, item: *T) void {
82 | item.reset();
83 |
84 | var items = self._items;
85 | self._mutex.lock();
86 | const available = self._available;
87 | if (available == items.len) {
88 | self._mutex.unlock();
89 | item.deinit();
90 | self._allocator.destroy(item);
91 | return;
92 | }
93 | items[available] = item;
94 | self._available = available + 1;
95 | self._mutex.unlock();
96 | }
97 | };
98 | }
99 |
100 | const t = @import("zul.zig").testing;
101 | test "pool: acquire and release" {
102 | var p = try Growing(TestPoolItem, void).init(t.allocator, {}, .{ .count = 2 });
103 | defer p.deinit();
104 |
105 | const i1a = try p.acquire();
106 | try t.expectEqual(0, i1a.data[0]);
107 | i1a.data[0] = 250;
108 |
109 | const i2a = try p.acquire();
110 | const i3a = try p.acquire(); // this should be dynamically generated
111 |
112 | try t.expectEqual(false, i1a.data.ptr == i2a.data.ptr);
113 | try t.expectEqual(false, i2a.data.ptr == i3a.data.ptr);
114 |
115 | p.release(i1a);
116 |
117 | const i1b = try p.acquire();
118 | try t.expectEqual(0, i1b.data[0]); // ensure we called reset
119 | try t.expectEqual(true, i1a.data.ptr == i1b.data.ptr);
120 |
121 | p.release(i3a);
122 | p.release(i2a);
123 | p.release(i1b);
124 | }
125 |
126 | test "pool: threadsafety" {
127 | var p = try Growing(TestPoolItem, void).init(t.allocator, {}, .{ .count = 3 });
128 | defer p.deinit();
129 |
130 | // initialize this to 0 since we're asserting that it's 0
131 | for (p._items) |item| {
132 | item.data[0] = 0;
133 | }
134 |
135 | const t1 = try std.Thread.spawn(.{}, testPool, .{&p});
136 | const t2 = try std.Thread.spawn(.{}, testPool, .{&p});
137 | const t3 = try std.Thread.spawn(.{}, testPool, .{&p});
138 |
139 | t1.join();
140 | t2.join();
141 | t3.join();
142 | }
143 |
144 | fn testPool(p: *Growing(TestPoolItem, void)) void {
145 | const random = t.Random.random();
146 |
147 | for (0..5000) |_| {
148 | var sb = p.acquire() catch unreachable;
149 | // no other thread should have set this to 255
150 | std.debug.assert(sb.data[0] == 0);
151 |
152 | sb.data[0] = 255;
153 | std.Thread.sleep(random.uintAtMost(u32, 100000));
154 | sb.data[0] = 0;
155 | p.release(sb);
156 | }
157 | }
158 |
159 | const TestPoolItem = struct {
160 | data: []u8,
161 | allocator: Allocator,
162 |
163 | fn init(allocator: Allocator) !TestPoolItem {
164 | const data = try allocator.alloc(u8, 1);
165 | data[0] = 0;
166 |
167 | return .{
168 | .data = data,
169 | .allocator = allocator,
170 | };
171 | }
172 |
173 | fn deinit(self: *TestPoolItem) void {
174 | self.allocator.free(self.data);
175 | }
176 |
177 | fn reset(self: *TestPoolItem) void {
178 | self.data[0] = 0;
179 | }
180 | };
181 |
--------------------------------------------------------------------------------
/docs/src/scheduler.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: site.njk
3 | title: zul.Scheduler
4 | ---
5 |
6 |
7 | Ephemeral thread-based task scheduler used to run tasks at a specific time.
8 |
9 |
10 | {% highlight zig %}
11 | // Where multiple types of tasks can be scheduled using the same schedule,
12 | // a tagged union is ideal.
13 | const Task = union(enum) {
14 | say: []const u8,
15 |
16 | // Whether T is a tagged union (as here) or another type, a public
17 | // run function must exist
18 | pub fn run(task: Task, ctx: void, at: i64) void {
19 | // the original time the task was scheduled for
20 | _ = at;
21 |
22 | // application-specific context that will be passed to each task
23 | _ ctx;
24 |
25 | switch (task) {
26 | .say => |msg| {std.debug.print("{s}\n", .{msg}),
27 | }
28 | }
29 | }
30 |
31 | ...
32 |
33 | // This example doesn't use a app-context, so we specify its
34 | // type as void
35 | var s = zul.Scheduler(Task, void).init(allocator);
36 | defer s.deinit();
37 |
38 | // Starts the scheduler, launching a new thread
39 | // We pass our context. Since we have a null context
40 | // we pass a null value, i.e. {}
41 | try s.start({});
42 |
43 | // will run the say task in 5 seconds
44 | try s.scheduleIn(.{.say = "world"}, std.time.ms_per_s * 5);
45 |
46 | // will run the say task in 100 milliseconds
47 | try s.schedule(.{.say = "hello"}, std.time.milliTimestamp() + 100);
48 | {% endhighlight %}
49 |
50 |
51 | zul.Scheduler(T) is a thread-safe way to schedule tasks for future execution. Tasks can be scheduled using the schedule(), scheduleIn() or scheduleAt() methods. These methods are thread-safe.
52 |
53 | The scheduler runs in its own thread (started by calling start()). Importantly, tasks are run within the scheduler thread and thus can delay each other. Consider having your tasks use zul.ThreadPool in more advanced cases.
54 |
55 | Tasks are not persisted (e.g. if the schedule or program crashes, scheduled jobs are lost). Otherwise, the scheduler never discards a task. Tasks that were scheduled in the past are run normally. Applications that care about stale tasks can use the last parameter passed to T.run which is the original scheduled timestamp (in milliseconds).
56 |
57 |
58 | T must have a run method. Two variants are supported:
59 |
60 |
71 |
72 |
73 |
74 |
80 |
81 |
deinit(self: *Scheduler(T)) void
82 |
83 |
Stops the task scheduler thread (if it was started) and deallocates the scheduler. This method is thread-safe.
84 |
85 |
86 |
87 |
start(self: *Scheduler(T), ctx: C) !void
88 |
89 |
Launches the task scheduler in a new thread. This method is thread-safe. An error is returned if start is called multiple times.
90 |
91 |
The provided ctx will be passed to T.run. In cases where no ctx is needed, a void context should be used, as shown in the initial example.
92 |
93 |
94 |
95 |
stop(self: *Scheduler(T)) void
96 |
97 |
Stops the task scheduler. This method is thread-safe. It is safe to call multiple times, even if the scheduler is not started. Since deinit calls this method, it is usually not necessary to call it.
98 |
99 |
100 |
106 |
112 |
118 |
119 |
--------------------------------------------------------------------------------
/docs/src/benchmark.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: site.njk
3 | title: zul.benchmark.run
4 | ---
5 |
6 |
7 | Simple benchmarking function.
8 |
9 |
10 | {% highlight zig %}
11 | const HAYSTACK = "abcdefghijklmnopqrstvuwxyz0123456789";
12 |
13 | pub fn main() !void {
14 | (try zul.benchmark.run(indexOfScalar, .{})).print("indexOfScalar");
15 | (try zul.benchmark.run(lastIndexOfScalar, .{})).print("lastIndexOfScalar");
16 | }
17 |
18 | fn indexOfScalar(_: Allocator, _: *std.time.Timer) !void {
19 | const i = std.mem.indexOfScalar(u8, HAYSTACK, '9').?;
20 | if (i != 35) {
21 | @panic("fail");
22 | }
23 | }
24 |
25 | fn lastIndexOfScalar(_: Allocator, _: *std.time.Timer) !void {
26 | const i = std.mem.lastIndexOfScalar(u8, HAYSTACK, 'a').?;
27 | if (i != 0) {
28 | @panic("fail");
29 | }
30 | }
31 |
32 | // indexOfScalar
33 | // 49882322 iterations 59.45ns per iterations
34 | // worst: 167ns median: 42ns stddev: 20.66ns
35 | //
36 | // lastIndexOfScalar
37 | // 20993066 iterations 142.15ns per iterations
38 | // worst: 292ns median: 125ns stddev: 23.13ns
39 | {% endhighlight %}
40 |
41 |
42 |
43 |
44 |
allocator: std.mem.Allocator
45 |
46 |
Provided for any allocation the function must make. When used, the Result will contain the requested_bytes.
47 |
48 |
49 |
50 |
timer: *std.time.Timer
51 |
52 |
In some cases, the function under benchmark might require setup that should not count towards the execution time. Use timer.reset() to reset the execution time to 0.
53 |
54 | {% highlight zig %}
55 | fn myfunc(_: Allocator, timer: *std.time.Timer) !void {
56 | // some expensive setup
57 | timer.reset();
58 | // code to benchmark
59 | }
60 | {% endhighlight %}
61 |
62 |
In most cases, it is better to use runC and provide a context.
63 |
64 |
65 |
66 |
opts: zul.benchmark.Opts
67 |
68 |
Options that control how the benchmark is run. Must be comptime-known.
69 |
70 | samples: u32 - The maximum number of samples to take for calculating metrics. Defaults to 10_000
71 | runtime: usize - The time, in milliseconds, to run the benchmark for. Defaults ot 3000 (3 seconds).
72 |
73 |
74 |
75 |
76 |
77 |
78 | A variant of run that passes arbitrary data to the benchmark function. For example, rather than relying on a global INPUT, our above example could leverage runC:
79 |
80 | {% highlight zig %}
81 | pub fn main() !void {
82 | const ctx = Context{
83 | .input = "abcdefghijklmnopqrstvuwxyz0123456789",
84 | };
85 |
86 | (try zul.benchmark.runC(ctx, indexOfScalar, .{})).print("indexOfScalar");
87 | (try zul.benchmark.runC(ctx, lastIndexOfScalar, .{})).print("lastIndexOfScalar");
88 | }
89 |
90 | const Context = struct{
91 | input: []const u8,
92 | };
93 |
94 | fn indexOfScalar(ctx: Context, _: Allocator, _: *std.time.Timer) !void {
95 | const i = std.mem.indexOfScalar(u8, ctx.input, '9').?;
96 | if (i != 35) {
97 | @panic("fail");
98 | }
99 | }
100 |
101 | fn lastIndexOfScalar(ctx: Context, _: Allocator, _: *std.time.Timer) !void {
102 | const i = std.mem.lastIndexOfScalar(u8, ctx.input, 'a').?;
103 | if (i != 0) {
104 | @panic("fail");
105 | }
106 | }
107 | {% endhighlight %}
108 |
109 |
110 | A zul.benchmark.Result(sample_size) is returned by the call to run or runC.
111 |
112 |
113 |
114 |
115 |
total: u64
116 |
117 |
Total time, in nanosecond, that the benchmark ran for. This can be greater than the sum of values in samples().
118 |
119 |
120 |
121 |
iterations: u64
122 |
123 |
Number of times the benchmarked function was called. This can be greater than samples().len.
124 |
125 |
126 |
127 |
requested_bytes: usize
128 |
129 |
Total number of bytes allocated by the allocator.
130 |
131 |
132 |
133 |
134 |
135 |
136 |
142 |
148 |
154 |
160 |
166 |
172 |
173 |
--------------------------------------------------------------------------------
/src/testing.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const builtin = @import("builtin");
3 |
4 | // std.testing.expectEqual won't coerce expected to actual, which is a problem
5 | // when expected is frequently a comptime.
6 | // https://github.com/ziglang/zig/issues/4437
7 | pub fn expectEqual(expected: anytype, actual: anytype) !void {
8 | switch (@typeInfo(@TypeOf(actual))) {
9 | .array => |arr| if (arr.child == u8) {
10 | return std.testing.expectEqualStrings(expected, &actual);
11 | },
12 | .pointer => |ptr| if (ptr.child == u8) {
13 | return std.testing.expectEqualStrings(expected, actual);
14 | } else if (comptime isStringArray(ptr.child)) {
15 | return std.testing.expectEqualStrings(expected, actual);
16 | } else if (ptr.child == []u8 or ptr.child == []const u8) {
17 | return expectStrings(expected, actual);
18 | },
19 | .@"struct" => |structType| {
20 | inline for (structType.fields) |field| {
21 | try expectEqual(@field(expected, field.name), @field(actual, field.name));
22 | }
23 | return;
24 | },
25 | .@"union" => |union_info| {
26 | if (union_info.tag_type == null) {
27 | @compileError("Unable to compare untagged union values");
28 | }
29 | const Tag = std.meta.Tag(@TypeOf(expected));
30 |
31 | const expectedTag = @as(Tag, expected);
32 | const actualTag = @as(Tag, actual);
33 | try expectEqual(expectedTag, actualTag);
34 |
35 | inline for (std.meta.fields(@TypeOf(actual))) |fld| {
36 | if (std.mem.eql(u8, fld.name, @tagName(actualTag))) {
37 | try expectEqual(@field(expected, fld.name), @field(actual, fld.name));
38 | return;
39 | }
40 | }
41 | unreachable;
42 | },
43 | else => {},
44 | }
45 | return std.testing.expectEqual(@as(@TypeOf(actual), expected), actual);
46 | }
47 |
48 | fn expectStrings(expected: []const []const u8, actual: anytype) !void {
49 | try t.expectEqual(expected.len, actual.len);
50 | for (expected, actual) |e, a| {
51 | try std.testing.expectEqualStrings(e, a);
52 | }
53 | }
54 |
55 | fn isStringArray(comptime T: type) bool {
56 | if (!is(.array)(T) and !isPtrTo(.array)(T)) {
57 | return false;
58 | }
59 | return std.meta.Elem(T) == u8;
60 | }
61 |
62 | pub const TraitFn = fn (type) bool;
63 | pub fn is(comptime id: std.builtin.TypeId) TraitFn {
64 | const Closure = struct {
65 | pub fn trait(comptime T: type) bool {
66 | return id == @typeInfo(T);
67 | }
68 | };
69 | return Closure.trait;
70 | }
71 |
72 | pub fn isPtrTo(comptime id: std.builtin.TypeId) TraitFn {
73 | const Closure = struct {
74 | pub fn trait(comptime T: type) bool {
75 | if (!comptime isSingleItemPtr(T)) return false;
76 | return id == @typeInfo(std.meta.Child(T));
77 | }
78 | };
79 | return Closure.trait;
80 | }
81 |
82 | pub fn isSingleItemPtr(comptime T: type) bool {
83 | if (comptime is(.pointer)(T)) {
84 | return @typeInfo(T).pointer.size == .one;
85 | }
86 | return false;
87 | }
88 |
89 | pub fn expectDelta(expected: anytype, actual: anytype, delta: anytype) !void {
90 | var diff = expected - actual;
91 | if (diff < 0) {
92 | diff = -diff;
93 | }
94 | if (diff <= delta) {
95 | return;
96 | }
97 |
98 | print("Expected {} to be within {} of {}. Actual diff: {}", .{ expected, delta, actual, diff });
99 | return error.NotWithinDelta;
100 | }
101 |
102 | pub fn print(comptime fmt: []const u8, args: anytype) void {
103 | if (@inComptime()) {
104 | @compileError(std.fmt.comptimePrint(fmt, args));
105 | } else {
106 | std.debug.print(fmt, args);
107 | }
108 | }
109 |
110 | // Re-expose these as-is so that more cases can rely on zul.testing exclusively.
111 | // Else, it's a pain to have both std.testing and zul.testing in a test.
112 | pub const expect = std.testing.expect;
113 | pub const expectFmt = std.testing.expectFmt;
114 | pub const expectError = std.testing.expectError;
115 | pub const expectEqualSlices = std.testing.expectEqualSlices;
116 | pub const expectEqualStrings = std.testing.expectEqualStrings;
117 | pub const expectEqualSentinel = std.testing.expectEqualSentinel;
118 | pub const expectApproxEqAbs = std.testing.expectApproxEqAbs;
119 | pub const expectApproxEqRel = std.testing.expectApproxEqRel;
120 |
121 | pub const allocator = std.testing.allocator;
122 | pub var arena = std.heap.ArenaAllocator.init(allocator);
123 |
124 | pub fn reset() void {
125 | _ = arena.reset(.free_all);
126 | }
127 |
128 | pub const Random = struct {
129 | var instance: ?std.Random.DefaultPrng = null;
130 |
131 | pub fn bytes(min: usize, max: usize) []u8 {
132 | var r = random();
133 | const l = r.intRangeAtMost(usize, min, max);
134 | const buf = arena.allocator().alloc(u8, l) catch unreachable;
135 | r.bytes(buf);
136 | return buf;
137 | }
138 |
139 | pub fn fill(buf: []u8) void {
140 | var r = random();
141 | r.bytes(buf);
142 | }
143 |
144 | pub fn fillAtLeast(buf: []u8, min: usize) []u8 {
145 | var r = random();
146 | const l = r.intRangeAtMost(usize, min, buf.len);
147 | r.bytes(buf[0..l]);
148 | return buf;
149 | }
150 |
151 | pub fn intRange(comptime T: type, min: T, max: T) T {
152 | var r = random();
153 | return r.intRangeAtMost(T, min, max);
154 | }
155 |
156 | pub fn random() std.Random {
157 | if (instance == null) {
158 | var seed: u64 = undefined;
159 | std.posix.getrandom(std.mem.asBytes(&seed)) catch unreachable;
160 | instance = std.Random.DefaultPrng.init(seed);
161 | }
162 | return instance.?.random();
163 | }
164 | };
165 |
166 | const t = @This();
167 | test "testing.rand: bytes" {
168 | defer t.reset();
169 | for (0..10) |_| {
170 | const bytes = Random.bytes(4, 8);
171 | try t.expectEqual(true, bytes.len >= 4 and bytes.len <= 8);
172 | }
173 | }
174 |
175 | test "testing.rand: fillAtLeast" {
176 | var buf: [10]u8 = undefined;
177 |
178 | for (0..10) |_| {
179 | const bytes = Random.fillAtLeast(&buf, 7);
180 | try t.expectEqual(true, bytes.len >= 7 and bytes.len <= 10);
181 | }
182 | }
183 |
184 | test "testing.rand: intRange" {
185 | for (0..10) |_| {
186 | const value = Random.intRange(u16, 3, 6);
187 | try t.expectEqual(true, value >= 3 and value <= 6);
188 | }
189 | }
190 |
191 | test "testing: doc example" {
192 | // clear's the arena allocator
193 | defer t.reset();
194 |
195 | // In addition to exposing std.testing.allocator as zul.testing.allocator
196 | // zul.testing.arena is an ArenaAllocator. An ArenaAllocator can
197 | // make managing test-specific allocations a lot simpler.
198 | // Just stick a `defer zul.testing.reset()` atop your test.
199 | var buf = try t.arena.allocator().alloc(u8, 5);
200 |
201 | // unlike std.testing.expectEqual, zul's expectEqual
202 | // will coerce expected to actual's type, so this is valid:
203 | try t.expectEqual(5, buf.len);
204 |
205 | @memcpy(buf[0..5], "hello");
206 |
207 | // zul's expectEqual also works with strings.
208 | try t.expectEqual("hello", buf);
209 | }
210 |
--------------------------------------------------------------------------------
/docs/src/testing.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: site.njk
3 | title: zul.testing
4 | ---
5 |
6 |
7 | Helpers for writing tests.
8 |
9 |
10 |
11 | {% highlight zig %}
12 | const t = zul.testing;
13 |
14 | test "memcpy" {
15 | // clear's the arena allocator
16 | defer t.reset();
17 |
18 | // In addition to exposing std.testing.allocator as zul.testing.allocator
19 | // zul.testing.arena is an ArenaAllocator. An ArenaAllocator can
20 | // make managing test-specific allocations a lot simpler.
21 | // Just stick a `defer zul.testing.reset()` atop your test.
22 | var buf = try t.arena.allocator().alloc(u8, 5);
23 |
24 | // unlike std.testing.expectEqual, zul's expectEqual
25 | // will coerce expected to actual's type, so this is valid:
26 | try t.expectEqual(5, buf.len);
27 |
28 | @memcpy(buf[0..5], "hello");
29 |
30 | // zul's expectEqual also works with strings.
31 | try t.expectEqual("hello", buf);
32 | }
33 | {% endhighlight %}
34 |
35 |
36 | zul.testing directly re-exports some functionality from std.testing.. This is to minimize the number of cases where both std.testing and zul.testing are needed. The following variables and functions are available under zul.testing:
37 |
38 | {% highlight zig %}
39 | pub const allocator = std.testing.allocator;
40 |
41 | pub const expect = std.testing.expect;
42 | pub const expectFmt = std.testing.expectFmt;
43 | pub const expectError = std.testing.expectError;
44 | pub const expectEqualSlices = std.testing.expectEqualSlices;
45 | pub const expectEqualStrings = std.testing.expectEqualStrings;
46 | pub const expectEqualSentinel = std.testing.expectEqualSentinel;
47 | pub const expectApproxEqAbs = std.testing.expectApproxEqAbs;
48 | pub const expectApproxEqRel = std.testing.expectApproxEqRel;
49 | {% endhighlight %}
50 |
51 |
52 |
53 |
54 |
arena: std.heap.ArenaAllocator
55 |
56 |
Complex tests often require their own allocation. This test-only data is particularly well suited for an ArenaAllocator.
57 |
58 |
Take care when using the arena. While it can streamline tests, it's easy to abuse. Code under test should use zul.testing.allocator (which is std.testing.allocator) so that leaks can be properly detected. Consider this slightly modified example taken from a real readLines test:
59 |
60 | {% highlight zig %}
61 | const t = zul.testing;
62 | test "readLines" {
63 | // clears the arena
64 | defer t.reset();
65 |
66 | const aa = t.arena.allocator();
67 | const path = try std.fs.cwd().realpathAlloc(aa, "tests/sample");
68 |
69 | var out: [30]u8 = undefined;
70 | var it = try readLines(path, &out, .{});
71 | defer it.deinit();
72 |
73 | try t.expectEqual("Consider Phlebas", it.next().?);
74 | try t.expectEqual("Old Man's War", it.next().?);
75 | try t.expectEqual(null, it.next());
76 | }
77 | {% endhighlight %}
78 |
79 |
path is clearly data that belongs to the test and its lifecycle isn't something that our function under test, readLines, should be concerned with. If however, readLInes took some type of ownership over path, then zul.testing.allocator should have been used.
80 |
81 |
82 |
83 |
84 |
85 |
86 |
92 |
98 |
99 |
reset()
100 |
101 |
Resets the zul.testing.arena. Typically called in a defer atop the test when the zul.testing.arena is used.
102 |
103 |
104 |
105 |
106 |
107 | {% highlight zig %}
108 | const t = zul.testing;
109 | test "random example" {
110 | // Some random functions use the zul.testing.arena allocator
111 | // so we need to free that
112 | defer t.reset();
113 |
114 | // create a random integer
115 | const min = t.Random.intRange(u32, 0, 10);
116 | const max = t.Random.intRange(u32, min, min + 10);
117 |
118 | // create a random []u8 between min and max length (inclusive)
119 | // created using zul.testing.arena
120 | var d1 = t.Random.bytes(min, max);
121 |
122 | // fill buf with random bytes, returns a slice which
123 | // is between min and buf.len in length (inclusive)
124 | var buf: [10]u8 = undefined;
125 | var d2 = t.Random.fillAtLeast(&buf, min);
126 | {% endhighlight %}
127 |
128 | Helpers to generate random data.
129 |
130 |
131 |
bytes(min: usize, max: usize) []u8
132 |
133 |
Populates a []u8 with random bytes. The created []u8 will be between min and max bytes in length (inclusive). It is created using the zul.testing.arena so reset should be called.
134 |
135 |
136 |
137 |
fill(buf: []u8) void
138 |
139 |
Fill buf with random bytes. Because this only fills buf, overwriting any previous data, and doesn't allocate, in a tight loop, it can be much faster than bytes .
140 |
141 |
142 |
143 |
fillAtLeast(buf: []u8, min: usize) []u8
144 |
145 |
Fill buf with random bytes. Returns a slice that is between min and buf.len bytes in length (inclusive). Because this only fills buf, overwriting any previous data, and doesn't allocate, in a tight loop, it can be much faster than bytes .
146 |
147 |
148 |
154 |
155 |
random() std.Random
156 |
157 |
Returns a randomly seeded std.Random instance.
158 |
159 |
160 |
161 |
162 |
--------------------------------------------------------------------------------
/docs/src/datetime.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: site.njk
3 | title: zul.DateTime
4 | ---
5 |
6 |
7 | Simple (no leap seconds, UTC-only), DateTime, Date and Time types.
8 |
9 |
10 | {% highlight zig %}
11 | // Currently only supports RFC3339
12 | const dt = try zul.DateTime.parse("2028-11-05T23:29:10Z", .rfc3339);
13 | const next_week = try dt.add(7, .days);
14 | std.debug.assert(next_week.order(dt) == .gt);
15 |
16 | // 1857079750000 == 2028-11-05T23:29:10Z
17 | std.debug.print("{d} == {s}", .{dt.unix(.milliseconds), dt});
18 | {% endhighlight %}
19 |
20 |
21 | zul.DateTime is a very basic DateTime struct designed for the main purpose of allowing RFC3339 dates to be read and written. It is simply an i64 representing the number of microseconds since unix epoch (Jan 1 1970). It supports valus ranging from Jan 1, -4712 to Dec 31, 9999. It does not support leap seconds and only supports UTC timezones.
22 |
23 | When serialized and parsed with JSON, or formatted using std.fmt, the RFC3339 representation is used.
24 |
25 |
26 |
27 |
28 |
micros: i64
29 |
30 |
Microseconds since unix epoch. A negative value indicates a DateTime before Jan 1, 1970.
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
initUTC(...) !DateTime
39 | {% highlight zig %}
40 | fn initUTC(
41 | year: i16,
42 | month: u8,
43 | day: u8,
44 | hour: u8,
45 | min: u8,
46 | sec: u8,
47 | micros: u32
48 | ) !DateTime
49 | {% endhighlight %}
50 |
51 |
52 |
Creates a DateTime. This will fail if the date is outside of the supported range (i.e. year < -4712 or year 9999), or if the date is invalid (e.g. Nov 31).
53 |
54 |
55 |
63 |
64 |
now() DateTime
65 |
66 |
Creates a DateTime for the current date and time.
67 |
68 |
69 |
75 |
81 |
87 |
93 |
99 |
105 |
106 |
107 |
108 | zul.Date represents a year, month and day. It is serialized and parsed to JSON or using std.fmt using the YYYY-MM-DD format (i.e. RFC3339).
109 |
110 |
111 |
112 |
113 | year: i16
114 |
115 |
116 |
month: u8
117 |
1-based (i.e. Jan == 1)
118 |
119 |
120 | day: u8
121 |
122 |
123 |
124 |
125 |
126 |
132 |
138 |
144 |
150 |
151 |
152 |
153 | zul.Time represents a hour, minute, second and microsecond. It is serialized and parsed to JSON or using std.fmt using the HH:MM:SS[.000000] format (i.e. RFC3339).
154 |
155 |
156 |
157 |
158 | hour: u8
159 |
160 |
161 | min: u8
162 |
163 |
164 | sec: u8
165 |
166 |
167 | micros: u32
168 |
169 |
170 |
171 |
172 |
173 |
179 |
185 |
191 |
197 |
198 |
--------------------------------------------------------------------------------
/src/scheduler.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const Thread = std.Thread;
4 | const Allocator = std.mem.Allocator;
5 | const DateTime = @import("zul.zig").DateTime;
6 |
7 | fn Job(comptime T: type) type {
8 | return struct {
9 | at: i64,
10 | task: T,
11 | };
12 | }
13 |
14 | pub fn Scheduler(comptime T: type, comptime C: type) type {
15 | return struct {
16 | queue: Q,
17 | running: bool,
18 | mutex: Thread.Mutex,
19 | cond: Thread.Condition,
20 | thread: ?Thread,
21 |
22 | const Q = std.PriorityQueue(Job(T), void, compare);
23 | const include_scheduler = @typeInfo(@TypeOf(T.run)).@"fn".params.len == 4;
24 |
25 | fn compare(_: void, a: Job(T), b: Job(T)) std.math.Order {
26 | return std.math.order(a.at, b.at);
27 | }
28 |
29 | const Self = @This();
30 |
31 | pub fn init(allocator: Allocator) Self {
32 | return .{
33 | .cond = .{},
34 | .mutex = .{},
35 | .thread = null,
36 | .running = false,
37 | .queue = Q.init(allocator, {}),
38 | };
39 | }
40 |
41 | pub fn deinit(self: *Self) void {
42 | self.stop();
43 | self.queue.deinit();
44 | }
45 |
46 | pub fn start(self: *Self, ctx: C) !void {
47 | {
48 | self.mutex.lock();
49 | defer self.mutex.unlock();
50 | if (self.running == true) {
51 | return error.AlreadyRunning;
52 | }
53 | self.running = true;
54 | }
55 | self.thread = try Thread.spawn(.{}, Self.run, .{ self, ctx });
56 | }
57 |
58 | pub fn stop(self: *Self) void {
59 | {
60 | self.mutex.lock();
61 | defer self.mutex.unlock();
62 | if (self.running == false) {
63 | return;
64 | }
65 | self.running = false;
66 | }
67 |
68 | self.cond.signal();
69 | self.thread.?.join();
70 | }
71 |
72 | pub fn scheduleAt(self: *Self, task: T, date: DateTime) !void {
73 | return self.schedule(task, date.unix(.milliseconds));
74 | }
75 |
76 | pub fn scheduleIn(self: *Self, task: T, ms: i64) !void {
77 | return self.schedule(task, std.time.milliTimestamp() + ms);
78 | }
79 |
80 | pub fn schedule(self: *Self, task: T, at: i64) !void {
81 | const job: Job(T) = .{
82 | .at = at,
83 | .task = task,
84 | };
85 |
86 | var reschedule = false;
87 | {
88 | self.mutex.lock();
89 | defer self.mutex.unlock();
90 |
91 | if (self.queue.peek()) |*next| {
92 | if (at < next.at) {
93 | reschedule = true;
94 | }
95 | } else {
96 | reschedule = true;
97 | }
98 | try self.queue.add(job);
99 | }
100 |
101 | if (reschedule) {
102 | // Our new job is scheduled before our previous earlier job
103 | // (or we had no previous jobs)
104 | // We need to reset our schedule
105 | self.cond.signal();
106 | }
107 | }
108 |
109 | // this is running in a separate thread, started by start()
110 | fn run(self: *Self, ctx: C) void {
111 | self.mutex.lock();
112 |
113 | while (true) {
114 | const ms_until_next = self.processPending(ctx);
115 |
116 | // mutex is locked when returning for processPending
117 |
118 | if (self.running == false) {
119 | self.mutex.unlock();
120 | return;
121 | }
122 |
123 | if (ms_until_next) |timeout| {
124 | const ns = @as(u64, @intCast(timeout * std.time.ns_per_ms));
125 | self.cond.timedWait(&self.mutex, ns) catch |err| {
126 | std.debug.assert(err == error.Timeout);
127 | // on success or error, cond locks mutex, which is what we want
128 | };
129 | } else {
130 | self.cond.wait(&self.mutex);
131 | }
132 | // if we woke up, it's because a new job was added with a more recent
133 | // scheduled time. This new job MAY not be ready to run yet, and
134 | // it's even possible for our cond variable to wake up randomly (as per
135 | // the docs), but processPending is defensive and will check this for us.
136 | }
137 | }
138 |
139 | // we enter this function with mutex locked
140 | // and we exit this function with the mutex locked
141 | // importantly, we don't lock the mutex will process the task
142 | fn processPending(self: *Self, ctx: C) ?i64 {
143 | while (true) {
144 | const next = self.queue.peek() orelse {
145 | // yes, we must return this function with a locked mutex
146 | return null;
147 | };
148 | const seconds_until_next = next.at - std.time.milliTimestamp();
149 | if (seconds_until_next > 0) {
150 | // this job isn't ready, yes, the mutex should remain locked!
151 | return seconds_until_next;
152 | }
153 |
154 | // delete the peeked job from the queue, because we're going to process it
155 | const job = self.queue.remove();
156 | self.mutex.unlock();
157 | defer self.mutex.lock();
158 | if (comptime include_scheduler) {
159 | job.task.run(ctx, self, next.at);
160 | } else {
161 | job.task.run(ctx, next.at);
162 | }
163 | }
164 | }
165 | };
166 | }
167 |
168 | const t = @import("zul.zig").testing;
169 | test "Scheduler: null context" {
170 | var s = Scheduler(TestTask, void).init(t.allocator);
171 | defer s.deinit();
172 |
173 | try s.start({});
174 |
175 | try t.expectError(error.AlreadyRunning, s.start({}));
176 |
177 | // test that past jobs are run
178 | var counter: usize = 0;
179 | try s.scheduleIn(.{ .counter = &counter }, -200);
180 | try s.scheduleAt(.{ .counter = &counter }, try DateTime.now().add(-20, .milliseconds));
181 | try s.schedule(.{ .counter = &counter }, 4);
182 |
183 | var history = TestTask.History{
184 | .pos = 0,
185 | .records = undefined,
186 | };
187 |
188 | try s.scheduleIn(.{ .recorder = .{ .value = 1, .history = &history } }, 10);
189 | try s.scheduleAt(.{ .recorder = .{ .value = 2, .history = &history } }, try DateTime.now().add(4, .milliseconds));
190 | try s.schedule(.{ .recorder = .{ .value = 3, .history = &history } }, std.time.milliTimestamp() + 8);
191 |
192 | // never gets run
193 | try s.scheduleAt(.{ .recorder = .{ .value = 0, .history = &history } }, try DateTime.now().add(2, .seconds));
194 |
195 | std.Thread.sleep(std.time.ns_per_ms * 20);
196 | s.stop();
197 |
198 | try t.expectEqual(3, counter);
199 | try t.expectEqual(3, history.pos);
200 | try t.expectEqual(2, history.records[0]);
201 | try t.expectEqual(3, history.records[1]);
202 | try t.expectEqual(1, history.records[2]);
203 | }
204 |
205 | test "Scheduler: with context" {
206 | var s = Scheduler(TestCtxTask, *usize).init(t.allocator);
207 | defer s.deinit();
208 |
209 | var ctx: usize = 3;
210 | try s.start(&ctx);
211 | // test that past jobs are run
212 | try s.scheduleIn(.{ .add = 2 }, 4);
213 | try s.scheduleIn(.{ .add = 4 }, 8);
214 |
215 | std.Thread.sleep(std.time.ns_per_ms * 20);
216 | s.stop();
217 |
218 | try t.expectEqual(9, ctx);
219 | }
220 |
221 | const TestTask = union(enum) {
222 | counter: *usize,
223 | recorder: Recorder,
224 |
225 | fn run(self: TestTask, _: void, _: i64) void {
226 | switch (self) {
227 | .counter => |c| c.* += 1,
228 | .recorder => |r| {
229 | const pos = r.history.pos;
230 | r.history.records[pos] = r.value;
231 | r.history.pos = pos + 1;
232 | },
233 | }
234 | }
235 |
236 | const Recorder = struct {
237 | value: usize,
238 | history: *History,
239 | };
240 |
241 | const History = struct {
242 | pos: usize,
243 | records: [3]usize,
244 | };
245 | };
246 |
247 | const TestCtxTask = union(enum) {
248 | add: usize,
249 |
250 | fn run(self: TestCtxTask, sum: *usize, _: i64) void {
251 | switch (self) {
252 | .add => |c| sum.* += c,
253 | }
254 | }
255 | };
256 |
--------------------------------------------------------------------------------
/tests/large:
--------------------------------------------------------------------------------
1 | aaaLorem ipsum dolor sit amet, consectetur adipiscing elit. Sed faucibus lorem non massa venenatis feugiat. Donec at diam nec ligula condimentum tristique ut nec sapien. Ut condimentum dui vitae pulvinar vehicula. Nullam tempus laoreet hendrerit. Praesent vulputate cursus hendrerit. Aliquam faucibus diam mauris, in malesuada elit pellentesque vel. Cras egestas laoreet nisi ac venenatis. Duis nec dictum tortor. Vivamus condimentum dui id euismod aliquet. Integer blandit eros non augue pulvinar, quis vulputate erat auctor. Maecenas leo purus, luctus ac fringilla non, semper eu metus. Etiam efficitur diam neque, quis lobortis eros sollicitudin ac. Quisque feugiat, justo vitae consectetur tincidunt, justo enim gravida elit, non dapibus nulla quam elementum nisi. Fusce venenatis tincidunt mi finibus viverra. Nunc sed magna rutrum, tempor lorem ut, rhoncus lectus. Morbi ac scelerisque elit.
2 |
3 | Nunc ultrices tortor et massa condimentum gravida. Proin sed turpis erat. Suspendisse vitae augue sapien. Donec vitae nunc ut urna posuere facilisis. Integer nec libero aliquet lorem vestibulum accumsan. Phasellus sed fringilla sapien. Quisque ut purus ut velit blandit convallis. Mauris sodales quis nunc ut aliquam.
4 |
5 | Morbi vulputate, ex quis molestie pharetra, ante justo iaculis augue, id ornare elit orci a eros. Aenean sit amet ipsum eu est pulvinar eleifend at sed ipsum. Phasellus non pretium velit, ut mattis diam. Donec in pellentesque odio. Phasellus tincidunt congue nibh ac rhoncus. Phasellus lacus felis, tempus sed viverra et, aliquet id massa. Pellentesque nec aliquam ligula, vel hendrerit magna. Aenean sed convallis velit. Vivamus eu viverra massa, in euismod mi. Suspendisse ac dolor nisi. Aenean at suscipit mi. In rhoncus, metus non pharetra tempus, nulla magna ullamcorper ligula, sed eleifend erat ipsum id erat. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla aliquet ex a nisl iaculis posuere. Vivamus eget orci id augue pellentesque pharetra.
6 |
7 | Vivamus in felis sed sem venenatis condimentum. Sed porttitor libero et feugiat imperdiet. Aenean a dui hendrerit est scelerisque gravida. Sed in nibh eget purus hendrerit pellentesque. Sed in dui a lectus ultricies consectetur. Cras ex eros, consectetur nec eros id, egestas ullamcorper est. Cras ut orci lorem.
8 |
9 | Praesent volutpat interdum diam. Pellentesque orci lacus, commodo vel interdum et, blandit nec nisl. Sed aliquet maximus turpis, eu imperdiet felis. Nam blandit diam vel augue pharetra, nec commodo felis fringilla. Curabitur dui diam, auctor at ante ac, sollicitudin facilisis lorem. Vivamus tincidunt tellus ut nibh viverra tempor. Vivamus at justo molestie, condimentum tortor tempus, porttitor elit.
10 |
11 | Cras laoreet efficitur mi, vel lacinia velit vestibulum a. Phasellus commodo porta purus a mattis. Aliquam eu mattis lectus. Sed urna libero, pellentesque quis ligula sed, consequat pellentesque sapien. Suspendisse et finibus eros, sit amet hendrerit magna. Quisque in velit sit amet lectus fringilla vestibulum. Cras a nisi vitae odio pulvinar vestibulum.
12 |
13 | Sed cursus nulla sollicitudin libero faucibus, ut ultrices augue posuere. Aliquam fringilla lacus sit amet diam posuere finibus. Cras semper ultricies ligula, a hendrerit ipsum vestibulum finibus. Proin neque ligula, eleifend rhoncus consequat sit amet, sodales ut ex. Duis dapibus arcu a mi pretium hendrerit. Donec sit amet consequat enim. Maecenas et pharetra nulla. Fusce cursus orci ac egestas tincidunt. Etiam semper enim ipsum, vel pretium purus fringilla eget. Duis ligula arcu, aliquam in neque sed, euismod feugiat nunc.
14 |
15 | Aenean at erat dictum, venenatis velit et, mattis libero. Ut volutpat, metus at rhoncus tempor, sem magna efficitur quam, non venenatis ligula nibh a lacus. Pellentesque finibus ultrices nibh a posuere. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque maximus est nulla, sit amet consectetur quam finibus ut. Fusce in turpis non odio viverra gravida. Vivamus posuere vulputate mattis. Praesent quis felis vitae nunc aliquam pharetra sed nec lectus. Donec sit amet tincidunt augue. Aliquam vitae nulla sapien. Vivamus fringilla sem ut felis maximus pellentesque. Quisque et varius tortor.
16 |
17 | Integer et metus dignissim, molestie velit vitae, sodales purus. Proin eget lorem aliquam, consequat sapien at, elementum augue. Cras ac justo placerat, mollis erat in, maximus nibh. Aliquam erat volutpat. In laoreet gravida sapien, in condimentum risus cursus eu. Vivamus ullamcorper lectus condimentum nisi ultricies, porta rhoncus nisl tempus. Pellentesque sodales tellus sit amet est convallis pulvinar. Cras mauris orci, tristique eget tincidunt eu, venenatis ac augue. Cras dapibus tristique consectetur. Aenean imperdiet maximus risus, et pretium tortor ultrices eu. Maecenas posuere ac leo quis commodo.
18 |
19 | Aenean non vestibulum ex, sed sodales risus. Pellentesque lacinia diam at sagittis rutrum. Nullam fringilla dolor sit amet metus fringilla pretium. Integer ultrices felis id quam tristique, a posuere lacus sodales. Pellentesque efficitur tempus placerat. Suspendisse maximus faucibus leo, eu interdum urna euismod vitae. Vestibulum ultrices porttitor leo, eget molestie risus. Nulla interdum turpis metus, sed fringilla neque finibus et. Nullam eu malesuada nulla. Etiam nisi diam, fermentum sit amet feugiat ut, sollicitudin at tortor. Aenean blandit iaculis mattis. Morbi ut tempor purus, a vulputate orci. Integer bibendum magna sit amet porta lacinia.
20 |
21 | Etiam dictum imperdiet massa, vitae facilisis est dignissim pellentesque. Nam gravida tortor ligula, ut dictum justo posuere ac. Aliquam maximus magna ac ipsum ullamcorper, non faucibus felis rhoncus. Aenean commodo tincidunt metus, id efficitur orci fermentum ac. Sed congue turpis lorem. Nam in magna malesuada, porttitor lectus ac, tempus elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
22 |
23 | Vivamus vitae molestie dui. Nullam ut mi nec est scelerisque pretium. Praesent ac risus dictum, ornare tortor quis, mollis ex. Nullam sed dictum diam. Phasellus eu sapien accumsan, volutpat lectus eget, congue nunc. Pellentesque vulputate tempus magna, eu ultrices lacus congue vitae. In hac habitasse platea dictumst. Aenean elit purus, blandit vitae tincidunt et, sodales blandit purus. Nunc eu felis vel ante facilisis lacinia in sed tellus. Nam varius scelerisque erat et venenatis.
24 |
25 | Nullam aliquam ex orci, eget imperdiet elit consequat vel. Praesent sit amet turpis id mi ornare auctor at vel nisl. Pellentesque scelerisque blandit tempor. Suspendisse lobortis euismod orci, non mollis turpis. Vestibulum a nulla lacus. Ut non metus augue. Duis accumsan sed augue eu auctor. Praesent congue nulla interdum pretium vestibulum. Vivamus quis cursus turpis, at rhoncus est. Suspendisse ac massa quis ligula iaculis cursus vitae ut tortor. Quisque ut diam a mi gravida cursus vitae id est. Suspendisse eleifend elit ac nisi viverra facilisis. Maecenas eu nisl euismod, imperdiet massa quis, porta dolor. Etiam pharetra nulla diam, ac iaculis ex sollicitudin nec.
26 |
27 | Nulla dictum tortor risus, quis fringilla mauris lacinia sed. Vestibulum risus libero, ultrices in nisi nec, mollis facilisis purus. Nam sagittis nisl nisl, ac porttitor velit volutpat et. Cras suscipit maximus finibus. Morbi ut placerat eros, nec condimentum nibh. Nam sed augue magna. Praesent rutrum lobortis lectus, eget ullamcorper massa blandit nec. Phasellus posuere magna et nisi lacinia, sit amet ornare dolor laoreet. Vivamus vel massa vel leo faucibus blandit. Cras nibh enim, bibendum eu elit sed, scelerisque rutrum ligula. Nulla laoreet, est in lacinia semper, ipsum erat tincidunt metus, a venenatis nisl dui in urna. Aliquam in faucibus nibh.
28 |
29 | Nam molestie justo nulla, vel mollis augue tincidunt quis. Maecenas nec eros aliquet, imperdiet purus ac, tempor sapien. Proin vel aliquam purus, eu dapibus magna. Donec pretium lorem ut justo tincidunt, id ultrices elit sodales. Integer fermentum erat in magna porta, id luctus leo fermentum. Etiam at nibh at purus aliquam tincidunt in ut libero. Donec vel tempor enim. Mauris vel varius neque, ac sodales velit. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nam sed odio egestas, tempor odio non, bibendum dolor.
30 |
31 | Nulla non congue dui. In a tempus dui. In rhoncus convallis aliquet. Nulla vitae finibus purus, in dapibus tortor. Cras dignissim ultricies diam eu congue. Morbi fermentum dolor erat, a consectetur leo tincidunt vel. Nulla consequat condimentum erat eu egestas. Ut lacinia tellus sit amet turpis consequat tempor. Curabitur blandit euismod convallis. Morbi sollicitudin ornare odio sit amet feugiat. Donec posuere lorem nec neque consequat tincidunt. Etiam convallis, risus eu finibus eleifend, lacus justo rhoncus tortor, hendrerit congue nisl neque in ante.
32 |
33 | Suspendisse potenti. Ut sit amet fringilla velit, vitae convallis ex. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Phasellus aliquet eget ipsum venenatis euismod. Sed quis ex massa. Ut sit amet vestibulum augue. Integer sagittis ligula ac est vulputate, non molestie justo malesuada.
34 |
35 | Etiam vitae dolor accumsan, finibus felis sed, consectetur turpis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla vestibulum massa ligula, in gravida tortor bibendum vitae. Proin a erat sed velit porttitor pretium. Nunc sit amet ultricies nibh, a mattis neque. Aenean quis finibus mauris, nec fermentum nulla. In viverra, mi nec tincidunt aliquet, mauris leo malesuada dolor, in consequat dui eros et risus. Integer sit amet nisl eget nulla commodo posuere id quis est. Aenean consectetur, urna ac elementum elementum, nisi turpis tempor lorem, in fringilla risus ipsum ac mi. In fermentum suscipit lobortis. Donec venenatis urna vitae placerat volutpat. Nulla dolor nunc, lacinia mollis purus eget, auctor posuere nisl.
36 |
37 | Quisque pulvinar urna vitae faucibus blandit. Nulla facilisi. Curabitur tincidunt nisl massa, ut ornare turpis ullamcorper sit.zzz
38 |
--------------------------------------------------------------------------------
/src/fs.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const zul = @import("zul.zig");
3 |
4 | const Allocator = std.mem.Allocator;
5 |
6 | pub fn readJson(comptime T: type, allocator: Allocator, file_path: []const u8, opts: std.json.ParseOptions) !zul.Managed(T) {
7 | const file = blk: {
8 | if (std.fs.path.isAbsolute(file_path)) {
9 | break :blk try std.fs.openFileAbsolute(file_path, .{});
10 | } else {
11 | break :blk try std.fs.cwd().openFile(file_path, .{});
12 | }
13 | };
14 | defer file.close();
15 |
16 | var buffer: [1024]u8 = undefined;
17 | var file_reader = file.reader(&buffer);
18 | var json_reader = std.json.Reader.init(allocator, &file_reader.interface);
19 | defer json_reader.deinit();
20 |
21 | var o = opts;
22 | o.allocate = .alloc_always;
23 | const parsed = try std.json.parseFromTokenSource(T, allocator, &json_reader, o);
24 | return zul.Managed(T).fromJson(parsed);
25 | }
26 |
27 | pub fn readDir(dir_path: []const u8) !Iterator {
28 | const dir = blk: {
29 | if (std.fs.path.isAbsolute(dir_path)) {
30 | break :blk try std.fs.openDirAbsolute(dir_path, .{ .iterate = true });
31 | } else {
32 | break :blk try std.fs.cwd().openDir(dir_path, .{ .iterate = true });
33 | }
34 | };
35 |
36 | return .{
37 | .dir = dir,
38 | .it = dir.iterate(),
39 | };
40 | }
41 |
42 | pub const Iterator = struct {
43 | dir: Dir,
44 | it: Dir.Iterator,
45 | arena: ?*std.heap.ArenaAllocator = null,
46 |
47 | const Dir = std.fs.Dir;
48 | const Entry = Dir.Entry;
49 |
50 | pub const Sort = enum {
51 | none,
52 | alphabetic,
53 | dir_first,
54 | dir_last,
55 | };
56 |
57 | pub fn deinit(self: *Iterator) void {
58 | self.dir.close();
59 | if (self.arena) |arena| {
60 | const allocator = arena.child_allocator;
61 | arena.deinit();
62 | allocator.destroy(arena);
63 | }
64 | }
65 |
66 | pub fn reset(self: *Iterator) void {
67 | self.it.reset();
68 | }
69 |
70 | pub fn next(self: *Iterator) !?std.fs.Dir.Entry {
71 | return self.it.next();
72 | }
73 |
74 | pub fn all(self: *Iterator, allocator: Allocator, sort: Sort) ![]std.fs.Dir.Entry {
75 | var arena = try allocator.create(std.heap.ArenaAllocator);
76 | errdefer allocator.destroy(arena);
77 |
78 | arena.* = std.heap.ArenaAllocator.init(allocator);
79 | errdefer arena.deinit();
80 |
81 | const aa = arena.allocator();
82 |
83 | var arr: std.ArrayList(Entry) = .empty;
84 |
85 | var it = self.it;
86 | while (try it.next()) |entry| {
87 | try arr.append(aa, .{
88 | .kind = entry.kind,
89 | .name = try aa.dupe(u8, entry.name),
90 | });
91 | }
92 |
93 | self.arena = arena;
94 | const items = arr.items;
95 |
96 | switch (sort) {
97 | .alphabetic => std.sort.pdq(Entry, items, {}, sortEntriesAlphabetic),
98 | .dir_first => std.sort.pdq(Entry, items, {}, sortEntriesDirFirst),
99 | .dir_last => std.sort.pdq(Entry, items, {}, sortEntriesDirLast),
100 | .none => {},
101 | }
102 | return items;
103 | }
104 |
105 | fn sortEntriesAlphabetic(ctx: void, a: Entry, b: Entry) bool {
106 | _ = ctx;
107 | return std.ascii.lessThanIgnoreCase(a.name, b.name);
108 | }
109 | fn sortEntriesDirFirst(ctx: void, a: Entry, b: Entry) bool {
110 | _ = ctx;
111 | if (a.kind == b.kind) {
112 | return std.ascii.lessThanIgnoreCase(a.name, b.name);
113 | }
114 | return a.kind == .directory;
115 | }
116 | fn sortEntriesDirLast(ctx: void, a: Entry, b: Entry) bool {
117 | _ = ctx;
118 | if (a.kind == b.kind) {
119 | return std.ascii.lessThanIgnoreCase(a.name, b.name);
120 | }
121 | return a.kind != .directory;
122 | }
123 | };
124 |
125 | const t = zul.testing;
126 | test "fs.readJson: file not found" {
127 | try t.expectError(error.FileNotFound, readJson(TestStruct, t.allocator, "tests/does_not_exist", .{}));
128 | try t.expectError(error.FileNotFound, readJson(TestStruct, t.allocator, "/tmp/zul/tests/does_not_exist", .{}));
129 | }
130 |
131 | test "fs.readJson: invalid json" {
132 | try t.expectError(error.SyntaxError, readJson(TestStruct, t.allocator, "tests/fs/lines", .{}));
133 | }
134 |
135 | test "fs.readJson: success" {
136 | defer t.reset();
137 | for (testAbsoluteAndRelative("tests/fs/test_struct.json")) |file_path| {
138 | const s = try readJson(TestStruct, t.allocator, file_path, .{});
139 | defer s.deinit();
140 | try t.expectEqual(9001, s.value.id);
141 | try t.expectEqual("Goku", s.value.name);
142 | try t.expectEqual("c", s.value.tags[2]);
143 | }
144 | }
145 |
146 | test "fs.readDir: dir not found" {
147 | try t.expectError(error.FileNotFound, readDir("tests/fs/not_found"));
148 | try t.expectError(error.FileNotFound, readDir("/tmp/zul/tests/fs/not_found"));
149 | }
150 |
151 | test "fs.readDir: iterate" {
152 | defer t.reset();
153 |
154 | for (testAbsoluteAndRelative("tests/fs")) |dir_path| {
155 | var it = try readDir(dir_path);
156 | defer it.deinit();
157 |
158 | //loop twice, it.reset() should allow a re-iteration
159 | for (0..2) |_| {
160 | it.reset();
161 | var expected = testFsEntires();
162 |
163 | while (try it.next()) |entry| {
164 | const found = expected.fetchRemove(entry.name) orelse {
165 | std.debug.print("fs.iterate unknown entry: {s}", .{entry.name});
166 | return error.UnknownEntry;
167 | };
168 | try t.expectEqual(found.value, entry.kind);
169 | }
170 | try t.expectEqual(0, expected.count());
171 | }
172 | }
173 | }
174 |
175 | test "fs.readDir: all unsorted" {
176 | defer t.reset();
177 | for (testAbsoluteAndRelative("tests/fs")) |dir_path| {
178 | var expected = testFsEntires();
179 |
180 | var it = try readDir(dir_path);
181 | defer it.deinit();
182 | const entries = try it.all(t.allocator, .none);
183 | for (entries) |entry| {
184 | const found = expected.fetchRemove(entry.name) orelse {
185 | std.debug.print("fs.iterate unknown entry: {s}", .{entry.name});
186 | return error.UnknownEntry;
187 | };
188 | try t.expectEqual(found.value, entry.kind);
189 | }
190 | try t.expectEqual(0, expected.count());
191 | }
192 | }
193 |
194 | test "fs.readDir: sorted alphabetic" {
195 | defer t.reset();
196 | for (testAbsoluteAndRelative("tests/fs")) |dir_path| {
197 | var it = try readDir(dir_path);
198 | defer it.deinit();
199 |
200 | const entries = try it.all(t.allocator, .alphabetic);
201 | try t.expectEqual(6, entries.len);
202 | try t.expectEqual("lines", entries[0].name);
203 | try t.expectEqual("long_line", entries[1].name);
204 | try t.expectEqual("single_char", entries[2].name);
205 | try t.expectEqual("sub-1", entries[3].name);
206 | try t.expectEqual("sub-2", entries[4].name);
207 | try t.expectEqual("test_struct.json", entries[5].name);
208 | }
209 | }
210 |
211 | test "fs.readDir: sorted dir first" {
212 | defer t.reset();
213 | for (testAbsoluteAndRelative("tests/fs")) |dir_path| {
214 | var it = try readDir(dir_path);
215 | defer it.deinit();
216 |
217 | const entries = try it.all(t.allocator, .dir_first);
218 | try t.expectEqual(6, entries.len);
219 | try t.expectEqual("sub-1", entries[0].name);
220 | try t.expectEqual("sub-2", entries[1].name);
221 | try t.expectEqual("lines", entries[2].name);
222 | try t.expectEqual("long_line", entries[3].name);
223 | try t.expectEqual("single_char", entries[4].name);
224 | try t.expectEqual("test_struct.json", entries[5].name);
225 | }
226 | }
227 |
228 | test "fs.readDir: sorted dir last" {
229 | defer t.reset();
230 | for (testAbsoluteAndRelative("tests/fs")) |dir_path| {
231 | var it = try readDir(dir_path);
232 | defer it.deinit();
233 |
234 | const entries = try it.all(t.allocator, .dir_last);
235 | try t.expectEqual(6, entries.len);
236 | try t.expectEqual("lines", entries[0].name);
237 | try t.expectEqual("long_line", entries[1].name);
238 | try t.expectEqual("single_char", entries[2].name);
239 | try t.expectEqual("test_struct.json", entries[3].name);
240 | try t.expectEqual("sub-1", entries[4].name);
241 | try t.expectEqual("sub-2", entries[5].name);
242 | }
243 | }
244 |
245 | const TestStruct = struct {
246 | id: i32,
247 | name: []const u8,
248 | tags: [][]const u8,
249 | };
250 |
251 | fn testAbsoluteAndRelative(relative: []const u8) [2][]const u8 {
252 | const allocator = t.arena.allocator();
253 | return [2][]const u8{
254 | allocator.dupe(u8, relative) catch unreachable,
255 | std.fs.cwd().realpathAlloc(allocator, relative) catch unreachable,
256 | };
257 | }
258 |
259 | fn testFsEntires() std.StringHashMap(std.fs.File.Kind) {
260 | var map = std.StringHashMap(std.fs.File.Kind).init(t.arena.allocator());
261 | map.put("sub-1", .directory) catch unreachable;
262 | map.put("sub-2", .directory) catch unreachable;
263 | map.put("single_char", .file) catch unreachable;
264 | map.put("lines", .file) catch unreachable;
265 | map.put("long_line", .file) catch unreachable;
266 | map.put("test_struct.json", .file) catch unreachable;
267 | return map;
268 | }
269 |
--------------------------------------------------------------------------------
/src/command_line_args.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const Allocator = std.mem.Allocator;
4 | const ArenaAllocator = std.heap.ArenaAllocator;
5 |
6 | pub const CommandLineArgs = struct {
7 | _arena: *ArenaAllocator,
8 | _lookup: std.StringHashMapUnmanaged([]const u8),
9 |
10 | exe: []const u8,
11 | tail: [][]const u8,
12 | list: [][]const u8,
13 |
14 | pub fn parse(parent: Allocator) !CommandLineArgs {
15 | const arena = try parent.create(ArenaAllocator);
16 | errdefer parent.destroy(arena);
17 |
18 | arena.* = ArenaAllocator.init(parent);
19 | errdefer arena.deinit();
20 |
21 | var it = try std.process.argsWithAllocator(arena.allocator());
22 | return parseFromIterator(arena, &it);
23 | }
24 |
25 | // Done this way, with our anytype iterator so that we can write unit tests
26 | fn parseFromIterator(arena: *ArenaAllocator, it: anytype) !CommandLineArgs {
27 | const allocator = arena.allocator();
28 | var list: std.ArrayList([]const u8) = .empty;
29 | var lookup: std.StringHashMapUnmanaged([]const u8) = .{};
30 |
31 | const exe = blk: {
32 | const first = it.next() orelse return .{
33 | .exe = "",
34 | .tail = &[_][]const u8{},
35 | .list = &[_][]const u8{},
36 | ._arena = arena,
37 | ._lookup = lookup,
38 | };
39 | const exe = try allocator.dupe(u8, first);
40 | try list.append(allocator, exe);
41 | break :blk exe;
42 | };
43 |
44 | // first thing we do is collect them all into our list. This will let us
45 | // move forwards and backwards when we do our simple parsing
46 | while (it.next()) |arg| {
47 | try list.append(allocator, try allocator.dupe(u8, arg));
48 | }
49 |
50 | // 1, skip the exe
51 | var i: usize = 1;
52 | var tail_start: usize = 1;
53 |
54 | const items = list.items;
55 | while (i < items.len) {
56 | const arg = items[i];
57 | if (arg.len == 1 or arg[0] != '-') {
58 | // can't be a valid parameter, so it must be the start of our tail
59 | break;
60 | }
61 |
62 | if (arg[1] == '-') {
63 | const kv = KeyValue.from(arg[2..], items, &i);
64 | try lookup.put(allocator, kv.key, kv.value);
65 | } else {
66 | const kv = KeyValue.from(arg[1..], items, &i);
67 | const key = kv.key;
68 |
69 | // -xvf file.tar.gz
70 | // parses into x=>"", v=>"", f=>"file.tar.gz"
71 | for (0..key.len - 1) |j| {
72 | try lookup.put(allocator, key[j .. j + 1], "");
73 | }
74 | try lookup.put(allocator, key[key.len - 1 ..], kv.value);
75 | }
76 | tail_start = i;
77 | }
78 |
79 | return .{
80 | .exe = exe,
81 | // safe to do since all the memory is managed by our arena
82 | .tail = list.items[tail_start..],
83 | .list = list.items,
84 | ._arena = arena,
85 | ._lookup = lookup,
86 | };
87 | }
88 |
89 | pub fn deinit(self: CommandLineArgs) void {
90 | const arena = self._arena;
91 | const allocator = arena.child_allocator;
92 | arena.deinit();
93 | allocator.destroy(arena);
94 | }
95 |
96 | pub fn contains(self: *const CommandLineArgs, name: []const u8) bool {
97 | return self._lookup.contains(name);
98 | }
99 |
100 | pub fn get(self: *const CommandLineArgs, name: []const u8) ?[]const u8 {
101 | return self._lookup.get(name);
102 | }
103 |
104 | pub fn count(self: *const CommandLineArgs) u32 {
105 | return self._lookup.count();
106 | }
107 | };
108 |
109 | const KeyValue = struct {
110 | key: []const u8,
111 | value: []const u8,
112 |
113 | fn from(key: []const u8, items: [][]const u8, i: *usize) KeyValue {
114 | const item_index = i.*;
115 | if (std.mem.indexOfScalarPos(u8, key, 0, '=')) |pos| {
116 | // this parameter is in the form of --key=value, or -k=value
117 | // we just skip the key
118 | i.* = item_index + 1;
119 |
120 | return .{
121 | .key = key[0..pos],
122 | .value = key[pos + 1 ..],
123 | };
124 | }
125 |
126 | if (item_index == items.len - 1 or items[item_index + 1][0] == '-') {
127 | // our key is at the end of the arguments OR
128 | // the next argument starts with a '-'. This means this key has no value
129 |
130 | // we just skip the key
131 | i.* = item_index + 1;
132 |
133 | return .{
134 | .key = key,
135 | .value = "",
136 | };
137 | }
138 |
139 | // skip the current key, and the next arg (which is our value)
140 | i.* = item_index + 2;
141 | return .{
142 | .key = key,
143 | .value = items[item_index + 1],
144 | };
145 | }
146 | };
147 |
148 | const t = @import("zul.zig").testing;
149 | test "CommandLineArgs: empty" {
150 | var args = testParse(&.{});
151 | defer args.deinit();
152 | try t.expectEqual("", args.exe);
153 | try t.expectEqual(0, args.count());
154 | try t.expectEqual(0, args.list.len);
155 | try t.expectEqual(0, args.tail.len);
156 | }
157 |
158 | test "CommandLineArgs: exe only" {
159 | const input = [_][]const u8{"/tmp/exe"};
160 | var args = testParse(&input);
161 | defer args.deinit();
162 | try t.expectEqual("/tmp/exe", args.exe);
163 | try t.expectEqual(0, args.count());
164 | try t.expectEqual(0, args.tail.len);
165 | try t.expectEqual(&input, args.list);
166 | }
167 |
168 | test "CommandLineArgs: simple args" {
169 | const input = [_][]const u8{ "a binary", "--level", "info", "--silent", "-p", "5432", "-x" };
170 | var args = testParse(&input);
171 | defer args.deinit();
172 |
173 | try t.expectEqual("a binary", args.exe);
174 | try t.expectEqual(0, args.tail.len);
175 | try t.expectEqual(&input, args.list);
176 |
177 | try t.expectEqual(4, args.count());
178 | try t.expectEqual(true, args.contains("level"));
179 | try t.expectEqual("info", args.get("level").?);
180 |
181 | try t.expectEqual(true, args.contains("silent"));
182 | try t.expectEqual("", args.get("silent").?);
183 |
184 | try t.expectEqual(true, args.contains("p"));
185 | try t.expectEqual("5432", args.get("p").?);
186 |
187 | try t.expectEqual(true, args.contains("x"));
188 | try t.expectEqual("", args.get("x").?);
189 | }
190 |
191 | test "CommandLineArgs: single character flags" {
192 | const input = [_][]const u8{ "9001", "-a", "-bc", "-def", "-ghij", "data" };
193 | var args = testParse(&input);
194 | defer args.deinit();
195 |
196 | try t.expectEqual("9001", args.exe);
197 | try t.expectEqual(0, args.tail.len);
198 | try t.expectEqual(&input, args.list);
199 |
200 | try t.expectEqual(10, args.count());
201 | try t.expectEqual(true, args.contains("a"));
202 | try t.expectEqual("", args.get("a").?);
203 | try t.expectEqual(true, args.contains("b"));
204 | try t.expectEqual("", args.get("b").?);
205 | try t.expectEqual(true, args.contains("c"));
206 | try t.expectEqual("", args.get("c").?);
207 | try t.expectEqual(true, args.contains("d"));
208 | try t.expectEqual("", args.get("d").?);
209 | try t.expectEqual(true, args.contains("e"));
210 | try t.expectEqual("", args.get("e").?);
211 | try t.expectEqual(true, args.contains("f"));
212 | try t.expectEqual("", args.get("f").?);
213 | try t.expectEqual(true, args.contains("g"));
214 | try t.expectEqual("", args.get("g").?);
215 | try t.expectEqual(true, args.contains("h"));
216 | try t.expectEqual("", args.get("h").?);
217 | try t.expectEqual(true, args.contains("i"));
218 | try t.expectEqual("", args.get("i").?);
219 |
220 | try t.expectEqual(true, args.contains("j"));
221 | try t.expectEqual("data", args.get("j").?);
222 | }
223 |
224 | test "CommandLineArgs: simple args with =" {
225 | const input = [_][]const u8{ "a binary", "--level=error", "-k", "-p=6669" };
226 | var args = testParse(&input);
227 | defer args.deinit();
228 |
229 | try t.expectEqual("a binary", args.exe);
230 | try t.expectEqual(0, args.tail.len);
231 | try t.expectEqual(&input, args.list);
232 |
233 | try t.expectEqual(3, args.count());
234 | try t.expectEqual(true, args.contains("level"));
235 | try t.expectEqual("error", args.get("level").?);
236 |
237 | try t.expectEqual(true, args.contains("k"));
238 | try t.expectEqual("", args.get("k").?);
239 |
240 | try t.expectEqual(true, args.contains("p"));
241 | try t.expectEqual("6669", args.get("p").?);
242 | }
243 |
244 | test "CommandLineArgs: tail" {
245 | const input = [_][]const u8{ "a binary", "-l", "--k", "x", "ts", "-p=6669", "hello" };
246 | var args = testParse(&input);
247 | defer args.deinit();
248 |
249 | try t.expectEqual("a binary", args.exe);
250 | try t.expectEqual(&.{ "ts", "-p=6669", "hello" }, args.tail);
251 | try t.expectEqual(&input, args.list);
252 |
253 | try t.expectEqual(2, args.count());
254 | try t.expectEqual(true, args.contains("l"));
255 | try t.expectEqual("", args.get("l").?);
256 |
257 | try t.expectEqual(true, args.contains("k"));
258 | try t.expectEqual("x", args.get("k").?);
259 | }
260 |
261 | fn testParse(args: []const []const u8) CommandLineArgs {
262 | const arena = t.allocator.create(ArenaAllocator) catch unreachable;
263 | arena.* = ArenaAllocator.init(t.allocator);
264 |
265 | const it = arena.allocator().create(TestIterator) catch unreachable;
266 | it.* = .{ .args = args };
267 | return CommandLineArgs.parseFromIterator(arena, it) catch unreachable;
268 | }
269 |
270 | const TestIterator = struct {
271 | pos: usize = 0,
272 | args: []const []const u8,
273 |
274 | fn next(self: *TestIterator) ?[]const u8 {
275 | const pos = self.pos;
276 | const args = self.args;
277 | if (pos == args.len) {
278 | return null;
279 | }
280 | const arg = args[pos];
281 | self.pos = pos + 1;
282 | return arg;
283 | }
284 | };
285 |
--------------------------------------------------------------------------------
/test_runner.zig:
--------------------------------------------------------------------------------
1 | // in your build.zig, you can specify a custom test runner:
2 | // const tests = b.addTest(.{
3 | // .root_module = $MODULE_BEING_TESTED,
4 | // .test_runner = .{ .path = b.path("test_runner.zig"), .mode = .simple },
5 | // });
6 |
7 | const std = @import("std");
8 | const builtin = @import("builtin");
9 |
10 | const Allocator = std.mem.Allocator;
11 |
12 | const BORDER = "=" ** 80;
13 |
14 | // use in custom panic handler
15 | var current_test: ?[]const u8 = null;
16 |
17 | pub fn main() !void {
18 | var mem: [8192]u8 = undefined;
19 | var fba = std.heap.FixedBufferAllocator.init(&mem);
20 |
21 | const allocator = fba.allocator();
22 |
23 | const env = Env.init(allocator);
24 | defer env.deinit(allocator);
25 |
26 | var slowest = SlowTracker.init(allocator, 5);
27 | defer slowest.deinit();
28 |
29 | var pass: usize = 0;
30 | var fail: usize = 0;
31 | var skip: usize = 0;
32 | var leak: usize = 0;
33 |
34 | Printer.fmt("\r\x1b[0K", .{}); // beginning of line and clear to end of line
35 |
36 | for (builtin.test_functions) |t| {
37 | if (isSetup(t)) {
38 | t.func() catch |err| {
39 | Printer.status(.fail, "\nsetup \"{s}\" failed: {}\n", .{ t.name, err });
40 | return err;
41 | };
42 | }
43 | }
44 |
45 | for (builtin.test_functions) |t| {
46 | if (isSetup(t) or isTeardown(t)) {
47 | continue;
48 | }
49 |
50 | var status = Status.pass;
51 | slowest.startTiming();
52 |
53 | const is_unnamed_test = isUnnamed(t);
54 | if (env.filter) |f| {
55 | if (!is_unnamed_test and std.mem.indexOf(u8, t.name, f) == null) {
56 | continue;
57 | }
58 | }
59 |
60 | const friendly_name = blk: {
61 | const name = t.name;
62 | var it = std.mem.splitScalar(u8, name, '.');
63 | while (it.next()) |value| {
64 | if (std.mem.eql(u8, value, "test")) {
65 | const rest = it.rest();
66 | break :blk if (rest.len > 0) rest else name;
67 | }
68 | }
69 | break :blk name;
70 | };
71 |
72 | current_test = friendly_name;
73 | std.testing.allocator_instance = .{};
74 | const result = t.func();
75 | current_test = null;
76 |
77 | const ns_taken = slowest.endTiming(friendly_name);
78 |
79 | if (std.testing.allocator_instance.deinit() == .leak) {
80 | leak += 1;
81 | Printer.status(.fail, "\n{s}\n\"{s}\" - Memory Leak\n{s}\n", .{ BORDER, friendly_name, BORDER });
82 | }
83 |
84 | if (result) |_| {
85 | pass += 1;
86 | } else |err| switch (err) {
87 | error.SkipZigTest => {
88 | skip += 1;
89 | status = .skip;
90 | },
91 | else => {
92 | status = .fail;
93 | fail += 1;
94 | Printer.status(.fail, "\n{s}\n\"{s}\" - {s}\n{s}\n", .{ BORDER, friendly_name, @errorName(err), BORDER });
95 | if (@errorReturnTrace()) |trace| {
96 | std.debug.dumpStackTrace(trace.*);
97 | }
98 | if (env.fail_first) {
99 | break;
100 | }
101 | },
102 | }
103 |
104 | if (env.verbose) {
105 | const ms = @as(f64, @floatFromInt(ns_taken)) / 1_000_000.0;
106 | Printer.status(status, "{s} ({d:.2}ms)\n", .{ friendly_name, ms });
107 | } else {
108 | Printer.status(status, ".", .{});
109 | }
110 | }
111 |
112 | for (builtin.test_functions) |t| {
113 | if (isTeardown(t)) {
114 | t.func() catch |err| {
115 | Printer.status(.fail, "\nteardown \"{s}\" failed: {}\n", .{ t.name, err });
116 | return err;
117 | };
118 | }
119 | }
120 |
121 | const total_tests = pass + fail;
122 | const status = if (fail == 0) Status.pass else Status.fail;
123 | Printer.status(status, "\n{d} of {d} test{s} passed\n", .{ pass, total_tests, if (total_tests != 1) "s" else "" });
124 | if (skip > 0) {
125 | Printer.status(.skip, "{d} test{s} skipped\n", .{ skip, if (skip != 1) "s" else "" });
126 | }
127 | if (leak > 0) {
128 | Printer.status(.fail, "{d} test{s} leaked\n", .{ leak, if (leak != 1) "s" else "" });
129 | }
130 | Printer.fmt("\n", .{});
131 | try slowest.display();
132 | Printer.fmt("\n", .{});
133 | std.posix.exit(if (fail == 0) 0 else 1);
134 | }
135 |
136 | const Printer = struct {
137 | fn fmt(comptime format: []const u8, args: anytype) void {
138 | std.debug.print(format, args);
139 | }
140 |
141 | fn status(s: Status, comptime format: []const u8, args: anytype) void {
142 | switch (s) {
143 | .pass => std.debug.print("\x1b[32m", .{}),
144 | .fail => std.debug.print("\x1b[31m", .{}),
145 | .skip => std.debug.print("\x1b[33m", .{}),
146 | else => {},
147 | }
148 | std.debug.print(format ++ "\x1b[0m", args);
149 | }
150 | };
151 |
152 | const Status = enum {
153 | pass,
154 | fail,
155 | skip,
156 | text,
157 | };
158 |
159 | const SlowTracker = struct {
160 | const SlowestQueue = std.PriorityDequeue(TestInfo, void, compareTiming);
161 | max: usize,
162 | slowest: SlowestQueue,
163 | timer: std.time.Timer,
164 |
165 | fn init(allocator: Allocator, count: u32) SlowTracker {
166 | const timer = std.time.Timer.start() catch @panic("failed to start timer");
167 | var slowest = SlowestQueue.init(allocator, {});
168 | slowest.ensureTotalCapacity(count) catch @panic("OOM");
169 | return .{
170 | .max = count,
171 | .timer = timer,
172 | .slowest = slowest,
173 | };
174 | }
175 |
176 | const TestInfo = struct {
177 | ns: u64,
178 | name: []const u8,
179 | };
180 |
181 | fn deinit(self: SlowTracker) void {
182 | self.slowest.deinit();
183 | }
184 |
185 | fn startTiming(self: *SlowTracker) void {
186 | self.timer.reset();
187 | }
188 |
189 | fn endTiming(self: *SlowTracker, test_name: []const u8) u64 {
190 | var timer = self.timer;
191 | const ns = timer.lap();
192 |
193 | var slowest = &self.slowest;
194 |
195 | if (slowest.count() < self.max) {
196 | // Capacity is fixed to the # of slow tests we want to track
197 | // If we've tracked fewer tests than this capacity, than always add
198 | slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing");
199 | return ns;
200 | }
201 |
202 | {
203 | // Optimization to avoid shifting the dequeue for the common case
204 | // where the test isn't one of our slowest.
205 | const fastest_of_the_slow = slowest.peekMin() orelse unreachable;
206 | if (fastest_of_the_slow.ns > ns) {
207 | // the test was faster than our fastest slow test, don't add
208 | return ns;
209 | }
210 | }
211 |
212 | // the previous fastest of our slow tests, has been pushed off.
213 | _ = slowest.removeMin();
214 | slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing");
215 | return ns;
216 | }
217 |
218 | fn display(self: *SlowTracker) !void {
219 | var slowest = self.slowest;
220 | const count = slowest.count();
221 | Printer.fmt("Slowest {d} test{s}: \n", .{ count, if (count != 1) "s" else "" });
222 | while (slowest.removeMinOrNull()) |info| {
223 | const ms = @as(f64, @floatFromInt(info.ns)) / 1_000_000.0;
224 | Printer.fmt(" {d:.2}ms\t{s}\n", .{ ms, info.name });
225 | }
226 | }
227 |
228 | fn compareTiming(context: void, a: TestInfo, b: TestInfo) std.math.Order {
229 | _ = context;
230 | return std.math.order(a.ns, b.ns);
231 | }
232 | };
233 |
234 | const Env = struct {
235 | verbose: bool,
236 | fail_first: bool,
237 | filter: ?[]const u8,
238 |
239 | fn init(allocator: Allocator) Env {
240 | return .{
241 | .verbose = readEnvBool(allocator, "TEST_VERBOSE", true),
242 | .fail_first = readEnvBool(allocator, "TEST_FAIL_FIRST", false),
243 | .filter = readEnv(allocator, "TEST_FILTER"),
244 | };
245 | }
246 |
247 | fn deinit(self: Env, allocator: Allocator) void {
248 | if (self.filter) |f| {
249 | allocator.free(f);
250 | }
251 | }
252 |
253 | fn readEnv(allocator: Allocator, key: []const u8) ?[]const u8 {
254 | const v = std.process.getEnvVarOwned(allocator, key) catch |err| {
255 | if (err == error.EnvironmentVariableNotFound) {
256 | return null;
257 | }
258 | std.log.warn("failed to get env var {s} due to err {}", .{ key, err });
259 | return null;
260 | };
261 | return v;
262 | }
263 |
264 | fn readEnvBool(allocator: Allocator, key: []const u8, deflt: bool) bool {
265 | const value = readEnv(allocator, key) orelse return deflt;
266 | defer allocator.free(value);
267 | return std.ascii.eqlIgnoreCase(value, "true");
268 | }
269 | };
270 |
271 | pub const panic = std.debug.FullPanic(struct {
272 | pub fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn {
273 | if (current_test) |ct| {
274 | std.debug.print("\x1b[31m{s}\npanic running \"{s}\"\n{s}\x1b[0m\n", .{ BORDER, ct, BORDER });
275 | }
276 | std.debug.defaultPanic(msg, first_trace_addr);
277 | }
278 | }.panicFn);
279 |
280 | fn isUnnamed(t: std.builtin.TestFn) bool {
281 | const marker = ".test_";
282 | const test_name = t.name;
283 | const index = std.mem.indexOf(u8, test_name, marker) orelse return false;
284 | _ = std.fmt.parseInt(u32, test_name[index + marker.len ..], 10) catch return false;
285 | return true;
286 | }
287 |
288 | fn isSetup(t: std.builtin.TestFn) bool {
289 | return std.mem.endsWith(u8, t.name, "tests:beforeAll");
290 | }
291 |
292 | fn isTeardown(t: std.builtin.TestFn) bool {
293 | return std.mem.endsWith(u8, t.name, "tests:afterAll");
294 | }
295 |
--------------------------------------------------------------------------------
/src/arc.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const Allocator = std.mem.Allocator;
4 | const ArenaAllocator = std.heap.ArenaAllocator;
5 |
6 | pub fn LockRefArenaArc(comptime T: type) type {
7 | // FullArgs is a tuple that represents the arguments to T.init(...)
8 | // The first argument to T.init must always be an Allocator (something we enforce here)
9 | // But, when the init(...) and setValue(...) methods of this struct are called
10 | // we don't expect FullArgs. We expect something like FullArgs[1..], that is,
11 | // the arguments without the allocator. This is because we'll inject an arena
12 | // allocator into the args within init/setValue.
13 | // Thus, Args, more or less, FullArgs[1..], but we can't just do FullArgs[1..]
14 | // we need to build a new Struct.
15 | const FullArgs = std.meta.ArgsTuple(@TypeOf(T.init));
16 | const full_fields = std.meta.fields(FullArgs);
17 | const ARG_COUNT = full_fields.len;
18 |
19 | if (ARG_COUNT == 0 or full_fields[0].type != std.mem.Allocator) {
20 | @compileError("The first argument to " ++ @typeName(T) ++ ".init must be an std.mem.Allocator");
21 | }
22 |
23 | var arg_fields: [full_fields.len - 1]std.builtin.Type.StructField = undefined;
24 | inline for (full_fields[1..], 0..) |field, index| {
25 | arg_fields[index] = field;
26 | // shift the name down by 1
27 | // so if our FullArgs is (allocator: Allocator, id: usize)
28 | // 0 1
29 | // then our Args will be (id: usize)
30 | // 0
31 | arg_fields[index].name = std.fmt.comptimePrint("{d}", .{index});
32 | }
33 |
34 | const Args = @Type(.{
35 | .@"struct" = .{
36 | .layout = .auto,
37 | .is_tuple = true,
38 | .fields = &arg_fields,
39 | .decls = &[_]std.builtin.Type.Declaration{},
40 | },
41 | });
42 |
43 | return struct {
44 | arc: *Arc,
45 | allocator: Allocator,
46 | mutex: std.Thread.Mutex,
47 |
48 | const Self = @This();
49 |
50 | pub fn init(allocator: Allocator, args: Args) !Self {
51 | return .{
52 | .mutex = .{},
53 | .allocator = allocator,
54 | .arc = try createArc(allocator, args),
55 | };
56 | }
57 |
58 | pub fn initWithValue(allocator: Allocator, value: T) !Self {
59 | const arena = try allocator.create(ArenaAllocator);
60 | errdefer allocator.destroy(arena);
61 |
62 | arena.* = std.heap.ArenaAllocator.init(allocator);
63 | errdefer arena.deinit();
64 |
65 | const arc = try arena.allocator().create(Self.Arc);
66 | arc.* = .{
67 | ._rc = 1,
68 | .arena = arena,
69 | .value = value,
70 | };
71 |
72 | return .{
73 | .arc = arc,
74 | .mutex = .{},
75 | .allocator = allocator,
76 | };
77 | }
78 |
79 | pub fn deinit(self: *Self) void {
80 | self.mutex.lock();
81 | self.arc.release();
82 | self.mutex.unlock();
83 | }
84 |
85 | pub fn acquire(self: *Self) *Arc {
86 | self.mutex.lock();
87 | defer self.mutex.unlock();
88 | var arc = self.arc;
89 | arc.acquire();
90 | return arc;
91 | }
92 |
93 | pub fn setValue(self: *Self, args: Args) !void {
94 | const arc = try createArc(self.allocator, args);
95 | self.mutex.lock();
96 | var existing = self.arc;
97 | self.arc = arc;
98 | self.mutex.unlock();
99 | existing.release();
100 | }
101 |
102 | fn createArc(allocator: Allocator, args: Args) !*Arc {
103 | const arena = try allocator.create(ArenaAllocator);
104 | errdefer allocator.destroy(arena);
105 |
106 | arena.* = std.heap.ArenaAllocator.init(allocator);
107 | errdefer arena.deinit();
108 |
109 | const aa = arena.allocator();
110 | // args doesn't contain our allocator
111 | // we're going to push the arc.arena.allocator at the head of args
112 | // which means creating a new args and copying the values over
113 | var full_args: FullArgs = undefined;
114 | full_args[0] = aa;
115 | inline for (1..ARG_COUNT) |i| {
116 | full_args[i] = args[i - 1];
117 | }
118 |
119 | const arc = try aa.create(Arc);
120 | arc.* = .{
121 | ._rc = 1,
122 | .arena = arena,
123 | .value = try @call(.auto, T.init, full_args),
124 | };
125 | return arc;
126 | }
127 |
128 | pub fn jsonStringify(self: *Self, jws: anytype) !void {
129 | var arc = self.acquire();
130 | defer arc.release();
131 | return jws.write(arc.value);
132 | }
133 |
134 | pub const Arc = struct {
135 | value: T,
136 | arena: *ArenaAllocator,
137 | _rc: usize,
138 |
139 | pub fn acquire(self: *Arc) void {
140 | _ = @atomicRmw(usize, &self._rc, .Add, 1, .monotonic);
141 | }
142 |
143 | pub fn release(self: *Arc) void {
144 | // returns the value before the sub, so if the value before the sub was 1,
145 | // it means we no longer have anything referencing this
146 | if (@atomicRmw(usize, &self._rc, .Sub, 1, .monotonic) == 1) {
147 | const arena = self.arena;
148 | const allocator = self.arena.child_allocator;
149 | arena.deinit();
150 | allocator.destroy(arena);
151 | }
152 | }
153 |
154 | pub fn jsonStringify(self: *const Arc, jws: anytype) !void {
155 | return jws.write(self.value);
156 | }
157 | };
158 | };
159 | }
160 |
161 | pub fn LockRefArc(comptime T: type) type {
162 | return struct {
163 | arc: *Arc,
164 | allocator: Allocator,
165 | mutex: std.Thread.Mutex,
166 |
167 | const Self = @This();
168 |
169 | pub fn init(allocator: Allocator, value: T) !Self {
170 | return .{
171 | .mutex = .{},
172 | .allocator = allocator,
173 | .arc = try createArc(allocator, value),
174 | };
175 | }
176 |
177 | pub fn deinit(self: *Self) void {
178 | self.mutex.lock();
179 | self.arc.release();
180 | self.mutex.unlock();
181 | }
182 |
183 | pub fn acquire(self: *Self) *Arc {
184 | self.mutex.lock();
185 | defer self.mutex.unlock();
186 | var arc = self.arc;
187 | arc.acquire();
188 | return arc;
189 | }
190 |
191 | pub fn setValue(self: *Self, value: T) !void {
192 | const arc = try createArc(self.allocator, value);
193 | self.mutex.lock();
194 | var existing = self.arc;
195 | self.arc = arc;
196 | self.mutex.unlock();
197 | existing.release();
198 | }
199 |
200 | fn createArc(allocator: Allocator, value: T) !*Self.Arc {
201 | const arc = try allocator.create(Self.Arc);
202 | arc.* = .{
203 | ._rc = 1,
204 | .value = value,
205 | .allocator = allocator,
206 | };
207 | return arc;
208 | }
209 |
210 | pub fn jsonStringify(self: *Self, jws: anytype) !void {
211 | var arc = self.acquire();
212 | defer arc.release();
213 | return jws.write(arc.value);
214 | }
215 |
216 | pub const Arc = struct {
217 | value: T,
218 | allocator: Allocator,
219 | _rc: usize,
220 |
221 | pub fn acquire(self: *Arc) void {
222 | _ = @atomicRmw(usize, &self._rc, .Add, 1, .monotonic);
223 | }
224 |
225 | pub fn release(self: *Arc) void {
226 | // returns the value before the sub, so if the value before the sub was 1,
227 | // it means we no longer have anything referencing this
228 | if (@atomicRmw(usize, &self._rc, .Sub, 1, .monotonic) == 1) {
229 | const allocator = self.allocator;
230 | allocator.destroy(self);
231 | }
232 | }
233 |
234 | pub fn jsonStringify(self: *const Arc, jws: anytype) !void {
235 | return jws.write(self.value);
236 | }
237 | };
238 | };
239 | }
240 |
241 | const t = @import("zul.zig").testing;
242 | test "LockRefArenaArc" {
243 | {
244 | var ref = try LockRefArenaArc(TestArenaValue).init(t.allocator, .{"test"});
245 | ref.deinit();
246 | }
247 |
248 | var ref = try LockRefArenaArc(TestArenaValue).init(t.allocator, .{"hello"});
249 | defer ref.deinit();
250 |
251 | // keep this one around and re-test it at the end, it should still be valid
252 | // and still be the same value
253 | const arc1 = ref.acquire();
254 | defer arc1.release();
255 | try t.expectEqual("hello", arc1.value.str);
256 |
257 | try ref.setValue(.{"world"});
258 |
259 | {
260 | const arc2 = ref.acquire();
261 | defer arc2.release();
262 | try t.expectEqual("world", arc2.value.str);
263 | }
264 |
265 | // this reference should still be valid
266 | try t.expectEqual("hello", arc1.value.str);
267 | }
268 |
269 | test "LockRefArenaArc: initWithValue" {
270 | var ref = try LockRefArenaArc(TestArenaValue).initWithValue(t.allocator, .{ .str = "teg" });
271 | defer ref.deinit();
272 |
273 | const arc1 = ref.acquire();
274 | defer arc1.release();
275 | try t.expectEqual("teg", arc1.value.str);
276 | }
277 |
278 | test "LockRefArc" {
279 | {
280 | var ref = try LockRefArc(TestValue).init(t.allocator, .{ .str = "test" });
281 | ref.deinit();
282 | }
283 |
284 | var ref = try LockRefArc(TestValue).init(t.allocator, .{ .str = "hello" });
285 | defer ref.deinit();
286 |
287 | // keep this one around and re-test it at the end, it should still be valid
288 | // and still be the same value
289 | const arc1 = ref.acquire();
290 | defer arc1.release();
291 | try t.expectEqual("hello", arc1.value.str);
292 |
293 | try ref.setValue(.{ .str = "world" });
294 |
295 | {
296 | const arc2 = ref.acquire();
297 | defer arc2.release();
298 | try t.expectEqual("world", arc2.value.str);
299 | }
300 |
301 | // this reference should still be valid
302 | try t.expectEqual("hello", arc1.value.str);
303 | }
304 |
305 | const TestArenaValue = struct {
306 | str: []const u8,
307 |
308 | fn init(allocator: Allocator, original: []const u8) !TestArenaValue {
309 | return .{ .str = try allocator.dupe(u8, original) };
310 | }
311 | };
312 |
313 | const TestValue = struct {
314 | str: []const u8,
315 | };
316 |
--------------------------------------------------------------------------------
/src/uuid.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 |
3 | const fmt = std.fmt;
4 | const crypto = std.crypto;
5 | const Allocator = std.mem.Allocator;
6 |
7 | var clock_sequence: u16 = 0;
8 | var last_timestamp: u64 = 0;
9 |
10 | pub const UUID = struct {
11 | bin: [16]u8,
12 |
13 | pub fn seed() void {
14 | var b: [2]u8 = undefined;
15 | crypto.random.bytes(&b);
16 | @atomicStore(u16, *clock_sequence, std.mem.readInt(u16, &b, .big), .monotonic);
17 | }
18 |
19 | pub fn v4() UUID {
20 | var bin: [16]u8 = undefined;
21 | crypto.random.bytes(&bin);
22 | bin[6] = (bin[6] & 0x0f) | 0x40;
23 | bin[8] = (bin[8] & 0x3f) | 0x80;
24 | return .{ .bin = bin };
25 | }
26 |
27 | pub fn v7() UUID {
28 | const ts: u64 = @intCast(std.time.milliTimestamp());
29 | const last = @atomicRmw(u64, &last_timestamp, .Xchg, ts, .monotonic);
30 | const sequence = if (ts <= last)
31 | @atomicRmw(u16, &clock_sequence, .Add, 1, .monotonic) + 1
32 | else
33 | @atomicLoad(u16, &clock_sequence, .monotonic);
34 |
35 | var bin: [16]u8 = undefined;
36 | const ts_buf = std.mem.asBytes(&ts);
37 | bin[0] = ts_buf[5];
38 | bin[1] = ts_buf[4];
39 | bin[2] = ts_buf[3];
40 | bin[3] = ts_buf[2];
41 | bin[4] = ts_buf[1];
42 | bin[5] = ts_buf[0];
43 |
44 | const seq_buf = std.mem.asBytes(&sequence);
45 | // sequence + version
46 | bin[6] = (seq_buf[1] & 0x0f) | 0x70;
47 | bin[7] = seq_buf[0];
48 |
49 | crypto.random.bytes(bin[8..]);
50 |
51 | //variant
52 | bin[8] = (bin[8] & 0x3f) | 0x80;
53 |
54 | return .{ .bin = bin };
55 | }
56 |
57 | pub fn random() UUID {
58 | var bin: [16]u8 = undefined;
59 | crypto.random.bytes(&bin);
60 | return .{ .bin = bin };
61 | }
62 |
63 | pub fn parse(hex: []const u8) !UUID {
64 | var bin: [16]u8 = undefined;
65 |
66 | if (hex.len != 36 or hex[8] != '-' or hex[13] != '-' or hex[18] != '-' or hex[23] != '-') {
67 | return error.InvalidUUID;
68 | }
69 |
70 | inline for (encoded_pos, 0..) |i, j| {
71 | const hi = hex_to_nibble[hex[i + 0]];
72 | const lo = hex_to_nibble[hex[i + 1]];
73 | if (hi == 0xff or lo == 0xff) {
74 | return error.InvalidUUID;
75 | }
76 | bin[j] = hi << 4 | lo;
77 | }
78 | return .{ .bin = bin };
79 | }
80 |
81 | pub fn binToHex(bin: []const u8, case: std.fmt.Case) ![36]u8 {
82 | if (bin.len != 16) {
83 | return error.InvalidUUID;
84 | }
85 | var hex: [36]u8 = undefined;
86 | b2h(bin, &hex, case);
87 | return hex;
88 | }
89 |
90 | pub fn eql(self: UUID, other: UUID) bool {
91 | inline for (self.bin, other.bin) |a, b| {
92 | if (a != b) return false;
93 | }
94 | return true;
95 | }
96 |
97 | pub fn toHexAlloc(self: UUID, allocator: std.mem.Allocator, case: std.fmt.Case) ![]u8 {
98 | const hex = try allocator.alloc(u8, 36);
99 | _ = self.toHexBuf(hex, case);
100 | return hex;
101 | }
102 |
103 | pub fn toHex(self: UUID, case: std.fmt.Case) [36]u8 {
104 | var hex: [36]u8 = undefined;
105 | _ = self.toHexBuf(&hex, case);
106 | return hex;
107 | }
108 |
109 | pub fn toHexBuf(self: UUID, hex: []u8, case: std.fmt.Case) []u8 {
110 | std.debug.assert(hex.len >= 36);
111 | b2h(&self.bin, hex, case);
112 | return hex[0..36];
113 | }
114 |
115 | pub fn jsonStringify(self: UUID, out: anytype) !void {
116 | var hex: [38]u8 = undefined;
117 | hex[0] = '"';
118 | _ = self.toHexBuf(hex[1..37], .lower);
119 | hex[37] = '"';
120 | try out.print("{s}", .{hex});
121 | }
122 |
123 | pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !UUID {
124 | const hex = try std.json.innerParse([]const u8, allocator, source, options);
125 | return UUID.parse(hex) catch error.UnexpectedToken;
126 | }
127 |
128 | pub fn lower(self: UUID) Formatter {
129 | return .{ .uuid = self, .case = .lower };
130 | }
131 | pub fn upper(self: UUID) Formatter {
132 | return .{ .uuid = self, .case = .upper };
133 | }
134 | pub fn format(self: UUID, writer: *std.Io.Writer) !void {
135 | return writer.writeAll(&self.toHex(.lower));
136 | }
137 | };
138 |
139 | pub const Formatter = struct {
140 | uuid: UUID,
141 | case: std.fmt.Case,
142 |
143 | pub fn format(self: Formatter, writer: *std.Io.Writer) !void {
144 | return writer.writeAll(&self.uuid.toHex(self.case));
145 | }
146 | };
147 |
148 | fn b2h(bin: []const u8, hex: []u8, case: std.fmt.Case) void {
149 | const alphabet = if (case == .lower) "0123456789abcdef" else "0123456789ABCDEF";
150 |
151 | hex[8] = '-';
152 | hex[13] = '-';
153 | hex[18] = '-';
154 | hex[23] = '-';
155 |
156 | inline for (encoded_pos, 0..) |i, j| {
157 | hex[i + 0] = alphabet[bin[j] >> 4];
158 | hex[i + 1] = alphabet[bin[j] & 0x0f];
159 | }
160 | }
161 |
162 | const encoded_pos = [16]u8{ 0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34 };
163 |
164 | const hex_to_nibble = [_]u8{0xff} ** 48 ++ [_]u8{
165 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
166 | 0x08, 0x09, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
167 | 0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0xff,
168 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
169 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
170 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
171 | 0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0xff,
172 | } ++ [_]u8{0xff} ** 152;
173 |
174 | const t = @import("zul.zig").testing;
175 | test "uuid: parse" {
176 | const lower_uuids = [_][]const u8{
177 | "d0cd8041-0504-40cb-ac8e-d05960d205ec",
178 | "3df6f0e4-f9b1-4e34-ad70-33206069b995",
179 | "f982cf56-c4ab-4229-b23c-d17377d000be",
180 | "6b9f53be-cf46-40e8-8627-6b60dc33def8",
181 | "c282ec76-ac18-4d4a-8a29-3b94f5c74813",
182 | "00000000-0000-0000-0000-000000000000",
183 | };
184 |
185 | for (lower_uuids) |hex| {
186 | const uuid = try UUID.parse(hex);
187 | try t.expectEqual(hex, uuid.toHex(.lower));
188 | }
189 |
190 | const upper_uuids = [_][]const u8{
191 | "D0CD8041-0504-40CB-AC8E-D05960D205EC",
192 | "3DF6F0E4-F9B1-4E34-AD70-33206069B995",
193 | "F982CF56-C4AB-4229-B23C-D17377D000BE",
194 | "6B9F53BE-CF46-40E8-8627-6B60DC33DEF8",
195 | "C282EC76-AC18-4D4A-8A29-3B94F5C74813",
196 | "00000000-0000-0000-0000-000000000000",
197 | };
198 |
199 | for (upper_uuids) |hex| {
200 | const uuid = try UUID.parse(hex);
201 | try t.expectEqual(hex, uuid.toHex(.upper));
202 | }
203 | }
204 |
205 | test "uuid: parse invalid" {
206 | const uuids = [_][]const u8{
207 | "3df6f0e4-f9b1-4e34-ad70-33206069b99", // too short
208 | "3df6f0e4-f9b1-4e34-ad70-33206069b9912", // too long
209 | "3df6f0e4-f9b1-4e34-ad70_33206069b9912", // missing or invalid group separator
210 | "zdf6f0e4-f9b1-4e34-ad70-33206069b995", // invalid character
211 | };
212 |
213 | for (uuids) |uuid| {
214 | try t.expectError(error.InvalidUUID, UUID.parse(uuid));
215 | }
216 | }
217 |
218 | test "uuid: v4" {
219 | defer t.reset();
220 | const allocator = t.arena.allocator();
221 | var seen = std.StringHashMap(void).init(allocator);
222 | try seen.ensureTotalCapacity(100);
223 |
224 | for (0..100) |_| {
225 | const uuid = UUID.v4();
226 | try t.expectEqual(@as(usize, 16), uuid.bin.len);
227 | try t.expectEqual(4, uuid.bin[6] >> 4);
228 | try t.expectEqual(0x80, uuid.bin[8] & 0xc0);
229 | seen.putAssumeCapacity(try uuid.toHexAlloc(allocator, .lower), {});
230 | }
231 | try t.expectEqual(100, seen.count());
232 | }
233 |
234 | test "uuid: v7" {
235 | defer t.reset();
236 | const allocator = t.arena.allocator();
237 | var seen = std.StringHashMap(void).init(allocator);
238 | try seen.ensureTotalCapacity(100);
239 |
240 | var last: u64 = 0;
241 | for (0..100) |_| {
242 | const uuid = UUID.v7();
243 | try t.expectEqual(@as(usize, 16), uuid.bin.len);
244 | try t.expectEqual(7, uuid.bin[6] >> 4);
245 | try t.expectEqual(0x80, uuid.bin[8] & 0xc0);
246 | seen.putAssumeCapacity(try uuid.toHexAlloc(allocator, .lower), {});
247 |
248 | const ts = std.mem.readInt(u64, uuid.bin[0..8], .big);
249 | try t.expectEqual(true, ts > last);
250 | last = ts;
251 | }
252 | try t.expectEqual(100, seen.count());
253 | }
254 |
255 | test "uuid: hex" {
256 | for (0..20) |_| {
257 | const uuid = UUID.random();
258 | const upper = uuid.toHex(.upper);
259 | const lower = uuid.toHex(.lower);
260 |
261 | try t.expectEqual(true, std.ascii.eqlIgnoreCase(&lower, &upper));
262 |
263 | for (upper, lower, 0..) |u, l, i| {
264 | if (i == 8 or i == 13 or i == 18 or i == 23) {
265 | try t.expectEqual('-', u);
266 | try t.expectEqual('-', l);
267 | } else {
268 | try t.expectEqual(true, (u >= '0' and u <= '9') or (u >= 'A' and u <= 'F'));
269 | try t.expectEqual(true, (l >= '0' and l <= '9') or (l >= 'a' and l <= 'f'));
270 | }
271 | }
272 | }
273 | }
274 |
275 | test "uuid: binToHex" {
276 | for (0..20) |_| {
277 | const uuid = UUID.random();
278 | try t.expectEqual(&(try UUID.binToHex(&uuid.bin, .lower)), uuid.toHex(.lower));
279 | }
280 | }
281 |
282 | test "uuid: json" {
283 | defer t.reset();
284 | const uuid = try UUID.parse("938b1cd2-f479-442b-9ba6-59ebf441e695");
285 |
286 | const out = try std.json.Stringify.valueAlloc(t.arena.allocator(), .{
287 | .uuid = uuid,
288 | }, .{});
289 |
290 | try t.expectEqual("{\"uuid\":\"938b1cd2-f479-442b-9ba6-59ebf441e695\"}", out);
291 |
292 | const S = struct { uuid: UUID };
293 | const parsed = try std.json.parseFromSlice(S, t.allocator, out, .{});
294 | defer parsed.deinit();
295 |
296 | try t.expectEqual("938b1cd2-f479-442b-9ba6-59ebf441e695", &parsed.value.uuid.toHex(.lower));
297 | }
298 |
299 | test "uuid: format" {
300 | const uuid = try UUID.parse("d543E371-a33d-4e68-87ba-7c9e3470a3be");
301 |
302 | var buf: [50]u8 = undefined;
303 |
304 | {
305 | const str = try std.fmt.bufPrint(&buf, "[{f}]", .{uuid.lower()});
306 | try t.expectEqual("[d543e371-a33d-4e68-87ba-7c9e3470a3be]", str);
307 | }
308 |
309 | {
310 | const str = try std.fmt.bufPrint(&buf, "[{f}]", .{uuid});
311 | try t.expectEqual("[d543e371-a33d-4e68-87ba-7c9e3470a3be]", str);
312 | }
313 |
314 | {
315 | const str = try std.fmt.bufPrint(&buf, "[{f}]", .{uuid.upper()});
316 | try t.expectEqual("[D543E371-A33D-4E68-87BA-7C9E3470A3BE]", str);
317 | }
318 | }
319 |
320 | test "uuid: eql" {
321 | const uuid1 = UUID.v4();
322 | const uuid2 = try UUID.parse("2a7af44c-3b7e-41f6-8764-1aff701a024a");
323 | const uuid3 = try UUID.parse("2a7af44c-3b7e-41f6-8764-1aff701a024a");
324 | const uuid4 = try UUID.parse("5cc75a16-8592-4de3-8215-89824a9c62c0");
325 |
326 | try t.expectEqual(false, uuid1.eql(uuid2));
327 | try t.expectEqual(false, uuid2.eql(uuid1));
328 |
329 | try t.expectEqual(false, uuid1.eql(uuid3));
330 | try t.expectEqual(false, uuid3.eql(uuid1));
331 |
332 | try t.expectEqual(false, uuid1.eql(uuid4));
333 | try t.expectEqual(false, uuid4.eql(uuid1));
334 |
335 | try t.expectEqual(false, uuid2.eql(uuid4));
336 | try t.expectEqual(false, uuid4.eql(uuid2));
337 |
338 | try t.expectEqual(false, uuid3.eql(uuid4));
339 | try t.expectEqual(false, uuid4.eql(uuid3));
340 |
341 | try t.expectEqual(true, uuid2.eql(uuid3));
342 | try t.expectEqual(true, uuid3.eql(uuid2));
343 | }
344 |
--------------------------------------------------------------------------------
/src/ulid.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const crypto = std.crypto;
3 | const time = std.time;
4 | const testing = std.testing;
5 |
6 | // ULID Specification Implementation for Zig
7 | // Based on https://github.com/ulid/spec
8 |
9 | // Constants from ULID specification
10 | const ENCODING_LENGTH = 26;
11 | const TIME_LENGTH = 10;
12 | const RANDOM_LENGTH = 16;
13 | const TIMESTAMP_BYTES = 6;
14 | const RANDOMNESS_BYTES = 10;
15 | const TOTAL_BYTES = TIMESTAMP_BYTES + RANDOMNESS_BYTES;
16 |
17 | // Maximum timestamp value (2^48 - 1)
18 | const MAX_TIMESTAMP: u64 = 281474976710655;
19 |
20 | // Crockford's Base32 alphabet (excludes I, L, O, U to avoid confusion)
21 | const ENCODING_CHARS = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
22 |
23 | // Decoding lookup table for Crockford's Base32
24 | const DECODING_TABLE = blk: {
25 | var table: [256]u8 = [_]u8{0xFF} ** 256;
26 |
27 | // Numbers 0-9
28 | for (0..10) |i| {
29 | table['0' + i] = @intCast(i);
30 | }
31 |
32 | // Letters A-Z (excluding I, L, O, U)
33 | const letters = "ABCDEFGHJKMNPQRSTVWXYZ";
34 | for (letters, 0..) |c, i| {
35 | table[c] = @intCast(i + 10);
36 | // Also support lowercase
37 | table[c + 32] = @intCast(i + 10);
38 | }
39 |
40 | // Handle special cases for excluded letters
41 | table['I'] = 1;
42 | table['i'] = 1; // I -> 1
43 | table['L'] = 1;
44 | table['l'] = 1; // L -> 1
45 | table['O'] = 0;
46 | table['o'] = 0; // O -> 0
47 | table['U'] = table['V'];
48 | table['u'] = table['v']; // U -> V
49 |
50 | break :blk table;
51 | };
52 |
53 | pub const ULIDError = error{
54 | InvalidLength,
55 | InvalidCharacter,
56 | TimestampTooLarge,
57 | ClockGoingBackwards,
58 | OutOfMemory,
59 | };
60 |
61 | /// ULID structure representing a 128-bit identifier
62 | pub const ULID = struct {
63 | bytes: [TOTAL_BYTES]u8,
64 |
65 | const Self = @This();
66 |
67 | /// Create a new ULID with the current timestamp and random data
68 | pub fn new() Self {
69 | return newWithTime(time.milliTimestamp());
70 | }
71 |
72 | /// Create a new ULID with a specific timestamp
73 | pub fn newWithTime(ts: i64) Self {
74 | var prng = std.rand.DefaultPrng.init(@intCast(time.nanoTimestamp()));
75 | return newWithTimeAndRandom(ts, prng.random());
76 | }
77 |
78 | /// Create a new ULID with specific timestamp and random source
79 | pub fn newWithTimeAndRandom(ts: i64, random: std.rand.Random) Self {
80 | const ts_byte: u64 = @intCast(@max(0, ts));
81 |
82 | var ulid = Self{ .bytes = undefined };
83 |
84 | // Encode timestamp (48 bits / 6 bytes) in big-endian
85 | ulid.bytes[0] = @intCast((ts_byte >> 40) & 0xFF);
86 | ulid.bytes[1] = @intCast((ts_byte >> 32) & 0xFF);
87 | ulid.bytes[2] = @intCast((ts_byte >> 24) & 0xFF);
88 | ulid.bytes[3] = @intCast((ts_byte >> 16) & 0xFF);
89 | ulid.bytes[4] = @intCast((ts_byte >> 8) & 0xFF);
90 | ulid.bytes[5] = @intCast(ts_byte & 0xFF);
91 |
92 | // Fill randomness (80 bits / 10 bytes)
93 | random.bytes(ulid.bytes[TIMESTAMP_BYTES..]);
94 |
95 | return ulid;
96 | }
97 |
98 | /// Create ULID from raw bytes
99 | pub fn fromBytes(bytes: [TOTAL_BYTES]u8) Self {
100 | return Self{ .bytes = bytes };
101 | }
102 |
103 | /// Parse ULID from string representation
104 | pub fn fromString(str: []const u8) ULIDError!Self {
105 | if (str.len != ENCODING_LENGTH) {
106 | return ULIDError.InvalidLength;
107 | }
108 |
109 | var ulid = Self{ .bytes = undefined };
110 |
111 | // Decode the string using Crockford's Base32
112 | var acc: u128 = 0;
113 | for (str) |c| {
114 | const val = DECODING_TABLE[c];
115 | if (val == 0xFF) {
116 | return ULIDError.InvalidCharacter;
117 | }
118 | acc = (acc << 5) | val;
119 | }
120 |
121 | // Extract bytes in big-endian order
122 | for (0..TOTAL_BYTES) |i| {
123 | ulid.bytes[TOTAL_BYTES - 1 - i] = @intCast(acc & 0xFF);
124 | acc >>= 8;
125 | }
126 |
127 | // Validate timestamp
128 | if (ulid.timestamp() > MAX_TIMESTAMP) {
129 | return ULIDError.TimestampTooLarge;
130 | }
131 |
132 | return ulid;
133 | }
134 |
135 | /// Convert ULID to string representation
136 | pub fn toString(self: Self, buffer: []u8) []u8 {
137 | std.debug.assert(buffer.len >= ENCODING_LENGTH);
138 |
139 | // Convert bytes to 128-bit integer
140 | var acc: u128 = 0;
141 | for (self.bytes) |byte| {
142 | acc = (acc << 8) | byte;
143 | }
144 |
145 | // Encode using Crockford's Base32
146 | for (0..ENCODING_LENGTH) |i| {
147 | buffer[ENCODING_LENGTH - 1 - i] = ENCODING_CHARS[@intCast(acc & 0x1F)];
148 | acc >>= 5;
149 | }
150 |
151 | return buffer[0..ENCODING_LENGTH];
152 | }
153 |
154 | /// Get timestamp component as milliseconds since Unix epoch
155 | pub fn timestamp(self: Self) u64 {
156 | return (@as(u64, self.bytes[0]) << 40) |
157 | (@as(u64, self.bytes[1]) << 32) |
158 | (@as(u64, self.bytes[2]) << 24) |
159 | (@as(u64, self.bytes[3]) << 16) |
160 | (@as(u64, self.bytes[4]) << 8) |
161 | (@as(u64, self.bytes[5]));
162 | }
163 |
164 | /// Get randomness component as bytes
165 | pub fn randomness(self: Self) [RANDOMNESS_BYTES]u8 {
166 | return self.bytes[TIMESTAMP_BYTES..][0..RANDOMNESS_BYTES].*;
167 | }
168 |
169 | /// Compare two ULIDs for ordering (lexicographic)
170 | pub fn compare(self: Self, other: Self) std.math.Order {
171 | return std.mem.order(u8, &self.bytes, &other.bytes);
172 | }
173 |
174 | /// Check if two ULIDs are equal
175 | pub fn eql(self: Self, other: Self) bool {
176 | return std.mem.eql(u8, &self.bytes, &other.bytes);
177 | }
178 |
179 | /// Format ULID for printing
180 | pub fn format(
181 | self: Self,
182 | comptime fmt: []const u8,
183 | options: std.fmt.FormatOptions,
184 | writer: anytype,
185 | ) !void {
186 | _ = fmt;
187 | _ = options;
188 | var buffer: [ENCODING_LENGTH]u8 = undefined;
189 | const str = self.toString(&buffer);
190 | try writer.writeAll(str);
191 | }
192 | };
193 |
194 | /// Monotonic ULID generator that ensures lexicographic ordering
195 | pub const MonotonicGenerator = struct {
196 | last_timestamp: u64,
197 | last_random: [RANDOMNESS_BYTES]u8,
198 | prng: std.rand.DefaultPrng,
199 |
200 | const Self = @This();
201 |
202 | pub fn init() Self {
203 | return Self{
204 | .last_timestamp = 0,
205 | .last_random = [_]u8{0} ** RANDOMNESS_BYTES,
206 | .prng = std.rand.DefaultPrng.init(@intCast(time.nanoTimestamp())),
207 | };
208 | }
209 |
210 | pub fn next(self: *Self) ULIDError!ULID {
211 | return self.nextWithTime(time.milliTimestamp());
212 | }
213 |
214 | pub fn nextWithTime(self: *Self, timestamp: i64) ULIDError!ULID {
215 | const ts: u64 = @intCast(@max(0, timestamp));
216 |
217 | if (ts > MAX_TIMESTAMP) {
218 | return ULIDError.TimestampTooLarge;
219 | }
220 |
221 | var ulid = ULID{ .bytes = undefined };
222 |
223 | // Encode timestamp
224 | ulid.bytes[0] = @intCast((ts >> 40) & 0xFF);
225 | ulid.bytes[1] = @intCast((ts >> 32) & 0xFF);
226 | ulid.bytes[2] = @intCast((ts >> 24) & 0xFF);
227 | ulid.bytes[3] = @intCast((ts >> 16) & 0xFF);
228 | ulid.bytes[4] = @intCast((ts >> 8) & 0xFF);
229 | ulid.bytes[5] = @intCast(ts & 0xFF);
230 |
231 | if (ts == self.last_timestamp) {
232 | // Same timestamp: increment the random part
233 | var carry: u16 = 1;
234 | var i: usize = RANDOMNESS_BYTES;
235 | while (i > 0 and carry > 0) {
236 | i -= 1;
237 | const sum = @as(u16, self.last_random[i]) + carry;
238 | self.last_random[i] = @intCast(sum & 0xFF);
239 | carry = sum >> 8;
240 | }
241 |
242 | // If we overflow, we need to generate new random data
243 | if (carry > 0) {
244 | self.prng.random().bytes(&self.last_random);
245 | }
246 | } else if (ts < self.last_timestamp) {
247 | return ULIDError.ClockGoingBackwards;
248 | } else {
249 | // New timestamp: generate new random data
250 | self.prng.random().bytes(&self.last_random);
251 | self.last_timestamp = ts;
252 | }
253 |
254 | // Copy randomness
255 | @memcpy(ulid.bytes[TIMESTAMP_BYTES..], &self.last_random);
256 |
257 | return ulid;
258 | }
259 | };
260 |
261 | // Tests
262 | test "ULID creation and parsing" {
263 | const ulid1 = ULID.new();
264 |
265 | var buffer: [ENCODING_LENGTH]u8 = undefined;
266 | const str = ulid1.toString(&buffer);
267 |
268 | try testing.expect(str.len == ENCODING_LENGTH);
269 |
270 | const ulid2 = try ULID.fromString(str);
271 | try testing.expect(ulid1.eql(ulid2));
272 | }
273 |
274 | test "ULID ordering" {
275 | const ulid1 = ULID.newWithTime(1000);
276 | const ulid2 = ULID.newWithTime(2000);
277 |
278 | try testing.expect(ulid1.compare(ulid2) == .lt);
279 | try testing.expect(ulid2.compare(ulid1) == .gt);
280 | try testing.expect(ulid1.compare(ulid1) == .eq);
281 | }
282 |
283 | test "ULID timestamp extraction" {
284 | const timestamp: i64 = 1609459200000; // 2021-01-01T00:00:00Z
285 | const ulid = ULID.newWithTime(timestamp);
286 |
287 | try testing.expect(ulid.timestamp() == timestamp);
288 | }
289 |
290 | test "ULID string format validation" {
291 | // Test invalid length
292 | try testing.expectError(ULIDError.InvalidLength, ULID.fromString("01ARZ3NDEKTSV4RRFFQ69G5FA"));
293 | try testing.expectError(ULIDError.InvalidLength, ULID.fromString("01ARZ3NDEKTSV4RRFFQ69G5FAVV"));
294 |
295 | // Test invalid characters
296 | try testing.expectError(ULIDError.InvalidCharacter, ULID.fromString("01ARZ3NDEKTSV4RRFFQ69G5F@V"));
297 | }
298 |
299 | test "ULID maximum timestamp" {
300 | // Test maximum valid timestamp
301 | const ulid = ULID.newWithTime(@intCast(MAX_TIMESTAMP));
302 | try testing.expect(ulid.timestamp() == MAX_TIMESTAMP);
303 |
304 | // Test that parsing maximum encoded ULID works
305 | const max_ulid_str = "7ZZZZZZZZZZZZZZZZZZZZZZZZZ";
306 | const max_ulid = try ULID.fromString(max_ulid_str);
307 | try testing.expect(max_ulid.timestamp() == MAX_TIMESTAMP);
308 | }
309 |
310 | test "MonotonicGenerator" {
311 | var generator = MonotonicGenerator.init();
312 |
313 | const ulid1 = try generator.nextWithTime(1000);
314 | const ulid2 = try generator.nextWithTime(1000);
315 | const ulid3 = try generator.nextWithTime(2000);
316 |
317 | // ULIDs with same timestamp should be ordered
318 | try testing.expect(ulid1.compare(ulid2) == .lt);
319 | try testing.expect(ulid2.compare(ulid3) == .lt);
320 |
321 | // Test clock going backwards
322 | try testing.expectError(ULIDError.ClockGoingBackwards, generator.nextWithTime(1500));
323 | }
324 |
325 | test "ULID case insensitive parsing" {
326 | const ulid = ULID.new();
327 | var buffer: [ENCODING_LENGTH]u8 = undefined;
328 | const str = ulid.toString(&buffer);
329 |
330 | // Convert to lowercase
331 | var lower_str: [ENCODING_LENGTH]u8 = undefined;
332 | for (str, 0..) |c, i| {
333 | lower_str[i] = std.ascii.toLower(c);
334 | }
335 |
336 | const parsed_ulid = try ULID.fromString(&lower_str);
337 | try testing.expect(ulid.eql(parsed_ulid));
338 | }
339 |
340 | test "ULID excluded characters mapping" {
341 | // Test that excluded characters are properly mapped
342 | const test_cases = [_]struct { input: u8, expected: u8 }{
343 | .{ .input = 'I', .expected = '1' },
344 | .{ .input = 'i', .expected = '1' },
345 | .{ .input = 'L', .expected = '1' },
346 | .{ .input = 'l', .expected = '1' },
347 | .{ .input = 'O', .expected = '0' },
348 | .{ .input = 'o', .expected = '0' },
349 | };
350 |
351 | for (test_cases) |case| {
352 | const input_val = DECODING_TABLE[case.input];
353 | const expected_val = DECODING_TABLE[case.expected];
354 | try testing.expect(input_val == expected_val);
355 | }
356 | }
357 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Zig Utility Library
2 | The purpose of this library is to enhance Zig's standard library. Much of zul wraps Zig's std to provide simpler APIs for common tasks (e.g. reading lines from a file). In other cases, new functionality has been added (e.g. a UUID type).
3 |
4 | Besides Zig's standard library, there are no dependencies. Most functionality is contained within its own file and can be copy and pasted into an existing library or project.
5 |
6 | Full documentation is available at: [https://www.goblgobl.com/zul/](https://www.goblgobl.com/zul/).
7 |
8 | (This readme is auto-generated from [docs/src/readme.njk](https://github.com/karlseguin/zul/blob/master/docs/src/readme.njk))
9 |
10 | ## Usage
11 | In your build.zig.zon add a reference to Zul:
12 |
13 | ```zig
14 | .{
15 | .name = .your_app,
16 | .paths = .{""},
17 | .version = "0.0.0",
18 | .fingerprint = 0x00000000,
19 | .dependencies = .{
20 | .zul = .{
21 | .url = "https://github.com/karlseguin/zul/archive/master.tar.gz",
22 | .hash = "$INSERT_HASH_HERE"
23 | },
24 | },
25 | }
26 | ```
27 |
28 | To get the hash, run:
29 |
30 | ```bash
31 | zig fetch https://github.com/karlseguin/zul/archive/master.tar.gz
32 | ```
33 |
34 | Instead of `master` you can use a specific commit/tag.
35 |
36 | Next, in your `build.zig`, you should already have an executable, something like:
37 |
38 | ```zig
39 | const exe = b.addExecutable(.{
40 | .name = "my-app",
41 | .root_module = your_module,
42 | });
43 | ```
44 |
45 | Add the following line:
46 |
47 | ```zig
48 | exe.root_module.addImport("zul", b.dependency("zul", .{}).module("zul"));
49 | ```
50 |
51 | You can now `const zul = @import("zul");` in your project.
52 |
53 | ## [zul.benchmark.run](https://www.goblgobl.com/zul/benchmark/)
54 | Simple benchmarking function.
55 |
56 | ```zig
57 | const HAYSTACK = "abcdefghijklmnopqrstvuwxyz0123456789";
58 |
59 | pub fn main() !void {
60 | (try zul.benchmark.run(indexOfScalar, .{})).print("indexOfScalar");
61 | (try zul.benchmark.run(lastIndexOfScalar, .{})).print("lastIndexOfScalar");
62 | }
63 |
64 | fn indexOfScalar(_: Allocator, _: *std.time.Timer) !void {
65 | const i = std.mem.indexOfScalar(u8, HAYSTACK, '9').?;
66 | if (i != 35) {
67 | @panic("fail");
68 | }
69 | }
70 |
71 | fn lastIndexOfScalar(_: Allocator, _: *std.time.Timer) !void {
72 | const i = std.mem.lastIndexOfScalar(u8, HAYSTACK, 'a').?;
73 | if (i != 0) {
74 | @panic("fail");
75 | }
76 | }
77 |
78 | // indexOfScalar
79 | // 49882322 iterations 59.45ns per iterations
80 | // worst: 167ns median: 42ns stddev: 20.66ns
81 | //
82 | // lastIndexOfScalar
83 | // 20993066 iterations 142.15ns per iterations
84 | // worst: 292ns median: 125ns stddev: 23.13ns
85 | ```
86 |
87 | ## [zul.CommandLineArgs](https://www.goblgobl.com/zul/command_line_args/)
88 | A simple command line parser.
89 |
90 | ```zig
91 | var args = try zul.CommandLineArgs.parse(allocator);
92 | defer args.deinit();
93 |
94 | if (args.contains("version")) {
95 | //todo: print the version
96 | os.exit(0);
97 | }
98 |
99 | // Values retrieved from args.get are valid until args.deinit()
100 | // is called. Dupe the value if needed.
101 | const host = args.get("host") orelse "127.0.0.1";
102 | ...
103 | ```
104 |
105 | ## [zul.DateTime](https://www.goblgobl.com/zul/datetime/)
106 | Simple (no leap seconds, UTC-only), DateTime, Date and Time types.
107 |
108 | ```zig
109 | // Currently only supports RFC3339
110 | const dt = try zul.DateTime.parse("2028-11-05T23:29:10Z", .rfc3339);
111 | const next_week = try dt.add(7, .days);
112 | std.debug.assert(next_week.order(dt) == .gt);
113 |
114 | // 1857079750000 == 2028-11-05T23:29:10Z
115 | std.debug.print("{d} == {s}", .{dt.unix(.milliseconds), dt});
116 | ```
117 |
118 | ## [zul.fs.readDir](https://www.goblgobl.com/zul/fs/readdir/)
119 | Iterates, non-recursively, through a directory.
120 |
121 | ```zig
122 | // Parameters:
123 | // 1- Absolute or relative directory path
124 | var it = try zul.fs.readDir("/tmp/dir");
125 | defer it.deinit();
126 |
127 | // can iterate through the files
128 | while (try it.next()) |entry| {
129 | std.debug.print("{s} {any}\n", .{entry.name, entry.kind});
130 | }
131 |
132 | // reset the iterator
133 | it.reset();
134 |
135 | // or can collect them into a slice, optionally sorted:
136 | const sorted_entries = try it.all(allocator, .dir_first);
137 | for (sorted_entries) |entry| {
138 | std.debug.print("{s} {any}\n", .{entry.name, entry.kind});
139 | }
140 | ```
141 |
142 | ## [zul.fs.readJson](https://www.goblgobl.com/zul/fs/readjson/)
143 | Reads and parses a JSON file.
144 |
145 | ```zig
146 | // Parameters:
147 | // 1- The type to parse the JSON data into
148 | // 2- An allocator
149 | // 3- Absolute or relative path
150 | // 4- std.json.ParseOptions
151 | const managed_user = try zul.fs.readJson(User, allocator, "/tmp/data.json", .{});
152 |
153 | // readJson returns a zul.Managed(T)
154 | // managed_user.value is valid until managed_user.deinit() is called
155 | defer managed_user.deinit();
156 | const user = managed_user.value;
157 | ```
158 |
159 | ## [zul.fs.readLines](https://www.goblgobl.com/zul/fs/readlines/)
160 | Iterate over the lines in a file.
161 |
162 | ```zig
163 | // create a buffer large enough to hold the longest valid line
164 | var line_buffer: [1024]u8 = undefined;
165 |
166 | // Parameters:
167 | // 1- an absolute or relative path to the file
168 | // 2- the line buffer
169 | // 3- options (here we're using the default)
170 | var it = try zul.fs.readLines("/tmp/data.txt", &line_buffer, .{});
171 | defer it.deinit();
172 |
173 | while (try it.next()) |line| {
174 | // line is only valid until the next call to
175 | // it.next() or it.deinit()
176 | std.debug.print("line: {s}\n", .{line});
177 | }
178 | ```
179 |
180 | ## [zul.http.Client](https://www.goblgobl.com/zul/http/client/)
181 | A wrapper around std.http.Client to make it easier to create requests and consume responses.
182 |
183 | ```zig
184 | // The client is thread-safe
185 | var client = zul.http.Client.init(allocator);
186 | defer client.deinit();
187 |
188 | // Not thread safe, method defaults to .GET
189 | var req = try client.request("https://api.github.com/search/topics");
190 | defer req.deinit();
191 |
192 | // Set the querystring, can also be set in the URL passed to client.request
193 | // or a mix of setting in client.request and programmatically via req.query
194 | try req.query("q", "zig");
195 |
196 | try req.header("Authorization", "Your Token");
197 |
198 | // The lifetime of res is tied to req
199 | var res = try req.getResponse(.{});
200 | if (res.status != 200) {
201 | // TODO: handle error
202 | return;
203 | }
204 |
205 | // On success, this is a zul.Managed(SearchResult), its lifetime is detached
206 | // from the req, allowing it to outlive req.
207 | const managed = try res.json(SearchResult, allocator, .{});
208 |
209 | // Parsing the JSON and creating SearchResult [probably] required some allocations.
210 | // Internally an arena was created to manage this from the allocator passed to
211 | // res.json.
212 | defer managed.deinit();
213 |
214 | const search_result = managed.value;
215 | ```
216 |
217 | ## [zul.JsonString](https://www.goblgobl.com/zul/json_string/)
218 | Allows the embedding of already-encoded JSON strings into objects in order to avoid double encoded values.
219 |
220 | ```zig
221 | const an_encoded_json_value = "{\"over\": 9000}";
222 | const str = try std.json.stringifyAlloc(allocator, .{
223 | .name = "goku",
224 | .power = zul.jsonString(an_encoded_json_value),
225 | }, .{});
226 | ```
227 |
228 | ## [zul.pool](https://www.goblgobl.com/zul/pool/)
229 | A thread-safe object pool which will dynamically grow when empty and revert to the configured size.
230 |
231 | ```zig
232 | // create a pool for our Expensive class.
233 | // Our Expensive class takes a special initializing context, here an usize which
234 | // we set to 10_000. This is just to pass data from the pool into Expensive.init
235 | var pool = try zul.pool.Growing(Expensive, usize).init(allocator, 10_000, .{.count = 100});
236 | defer pool.deinit();
237 |
238 | // acquire will either pick an item from the pool
239 | // if the pool is empty, it'll create a new one (hence, "Growing")
240 | var exp1 = try pool.acquire();
241 | defer pool.release(exp1);
242 |
243 | ...
244 |
245 | // pooled object must have 3 functions
246 | const Expensive = struct {
247 | // an init function
248 | pub fn init(allocator: Allocator, size: usize) !Expensive {
249 | return .{
250 | // ...
251 | };
252 | }
253 |
254 | // a deinit method
255 | pub fn deinit(self: *Expensive) void {
256 | // ...
257 | }
258 |
259 | // a reset method, called when the item is released back to the pool
260 | pub fn reset(self: *Expensive) void {
261 | // ...
262 | }
263 | };
264 | ```
265 |
266 | ## [zul.Scheduler](https://www.goblgobl.com/zul/scheduler/)
267 | Ephemeral thread-based task scheduler used to run tasks at a specific time.
268 |
269 | ```zig
270 | // Where multiple types of tasks can be scheduled using the same schedule,
271 | // a tagged union is ideal.
272 | const Task = union(enum) {
273 | say: []const u8,
274 |
275 | // Whether T is a tagged union (as here) or another type, a public
276 | // run function must exist
277 | pub fn run(task: Task, ctx: void, at: i64) void {
278 | // the original time the task was scheduled for
279 | _ = at;
280 |
281 | // application-specific context that will be passed to each task
282 | _ ctx;
283 |
284 | switch (task) {
285 | .say => |msg| {std.debug.print("{s}\n", .{msg}),
286 | }
287 | }
288 | }
289 |
290 | ...
291 |
292 | // This example doesn't use a app-context, so we specify its
293 | // type as void
294 | var s = zul.Scheduler(Task, void).init(allocator);
295 | defer s.deinit();
296 |
297 | // Starts the scheduler, launching a new thread
298 | // We pass our context. Since we have a null context
299 | // we pass a null value, i.e. {}
300 | try s.start({});
301 |
302 | // will run the say task in 5 seconds
303 | try s.scheduleIn(.{.say = "world"}, std.time.ms_per_s * 5);
304 |
305 | // will run the say task in 100 milliseconds
306 | try s.schedule(.{.say = "hello"}, std.time.milliTimestamp() + 100);
307 | ```
308 |
309 | ## [zul.sort](https://www.goblgobl.com/zul/sort/)
310 | Helpers for sorting strings and integers
311 |
312 | ```zig
313 | // sorting strings based on their bytes
314 | var values = [_][]const u8{"ABC", "abc", "Dog", "Cat", "horse", "chicken"};
315 | zul.sort.strings(&values, .asc);
316 |
317 | // sort ASCII strings, ignoring case
318 | zul.sort.asciiIgnoreCase(&values, .desc);
319 |
320 | // sort integers or floats
321 | var numbers = [_]i32{10, -20, 33, 0, 2, 6};
322 | zul.sort.numbers(i32, &numbers, .asc);
323 | ```
324 |
325 | ## [zul.StringBuilder](https://www.goblgobl.com/zul/string_builder/)
326 | Efficiently create/concat strings or binary data, optionally using a thread-safe pool with pre-allocated static buffers.
327 |
328 | ```zig
329 | // StringBuilder can be used to efficiently concatenate strings
330 | // But it can also be used to craft binary payloads.
331 | var sb = zul.StringBuilder.init(allocator);
332 | defer sb.deinit();
333 |
334 | // We're going to generate a 4-byte length-prefixed message.
335 | // We don't know the length yet, so we'll skip 4 bytes
336 | // We get back a "view" which will let us backfill the length
337 | var view = try sb.skip(4);
338 |
339 | // Writes a single byte
340 | try sb.writeByte(10);
341 |
342 | // Writes a []const u8
343 | try sb.write("hello");
344 |
345 | // Using our view, which points to where the view was taken,
346 | // fill in the length.
347 | view.writeU32Big(@intCast(sb.len() - 4));
348 |
349 | std.debug.print("{any}\n", .{sb.string()});
350 | // []u8{0, 0, 0, 6, 10, 'h', 'e', 'l', 'l', 'o'}
351 | ```
352 |
353 | ## [zul.testing](https://www.goblgobl.com/zul/testing/)
354 | Helpers for writing tests.
355 |
356 | ```zig
357 | const t = zul.testing;
358 |
359 | test "memcpy" {
360 | // clear's the arena allocator
361 | defer t.reset();
362 |
363 | // In addition to exposing std.testing.allocator as zul.testing.allocator
364 | // zul.testing.arena is an ArenaAllocator. An ArenaAllocator can
365 | // make managing test-specific allocations a lot simpler.
366 | // Just stick a `defer zul.testing.reset()` atop your test.
367 | var buf = try t.arena.allocator().alloc(u8, 5);
368 |
369 | // unlike std.testing.expectEqual, zul's expectEqual
370 | // will coerce expected to actual's type, so this is valid:
371 | try t.expectEqual(5, buf.len);
372 |
373 | @memcpy(buf[0..5], "hello");
374 |
375 | // zul's expectEqual also works with strings.
376 | try t.expectEqual("hello", buf);
377 | }
378 | ```
379 |
380 | ## [zul.ThreadPool](https://www.goblgobl.com/zul/thread_pool/)
381 | Lightweight thread pool with back-pressure and zero allocations after initialization.
382 |
383 | ```zig
384 | var tp = try zul.ThreadPool(someTask).init(allocator, .{.count = 4, .backlog = 500});
385 | defer tp.deinit(allocator);
386 |
387 | // This will block if the threadpool has 500 pending jobs
388 | // where 500 is the configured backlog
389 | tp.spawn(.{1, true});
390 |
391 |
392 | fn someTask(i: i32, allow: bool) void {
393 | // process
394 | }
395 | ```
396 |
397 | ## [zul.UUID](https://www.goblgobl.com/zul/uuid/)
398 | Parse and generate version 4 and version 7 UUIDs.
399 |
400 | ```zig
401 | // v4() returns a zul.UUID
402 | const uuid1 = zul.UUID.v4();
403 |
404 | // toHex() returns a [36]u8
405 | const hex = uuid1.toHex(.lower);
406 |
407 | // returns a zul.UUID (or an error)
408 | const uuid2 = try zul.UUID.parse("761e3a9d-4f92-4e0d-9d67-054425c2b5c3");
409 | std.debug.print("{any}\n", uuid1.eql(uuid2));
410 |
411 | // create a UUIDv7
412 | const uuid3 = zul.UUID.v7();
413 |
414 | // zul.UUID can be JSON serialized
415 | try std.json.stringify(.{.id = uuid3}, .{}, writer);
416 | ```
417 |
418 |
419 |
--------------------------------------------------------------------------------
/docs/src/http/client.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: site.njk
3 | title: zul.http.Client
4 | ---
5 |
6 |
7 | A wrapper around std.http.Client to make it easier to create requests and consume responses.
8 |
9 |
10 | {% highlight zig %}
11 | // The client is thread-safe
12 | var client = zul.http.Client.init(allocator);
13 | defer client.deinit();
14 |
15 | // Not thread safe, method defaults to .GET
16 | var req = try client.request("https://api.github.com/search/topics");
17 | defer req.deinit();
18 |
19 | // Set the querystring, can also be set in the URL passed to client.request
20 | // or a mix of setting in client.request and programmatically via req.query
21 | try req.query("q", "zig");
22 |
23 | try req.header("Authorization", "Your Token");
24 |
25 | // The lifetime of res is tied to req
26 | var res = try req.getResponse(.{});
27 | if (res.status != 200) {
28 | // TODO: handle error
29 | return;
30 | }
31 |
32 | // On success, this is a zul.Managed(SearchResult), its lifetime is detached
33 | // from the req, allowing it to outlive req.
34 | const managed = try res.json(SearchResult, allocator, .{});
35 |
36 | // Parsing the JSON and creating SearchResult [probably] required some allocations.
37 | // Internally an arena was created to manage this from the allocator passed to
38 | // res.json.
39 | defer managed.deinit();
40 |
41 | const search_result = managed.value;
42 | {% endhighlight %}
43 |
44 |
45 | zul.http.Client is wrapper around std.http.Client. Is is thread-safe and its only purpose is to create zul.http.Request objects.
46 |
47 |
48 |
49 |
50 |
client: std.http.Client
51 |
52 |
Should only be used if tweaks to the underlying std.http.Client are needed.
53 |
54 |
55 |
56 |
57 |
58 |
59 |
65 |
66 |
deinit(self: *Client) void
67 |
68 |
Releases all memory associated with the client. The client should not be used after this is called.
69 |
70 |
71 |
77 |
78 |
fn allocRequest(...) !Request
79 | {% highlight zig %}
80 | fn allocRequest(
81 | self: *Client,
82 |
83 | // With the plain request() method, the client's allocator is used. A different
84 | // allocator can be used with this variant.
85 | allocator: Allocator,
86 |
87 | url: []const u8,
88 |
89 | ) !Request
90 | {% endhighlight %}
91 |
92 |
Creates a Request object, using the provided url. The Request will use the provided Allocator.
93 |
94 |
95 |
96 |
97 |
98 | zul.http.Request is used to build the request (querystring, headers, body) and issue the request to get a Response . A Request is not thread-safe. To get a request, use the client.request method.
99 |
100 |
101 |
102 |
103 |
headers: std.http.Headers
104 |
105 |
Gives direct access to the request headers. To add headers, prefer using the header method.
106 |
107 |
108 |
109 |
method: std.http.Method
110 |
111 |
Defaults to .GET.
112 |
113 |
114 |
115 |
url: zul.StringBuilder
116 |
117 |
Gives direct access to the URL. For manipulating the querystring, prefer using the query method.
118 |
119 |
120 |
121 |
122 |
123 |
124 |
130 |
136 |
142 |
143 |
144 |
145 |
Adds the given name-value pair to the request headers.
146 |
147 |
148 |
149 |
query(self: *Request, name: []const u8, value: []const u8) !void
150 |
151 |
Appends the given name-value pair to the request's querystring. Both the name and value will be automatically encoded if needed. The querystring can also be set when first creating the request as part of the specified URL. It is allowed to set some querystring values via the original URLs (which must be encoded by the caller) and others via this method.
152 |
153 |
154 |
160 |
161 |
getResponse(...) !Response
162 | {% highlight zig %}
163 | fn getResponse(
164 | req: *Request,
165 |
166 | opts: .{
167 | // whether or not to parse the respons headers
168 | .response_headers: bool = true,
169 |
170 | .write_progress_state: *anyopaque = undefined,
171 |
172 | .write_progress: ?*const fn(total: usize, written: usize, state: *anyopaque) void = null,
173 | }
174 | ) !Response
175 | {% endhighlight %}
176 |
177 |
Issues the requests and, on success, returns the Response .
178 |
179 |
The write_progress option field is a callback that will be called as the file body is uploaded. An optional state can be specified via the write_progress_state the option field which is passed into the callback.
180 |
181 | {% highlight zig %}
182 | var res = try req.getResponse(.{
183 | .write_progress = uploadProgress
184 | });
185 |
186 | // ...
187 |
188 | fn uploadProgress(total: usize, written: usize, state: *anyopaque) void {
189 | // It is an undefined behavior to try to access the state
190 | // when `write_progress_state` was not specified.
191 | _ = state;
192 |
193 | std.fmt.print("Written {d} of {d}", {written, total});
194 | }
195 | {% endhighlight %}
196 |
197 |
Or, with state:
198 |
199 | {% highlight zig %}
200 | // ProgressTracker can be anything, it's specific to your app
201 | var tracker = ProgressTracker{};
202 |
203 | var res = try req.getResponse(.{
204 | .write_progress = uploadProgress
205 | .write_progress_state = &tracker,
206 | });
207 |
208 | // ...
209 |
210 | fn uploadProgress(total: usize, written: usize, state: *anyopaque) void {
211 | var tracker: *ProgressTracker = @alignCast(@ptrCast(state));
212 | // use tracker however you want, it's your class!
213 | }
214 | {% endhighlight %}
215 |
216 |
217 |
218 |
219 |
220 | zul.http.Response lifetime is tied to the initiating request. Therefore, it has no deinit method. When request.deinit is called, the response is no longer valid. Note however that the methods for reading the body detach the body from this lifetime.
221 |
222 |
223 |
224 |
225 |
headers: std.StringHashMap([]const u8)
226 |
227 |
The response headrs. Only populated if the response_headers option is specified in getResponse (this option defaults to true).
228 |
229 |
230 |
231 |
req: *std.http.Client.Request
232 |
233 |
The underlying request.
234 |
235 |
236 |
237 |
res: *std.http.Client.Response
238 |
239 |
The underlying response.
240 |
241 |
242 |
243 |
status: u16
244 |
245 |
The response's HTTP status code.
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
Returns the value associated with the given header name, if any. The name is lower-case. Only populated if the response_headers option is specified in getResponse (this option defaults to true).
256 |
257 |
258 |
259 |
260 |
261 |
Returns an iterator to iterate over values of a given header. Useful for headers which may appear multiple times (i.e. set-cookie).
262 |
263 | {% highlight zig %}
264 | var it = res.headerIterator("set-cookie");
265 | while (it.next()) |value| {
266 | // ... value
267 | }
268 | {% endhighlight %}
269 |
To iterate over all values, use the headerIterator of the underlying *std.http.Client.Response, i.e.: var it = res.res.headerIterator()
270 |
271 |
272 |
273 |
fn json(...) !zul.Managed(T)
274 | {% highlight zig %}
275 | fn json(
276 | self: Response,
277 |
278 | // The type to parse into
279 | comptime T: type,
280 |
281 | // An arena allocator will be created from this allocator for any memory needed
282 | // to parse the JSON and create T
283 | allocator: std.mem.Allocator,
284 |
285 | // Consider setting ignore_unknown_fields = true
286 | // and the max_value_len
287 | opts: std.json.ParseOptions
288 |
289 | ) !zul.Managed(T)
290 | {% endhighlight %}
291 |
292 |
Attempts to parse the body as JSON. On success, the returned object has its own lifetime, independent of the initiating request or response.
293 |
294 |
zul.Manage is a renamed std.json.Parsed(T) (I dislike the name std.json.Parsed(T) because it represents data and behavior that has nothing to with with JSON or parsing).
295 |
296 |
297 |
298 |
fn allocBody(...) !zul.StringBuilder
299 | {% highlight zig %}
300 | fn allocBody (
301 | self: *Response,
302 |
303 | // Allocator will be used to create the []const u8 that will hold the body
304 | // If the response has a content-length, then exactly $content_length bytes will
305 | // be allocated
306 | allocator: std.mem.Allocator,
307 |
308 | // {.max_size = usize, .buffer_size: usize}
309 | opts: zul.StringBuilder.FromReaderOpts,
310 |
311 | ) !zul.StringBuilder
312 | {% endhighlight %}
313 |
314 |
Reads the body into a zul.StringBuilder. Consider setting the max_size field of opts to a reasonable value.
315 |
316 |
This method returns a zul.StringBuilder to support chunked-encoding responses where the length isn't known ahead of time and a growable buffer is needed. In such cases, a correctly sized []const u8 cannot be returned without doing an additional copy. zul.StringBuilder is preferred over std.ArrayList(u8) because of its more efficient ability to read from a std.Io.Reader.
317 |
318 |
319 |
320 |
--------------------------------------------------------------------------------
/src/context.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const testing = std.testing;
3 | const time = std.time;
4 | const Allocator = std.mem.Allocator;
5 |
6 | /// Error types for context operations
7 | pub const ContextError = error{
8 | DeadlineExceeded,
9 | Cancelled,
10 | InvalidDeadline,
11 | };
12 |
13 | /// Context provides deadline-based cancellation and timeout functionality
14 | pub const Context = struct {
15 | allocator: Allocator,
16 | deadline_ns: ?i128, // Nanoseconds since epoch, null means no deadline
17 | cancelled: bool,
18 | parent: ?*const Context,
19 | children: std.ArrayListUnmanaged(*Context),
20 | mutex: std.Thread.Mutex,
21 |
22 | /// Create a new root context with optional deadline
23 | pub fn init(allocator: Allocator, deadline_ns: ?i128) Context {
24 | return Context{
25 | .allocator = allocator,
26 | .deadline_ns = deadline_ns,
27 | .cancelled = false,
28 | .parent = null,
29 | .children = std.ArrayListUnmanaged(*Context){},
30 | .mutex = std.Thread.Mutex{},
31 | };
32 | }
33 |
34 | /// Create a background context (no deadline, never cancelled)
35 | pub fn background(allocator: Allocator) Context {
36 | return init(allocator, null);
37 | }
38 |
39 | /// Create a context with deadline from duration in milliseconds
40 | pub fn withTimeout(allocator: Allocator, timeout_ms: u64) !Context {
41 | const now_ns = time.nanoTimestamp();
42 | const deadline_ns = now_ns + (@as(i128, timeout_ms) * time.ns_per_ms);
43 | return init(allocator, deadline_ns);
44 | }
45 |
46 | /// Create a context with absolute deadline
47 | pub fn withDeadline(allocator: Allocator, deadline_ns: i128) !Context {
48 | const now_ns = time.nanoTimestamp();
49 | if (deadline_ns <= now_ns) {
50 | return ContextError.InvalidDeadline;
51 | }
52 | return init(allocator, deadline_ns);
53 | }
54 |
55 | /// Create a child context from parent with optional new deadline
56 | pub fn withParent(parent: *Context, deadline_ns: ?i128) !*Context {
57 | const child = try parent.allocator.create(Context);
58 | child.* = Context{
59 | .allocator = parent.allocator,
60 | .deadline_ns = if (deadline_ns) |d| blk: {
61 | if (parent.deadline_ns) |parent_deadline| {
62 | break :blk @min(d, parent_deadline);
63 | } else {
64 | break :blk d;
65 | }
66 | } else parent.deadline_ns,
67 | .cancelled = false,
68 | .parent = parent,
69 | .children = std.ArrayListUnmanaged(*Context){},
70 | .mutex = std.Thread.Mutex{},
71 | };
72 |
73 | // Add to parent's children list (this would need proper synchronization in real use)
74 | parent.mutex.lock();
75 | defer parent.mutex.unlock();
76 |
77 | try parent.children.append(parent.allocator, child);
78 |
79 | return child;
80 | }
81 |
82 | /// Clean up context resources
83 | pub fn deinit(self: *Context) void {
84 | self.mutex.lock();
85 | defer self.mutex.unlock();
86 |
87 | while (self.children.items.len > 0) {
88 | const child = self.children.pop().?;
89 | child.parent = null;
90 | child.deinit();
91 | self.allocator.destroy(child);
92 | }
93 | self.children.deinit(self.allocator);
94 | }
95 |
96 | /// Internal const check for cancellation (no mutex, for parent checking)
97 | fn isCancelledInternal(self: *const Context) bool {
98 | // Check if explicitly cancelled
99 | if (self.cancelled) return true;
100 |
101 | // Check if parent is cancelled
102 | if (self.parent) |parent| {
103 | if (parent.isCancelledInternal()) return true;
104 | }
105 |
106 | return false;
107 | }
108 |
109 | /// Check if context is cancelled
110 | pub fn isCancelled(self: *Context) bool {
111 | self.mutex.lock();
112 | defer self.mutex.unlock();
113 |
114 | return self.isCancelledInternal();
115 | }
116 |
117 | /// Check if deadline has been exceeded
118 | pub fn isExpired(self: *Context) bool {
119 | if (self.deadline_ns) |deadline| {
120 | return time.nanoTimestamp() >= deadline;
121 | }
122 | return false;
123 | }
124 |
125 | /// Check if context is done (cancelled or expired)
126 | pub fn isDone(self: *Context) bool {
127 | return self.isCancelled() or self.isExpired();
128 | }
129 |
130 | /// Get the error reason for why context is done
131 | pub fn err(self: *Context) ?ContextError {
132 | if (self.isCancelled()) return ContextError.Cancelled;
133 | if (self.isExpired()) return ContextError.DeadlineExceeded;
134 | return null;
135 | }
136 |
137 | /// Cancel the context
138 | pub fn cancel(self: *Context) !void {
139 | self.mutex.lock();
140 | defer self.mutex.unlock();
141 |
142 | if (self.cancelled) return; // Already cancelled
143 |
144 | self.cancelled = true;
145 |
146 | // Cancel all children
147 | for (self.children.items) |child| {
148 | try child.cancel();
149 | }
150 | }
151 |
152 | /// Get remaining time until deadline in nanoseconds
153 | pub fn remainingTime(self: *Context) ?i128 {
154 | if (self.deadline_ns) |deadline| {
155 | const now = time.nanoTimestamp();
156 | const remaining = deadline - now;
157 | return if (remaining > 0) remaining else 0;
158 | }
159 | return null;
160 | }
161 |
162 | /// Sleep with context cancellation check
163 | pub fn sleep(self: *Context, duration_ns: u64) ContextError!void {
164 | const start = time.nanoTimestamp();
165 | const end_time = start + @as(i128, duration_ns);
166 |
167 | while (time.nanoTimestamp() < end_time) {
168 | if (self.isDone()) {
169 | return self.err() orelse ContextError.Cancelled;
170 | }
171 | time.sleep(time.ns_per_ms); // Sleep 1ms between checks
172 | }
173 | }
174 |
175 | /// Wait for context to be done with optional timeout
176 | pub fn wait(self: *Context, timeout_ns: ?u64) ContextError!void {
177 | const start = time.nanoTimestamp();
178 | const timeout_deadline = if (timeout_ns) |t| start + @as(i128, t) else null;
179 |
180 | while (!self.isDone()) {
181 | if (timeout_deadline) |deadline| {
182 | if (time.nanoTimestamp() >= deadline) {
183 | return ContextError.DeadlineExceeded;
184 | }
185 | }
186 | time.sleep(time.ns_per_ms); // Sleep 1ms between checks
187 | }
188 |
189 | return self.err() orelse ContextError.Cancelled;
190 | }
191 | };
192 |
193 | test "Context: basic creation and background context" {
194 | const allocator = std.testing.allocator;
195 |
196 | var ctx = Context.background(allocator);
197 | defer ctx.deinit();
198 |
199 | try testing.expect(!ctx.isCancelled());
200 | try testing.expect(!ctx.isExpired());
201 | try testing.expect(!ctx.isDone());
202 | try testing.expect(ctx.err() == null);
203 | try testing.expect(ctx.deadline_ns == null);
204 | try testing.expect(ctx.remainingTime() == null);
205 | }
206 |
207 | test "Context: with timeout creation" {
208 | const allocator = std.testing.allocator;
209 |
210 | var ctx = try Context.withTimeout(allocator, 1000); // 1 second
211 | defer ctx.deinit();
212 |
213 | try testing.expect(!ctx.isCancelled());
214 | try testing.expect(!ctx.isExpired());
215 | try testing.expect(!ctx.isDone());
216 | try testing.expect(ctx.deadline_ns != null);
217 |
218 | const remaining = ctx.remainingTime();
219 | try testing.expect(remaining != null);
220 | try testing.expect(remaining.? > 0);
221 | try testing.expect(remaining.? <= 1000 * time.ns_per_ms);
222 | }
223 |
224 | test "Context: with deadline creation" {
225 | const allocator = std.testing.allocator;
226 |
227 | const future_deadline = time.nanoTimestamp() + (5000 * time.ns_per_ms); // 5 seconds from now
228 | var ctx = try Context.withDeadline(allocator, future_deadline);
229 | defer ctx.deinit();
230 |
231 | try testing.expect(!ctx.isCancelled());
232 | try testing.expect(!ctx.isExpired());
233 | try testing.expect(!ctx.isDone());
234 | try testing.expect(ctx.deadline_ns.? == future_deadline);
235 | }
236 |
237 | test "Context: invalid deadline" {
238 | const allocator = std.testing.allocator;
239 |
240 | const past_deadline = time.nanoTimestamp() - (1000 * time.ns_per_ms); // 1 second ago
241 | const result = Context.withDeadline(allocator, past_deadline);
242 | try testing.expectError(ContextError.InvalidDeadline, result);
243 | }
244 |
245 | test "Context: manual cancellation" {
246 | const allocator = std.testing.allocator;
247 |
248 | var ctx = Context.background(allocator);
249 | defer ctx.deinit();
250 |
251 | try testing.expect(!ctx.isDone());
252 |
253 | try ctx.cancel();
254 |
255 | try testing.expect(ctx.isCancelled());
256 | try testing.expect(ctx.isDone());
257 | try testing.expectEqual(ContextError.Cancelled, ctx.err().?);
258 | }
259 |
260 | test "Context: deadline expiration" {
261 | const allocator = std.testing.allocator;
262 |
263 | // Create context with very short timeout
264 | var ctx = try Context.withTimeout(allocator, 1); // 1ms
265 | defer ctx.deinit();
266 |
267 | // Wait for expiration
268 | time.sleep(5 * time.ns_per_ms); // Sleep 5ms
269 |
270 | try testing.expect(ctx.isExpired());
271 | try testing.expect(ctx.isDone());
272 | try testing.expectEqual(ContextError.DeadlineExceeded, ctx.err().?);
273 | }
274 |
275 | test "Context: parent-child relationship" {
276 | const allocator = std.testing.allocator;
277 |
278 | var parent = Context.background(allocator);
279 | defer parent.deinit();
280 |
281 | const future_deadline = time.nanoTimestamp() + (1000 * time.ns_per_ms);
282 | var child = try Context.withParent(&parent, future_deadline);
283 |
284 | try testing.expect(!child.isDone());
285 | try testing.expect(child.deadline_ns != null);
286 |
287 | // Cancel parent should affect child
288 | try parent.cancel();
289 | try testing.expect(child.isCancelled());
290 | try testing.expect(child.isDone());
291 | }
292 |
293 | test "Context: child inherits parent deadline" {
294 | const allocator = std.testing.allocator;
295 |
296 | const parent_deadline = time.nanoTimestamp() + (1000 * time.ns_per_ms);
297 | var parent = try Context.withDeadline(allocator, parent_deadline);
298 | defer parent.deinit();
299 |
300 | const child_deadline = time.nanoTimestamp() + (2000 * time.ns_per_ms); // Later than parent
301 | const child = try Context.withParent(&parent, child_deadline);
302 |
303 | // Child should inherit parent's earlier deadline
304 | try testing.expectEqual(parent_deadline, child.deadline_ns.?);
305 | }
306 |
307 | test "Context: sleep with cancellation" {
308 | const allocator = std.testing.allocator;
309 |
310 | var ctx = Context.background(allocator);
311 | defer ctx.deinit();
312 |
313 | // Cancel context in separate thread after delay
314 | const thread = try std.Thread.spawn(.{}, struct {
315 | fn cancelAfterDelay(context: *Context) void {
316 | time.sleep(10 * time.ns_per_ms); // 10ms delay
317 | context.cancel() catch {};
318 | }
319 | }.cancelAfterDelay, .{&ctx});
320 | defer thread.join();
321 |
322 | const start = time.nanoTimestamp();
323 | const result = ctx.sleep(100 * time.ns_per_ms); // Try to sleep 100ms
324 | const elapsed = time.nanoTimestamp() - start;
325 |
326 | try testing.expectError(ContextError.Cancelled, result);
327 | // Should have been cancelled before full sleep duration
328 | try testing.expect(elapsed < 50 * time.ns_per_ms);
329 | }
330 |
331 | test "Context: sleep with deadline" {
332 | const allocator = std.testing.allocator;
333 |
334 | var ctx = try Context.withTimeout(allocator, 10); // 10ms timeout
335 | defer ctx.deinit();
336 |
337 | const start = time.nanoTimestamp();
338 | const result = ctx.sleep(100 * time.ns_per_ms); // Try to sleep 100ms
339 | const elapsed = time.nanoTimestamp() - start;
340 |
341 | try testing.expectError(ContextError.DeadlineExceeded, result);
342 | // Should have been cancelled by deadline
343 | try testing.expect(elapsed >= 10 * time.ns_per_ms);
344 | try testing.expect(elapsed < 50 * time.ns_per_ms);
345 | }
346 |
347 | test "Context: remaining time calculation" {
348 | const allocator = std.testing.allocator;
349 |
350 | var ctx = try Context.withTimeout(allocator, 100); // 100ms
351 | defer ctx.deinit();
352 |
353 | const remaining1 = ctx.remainingTime();
354 | try testing.expect(remaining1 != null);
355 | try testing.expect(remaining1.? > 0);
356 |
357 | time.sleep(50 * time.ns_per_ms); // Sleep 50ms
358 |
359 | const remaining2 = ctx.remainingTime();
360 | try testing.expect(remaining2 != null);
361 | try testing.expect(remaining2.? < remaining1.?);
362 | }
363 |
364 | test "Context: wait for cancellation" {
365 | const allocator = std.testing.allocator;
366 |
367 | var ctx = Context.background(allocator);
368 | defer ctx.deinit();
369 |
370 | // Cancel after delay
371 | const thread = try std.Thread.spawn(.{}, struct {
372 | fn cancelAfterDelay(context: *Context) void {
373 | time.sleep(20 * time.ns_per_ms);
374 | context.cancel() catch {};
375 | }
376 | }.cancelAfterDelay, .{&ctx});
377 | defer thread.join();
378 |
379 | const result = ctx.wait(100 * time.ns_per_ms); // Wait up to 100ms
380 | try testing.expectError(ContextError.Cancelled, result);
381 | }
382 |
383 | test "Context: wait timeout" {
384 | const allocator = std.testing.allocator;
385 |
386 | var ctx = Context.background(allocator);
387 | defer ctx.deinit();
388 |
389 | const start = time.nanoTimestamp();
390 | const result = ctx.wait(10 * time.ns_per_ms); // Wait 10ms max
391 | const elapsed = time.nanoTimestamp() - start;
392 |
393 | try testing.expectError(ContextError.DeadlineExceeded, result);
394 | try testing.expect(elapsed >= 10 * time.ns_per_ms);
395 | }
396 |
397 | test "Context: nested cancellation" {
398 | const allocator = std.testing.allocator;
399 |
400 | var parent = Context.background(allocator);
401 | defer parent.deinit();
402 |
403 | var child = try Context.withParent(&parent, null);
404 |
405 | try testing.expect(!child.isCancelled());
406 | try testing.expect(!child.isDone());
407 |
408 | // Cancel parent
409 | try parent.cancel();
410 |
411 | try testing.expect(child.isCancelled());
412 | try testing.expect(child.isDone());
413 | }
414 |
--------------------------------------------------------------------------------