320 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/string_builder.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: site.njk
3 | title: zul.StringBuilder
4 | ---
5 |
6 |
7 |
Efficiently create/concat strings or binary data, optionally using a thread-safe pool with pre-allocated static buffers.
8 |
9 |
Applications that need to create many StringBuilders
should consider using a zul.StringBuilder.Pool
. StringBuilders
created via the pool have a re-usable static buffer which can eliminate or reduce the need for dynamic memory allocations (assume the size of the static buffer is larger than the typical data written).
10 |
11 |
12 | {% highlight zig %}
13 | // StringBuilder can be used to efficiently concatenate strings
14 | // But it can also be used to craft binary payloads.
15 | var sb = zul.StringBuilder.init(allocator);
16 | defer sb.deinit();
17 |
18 | // We're going to generate a 4-byte length-prefixed message.
19 | // We don't know the length yet, so we'll skip 4 bytes
20 | // We get back a "view" which will let us backfill the length
21 | var view = try sb.skip(4);
22 |
23 | // Writes a single byte
24 | try sb.writeByte(10);
25 |
26 | // Writes a []const u8
27 | try sb.write("hello");
28 |
29 | // Using our view, which points to where the view was taken,
30 | // fill in the length.
31 | view.writeU32Big(@intCast(sb.len() - 4));
32 |
33 | std.debug.print("{any}\n", .{sb.string()});
34 | // []u8{0, 0, 0, 6, 10, 'h', 'e', 'l', 'l', 'o'}
35 | {% endhighlight %}
36 |
37 |
38 |
zul.StringBuilder
is a wrapper around a dynamically growable []u8
and is designed for string concatenation or creating binary payloads. While std.ArrayList(u8)
can be used for the same purpose, the non-generic nature of StringBuilder
results in a cleaner interface.
39 |
40 |
41 |
42 |
43 |
endian: std.builtin.Endian
44 |
45 |
The endianness to use, .big
or .little
, when encoding integers. Defaults to the current architecture.
46 |
47 |
48 |
49 |
50 |
51 |
52 |
58 |
64 |
70 |
76 |
77 |
ensureUnusedCapacity(sb: *StringBuilder, n: usize) !void
78 |
79 |
Ensures that the the underlying buffer has n
spare bytes. If not, tries to allocates enough memory to allocate n
additional bytes.
80 |
This can be used to pre-grow the buffer prior to making a large number of writes.
81 |
82 |
83 |
84 |
fromOwnedSlice(...) StringBuilder
85 | {% highlight zig %}
86 | fn fromOwnedSlice(
87 | // When deinit is called on the returned StringBuilder, buf will be freed
88 | // with this allocator. Thus, buf must have been created with this
89 | // allocator.
90 | allocator: std.mem.Allocator,
91 |
92 | // The position of the returned StringBuilder will be set to buf.len (thus
93 | // any subsequent writes will be appended)
94 | buf: []u8,
95 |
96 | ) StringBuilder
97 | {% endhighlight %}
98 |
99 |
100 |
Creates a StringBuilder around the provided buf
. buf
must have been created by the provided allocator
.
101 |
102 |
103 |
104 |
fromReader(...) !StringBuilder
105 | {% highlight zig %}
106 | fn fromReader(
107 | allocator: std.mem.Allocator,
108 |
109 | // Typically an std.io.Reader, but can be any type that has a read([]u8) !size method
110 | reader: anytype,
111 |
112 | // {.max_size = usize, .buffer_size = usize}
113 | opts: FromReaderOpts
114 |
115 | ) !StringBuilder
116 | {% endhighlight %}
117 |
118 |
Creates and populates a StringBuilder
by reading from the reader
.
119 |
120 |
Options:
121 |
122 | max_size: usize
- Will return an error.TooBig
if more than the specified value is read. Default: std.math.maxInt(usize)
.
123 | buffer_size: usize
- For the sake of efficiency, the reader is read directly into the StringBuilder's underlying buffer (there is no intermediate buffer). `buffer_size` controls the (a) initial size and (b) growth of the underlying buffer. Defaults to `8192`. Is forced to 64
when the value is less than 64
124 |
125 |
126 |
127 |
133 |
134 |
release(self: *StringBuilder) void
135 |
136 |
Releases the StringBuilder
back to the pool. This method is only valid when the StringBuilder
was retrieved from pool.acquire()
. The StringBuilder
should not be used after this is called.
137 |
138 |
139 |
140 |
skip(sb: *StringBuilder, count: usize) !View
141 |
142 |
Skips the specified number of bytes and returns a View
that can be used to backfill the skipped data.
143 |
count
can be larger than the existing buffer length, in which case skip
will attempt to grow the buffer.
144 |
145 |
146 |
147 |
string(sb: StringBuilder) []u8
148 |
149 |
Returns the bytes written. Subsequent modifications to the StringBuilder
may invalidate the returned data
150 |
151 |
152 |
153 |
stringZ(sb: StringBuilder) ![:0]u8
154 |
155 |
Returns the bytes written as a null-terminated string. Subsequent modifications to the StringBuilder
may invalidate the returned data
156 |
157 |
158 |
164 |
170 |
176 |
182 |
188 |
194 |
195 |
writer(sb: *StringBuilder) std.io.Writer
196 |
197 |
Returns an std.io.Writer
. This allows the StringBuilder
to receive writes from a number of std
and third party libraries, such as std.json.stringify
.
198 |
199 |
200 |
201 |
202 |
203 |
The StringBuilder
exposes a variety of methods for writing integers. There's overlap between some of these functions in order to accommodate various styles/preferences.
204 |
205 |
206 |
213 |
220 |
227 |
234 |
241 |
248 |
249 |
writeInt(sb: *StringBuilder, value anytype) !void
250 |
251 |
Writes an integer using the encoding defined by sb.endian
. Type type of value
determines how the value is encoded.
252 |
It is a compile-time error to pass a comptime_int
. You must cast the constant.
253 |
254 |
255 |
262 |
263 |
264 |
265 |
When the sb.skip()
method is called, a View
is returned to backfill the skipped space. This is API exists to "reserve" space within the buffer for data unknown until a later point. The typical example is a length-prefixed message where the length is only known after the message has been written.
266 |
267 |
The View
exposes all of the same write*
methods as the StringBuilder
, but will not grow the buffer and does no bound-checking.
268 |
269 |
270 |
zul.StringBuilder.Pool
is a thread-safe pool of zul.StringBuilders
that will grow as needed. Each zul.StringBuilder
has a re-usable static buffer which will be used before any dynamic allocations are needed.
271 |
272 | {% highlight zig %}
273 | // create a pool of 10 StringBuilders
274 | // with each StringBuilder having a re-usable static buffer of 1024 bytes.
275 | // pool is thread-safe
276 | var pool = zul.StringBuilder.Pool.init(t.allocator, 10, 1024);
277 | defer pool.deinit();
278 |
279 | var sb = try pool.acquire();
280 | defer sb.release();
281 |
282 | // Use sb as documented above
283 | sb.write("hello world");
284 | ...
285 | {% endhighlight %}
286 |
287 |
288 |
289 |
295 |
296 |
deinit(self: *Pool) void
297 |
298 |
Releases all allocated memory. The Pool
and any of its StringBuilders
should not be used after this is called.
299 |
300 |
301 |
302 |
acquire(self: *Pool) ?*StringBuilder
303 |
304 |
Retrieves a StringBuilder
from the pool. If the pool is empty, will attempt to create a new one. This method is thread-safe.
305 |
306 |
307 |
308 |
release(self: *Pool, sb: *StringBuilder) void
309 |
310 |
Releases the StringBuilder
back into the pool. If the pool is full, the StringBuilder
is freed. This method is thread-safe. Most implentations will prefer to call release()
on the StringBuilder
directly.
311 |
312 |
313 |
314 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 = "my-app",
16 | .paths = .{""},
17 | .version = "0.0.0",
18 | .dependencies = .{
19 | .zul = .{
20 | .url = "https://github.com/karlseguin/zul/archive/master.tar.gz",
21 | .hash = "$INSERT_HASH_HERE"
22 | },
23 | },
24 | }
25 | ```
26 |
27 | To get the hash, run:
28 |
29 | ```bash
30 | zig fetch https://github.com/karlseguin/zul/archive/master.tar.gz
31 | ```
32 |
33 | Instead of `master` you can use a specific commit/tag.
34 |
35 | Next, in your `build.zig`, you should already have an executable, something like:
36 |
37 | ```zig
38 | const exe = b.addExecutable(.{
39 | .name = "my-app",
40 | .root_source_file = b.path("src/main.zig"),
41 | .target = target,
42 | .optimize = optimize,
43 | });
44 | ```
45 |
46 | Add the following line:
47 |
48 | ```zig
49 | exe.root_module.addImport("zul", b.dependency("zul", .{}).module("zul"));
50 | ```
51 |
52 | You can now `const zul = @import("zul");` in your project.
53 |
54 | ## [zul.benchmark.run](https://www.goblgobl.com/zul/benchmark/)
55 | Simple benchmarking function.
56 |
57 | ```zig
58 | const HAYSTACK = "abcdefghijklmnopqrstvuwxyz0123456789";
59 |
60 | pub fn main() !void {
61 | (try zul.benchmark.run(indexOfScalar, .{})).print("indexOfScalar");
62 | (try zul.benchmark.run(lastIndexOfScalar, .{})).print("lastIndexOfScalar");
63 | }
64 |
65 | fn indexOfScalar(_: Allocator, _: *std.time.Timer) !void {
66 | const i = std.mem.indexOfScalar(u8, HAYSTACK, '9').?;
67 | if (i != 35) {
68 | @panic("fail");
69 | }
70 | }
71 |
72 | fn lastIndexOfScalar(_: Allocator, _: *std.time.Timer) !void {
73 | const i = std.mem.lastIndexOfScalar(u8, HAYSTACK, 'a').?;
74 | if (i != 0) {
75 | @panic("fail");
76 | }
77 | }
78 |
79 | // indexOfScalar
80 | // 49882322 iterations 59.45ns per iterations
81 | // worst: 167ns median: 42ns stddev: 20.66ns
82 | //
83 | // lastIndexOfScalar
84 | // 20993066 iterations 142.15ns per iterations
85 | // worst: 292ns median: 125ns stddev: 23.13ns
86 | ```
87 |
88 | ## [zul.CommandLineArgs](https://www.goblgobl.com/zul/command_line_args/)
89 | A simple command line parser.
90 |
91 | ```zig
92 | var args = try zul.CommandLineArgs.parse(allocator);
93 | defer args.deinit();
94 |
95 | if (args.contains("version")) {
96 | //todo: print the version
97 | os.exit(0);
98 | }
99 |
100 | // Values retrieved from args.get are valid until args.deinit()
101 | // is called. Dupe the value if needed.
102 | const host = args.get("host") orelse "127.0.0.1";
103 | ...
104 | ```
105 |
106 | ## [zul.DateTime](https://www.goblgobl.com/zul/datetime/)
107 | Simple (no leap seconds, UTC-only), DateTime, Date and Time types.
108 |
109 | ```zig
110 | // Currently only supports RFC3339
111 | const dt = try zul.DateTime.parse("2028-11-05T23:29:10Z", .rfc3339);
112 | const next_week = try dt.add(7, .days);
113 | std.debug.assert(next_week.order(dt) == .gt);
114 |
115 | // 1857079750000 == 2028-11-05T23:29:10Z
116 | std.debug.print("{d} == {s}", .{dt.unix(.milliseconds), dt});
117 | ```
118 |
119 | ## [zul.fs.readDir](https://www.goblgobl.com/zul/fs/readdir/)
120 | Iterates, non-recursively, through a directory.
121 |
122 | ```zig
123 | // Parameters:
124 | // 1- Absolute or relative directory path
125 | var it = try zul.fs.readDir("/tmp/dir");
126 | defer it.deinit();
127 |
128 | // can iterate through the files
129 | while (try it.next()) |entry| {
130 | std.debug.print("{s} {any}\n", .{entry.name, entry.kind});
131 | }
132 |
133 | // reset the iterator
134 | it.reset();
135 |
136 | // or can collect them into a slice, optionally sorted:
137 | const sorted_entries = try it.all(allocator, .dir_first);
138 | for (sorted_entries) |entry| {
139 | std.debug.print("{s} {any}\n", .{entry.name, entry.kind});
140 | }
141 | ```
142 |
143 | ## [zul.fs.readJson](https://www.goblgobl.com/zul/fs/readjson/)
144 | Reads and parses a JSON file.
145 |
146 | ```zig
147 | // Parameters:
148 | // 1- The type to parse the JSON data into
149 | // 2- An allocator
150 | // 3- Absolute or relative path
151 | // 4- std.json.ParseOptions
152 | const managed_user = try zul.fs.readJson(User, allocator, "/tmp/data.json", .{});
153 |
154 | // readJson returns a zul.Managed(T)
155 | // managed_user.value is valid until managed_user.deinit() is called
156 | defer managed_user.deinit();
157 | const user = managed_user.value;
158 | ```
159 |
160 | ## [zul.fs.readLines](https://www.goblgobl.com/zul/fs/readlines/)
161 | Iterate over the lines in a file.
162 |
163 | ```zig
164 | // create a buffer large enough to hold the longest valid line
165 | var line_buffer: [1024]u8 = undefined;
166 |
167 | // Parameters:
168 | // 1- an absolute or relative path to the file
169 | // 2- the line buffer
170 | // 3- options (here we're using the default)
171 | var it = try zul.fs.readLines("/tmp/data.txt", &line_buffer, .{});
172 | defer it.deinit();
173 |
174 | while (try it.next()) |line| {
175 | // line is only valid until the next call to
176 | // it.next() or it.deinit()
177 | std.debug.print("line: {s}\n", .{line});
178 | }
179 | ```
180 |
181 | ## [zul.http.Client](https://www.goblgobl.com/zul/http/client/)
182 | A wrapper around std.http.Client to make it easier to create requests and consume responses.
183 |
184 | ```zig
185 | // The client is thread-safe
186 | var client = zul.http.Client.init(allocator);
187 | defer client.deinit();
188 |
189 | // Not thread safe, method defaults to .GET
190 | var req = try client.request("https://api.github.com/search/topics");
191 | defer req.deinit();
192 |
193 | // Set the querystring, can also be set in the URL passed to client.request
194 | // or a mix of setting in client.request and programmatically via req.query
195 | try req.query("q", "zig");
196 |
197 | try req.header("Authorization", "Your Token");
198 |
199 | // The lifetime of res is tied to req
200 | var res = try req.getResponse(.{});
201 | if (res.status != 200) {
202 | // TODO: handle error
203 | return;
204 | }
205 |
206 | // On success, this is a zul.Managed(SearchResult), its lifetime is detached
207 | // from the req, allowing it to outlive req.
208 | const managed = try res.json(SearchResult, allocator, .{});
209 |
210 | // Parsing the JSON and creating SearchResult [probably] required some allocations.
211 | // Internally an arena was created to manage this from the allocator passed to
212 | // res.json.
213 | defer managed.deinit();
214 |
215 | const search_result = managed.value;
216 | ```
217 |
218 | ## [zul.JsonString](https://www.goblgobl.com/zul/json_string/)
219 | Allows the embedding of already-encoded JSON strings into objects in order to avoid double encoded values.
220 |
221 | ```zig
222 | const an_encoded_json_value = "{\"over\": 9000}";
223 | const str = try std.json.stringifyAlloc(allocator, .{
224 | .name = "goku",
225 | .power = zul.jsonString(an_encoded_json_value),
226 | }, .{});
227 | ```
228 |
229 | ## [zul.pool](https://www.goblgobl.com/zul/pool/)
230 | A thread-safe object pool which will dynamically grow when empty and revert to the configured size.
231 |
232 | ```zig
233 | // create a pool for our Expensive class.
234 | // Our Expensive class takes a special initializing context, here an usize which
235 | // we set to 10_000. This is just to pass data from the pool into Expensive.init
236 | var pool = try zul.pool.Growing(Expensive, usize).init(allocator, 10_000, .{.count = 100});
237 | defer pool.deinit();
238 |
239 | // acquire will either pick an item from the pool
240 | // if the pool is empty, it'll create a new one (hence, "Growing")
241 | var exp1 = try pool.acquire();
242 | defer pool.release(exp1);
243 |
244 | ...
245 |
246 | // pooled object must have 3 functions
247 | const Expensive = struct {
248 | // an init function
249 | pub fn init(allocator: Allocator, size: usize) !Expensive {
250 | return .{
251 | // ...
252 | };
253 | }
254 |
255 | // a deinit method
256 | pub fn deinit(self: *Expensive) void {
257 | // ...
258 | }
259 |
260 | // a reset method, called when the item is released back to the pool
261 | pub fn reset(self: *Expensive) void {
262 | // ...
263 | }
264 | };
265 | ```
266 |
267 | ## [zul.Scheduler](https://www.goblgobl.com/zul/scheduler/)
268 | Ephemeral thread-based task scheduler used to run tasks at a specific time.
269 |
270 | ```zig
271 | // Where multiple types of tasks can be scheduled using the same schedule,
272 | // a tagged union is ideal.
273 | const Task = union(enum) {
274 | say: []const u8,
275 |
276 | // Whether T is a tagged union (as here) or another type, a public
277 | // run function must exist
278 | pub fn run(task: Task, ctx: void, at: i64) void {
279 | // the original time the task was scheduled for
280 | _ = at;
281 |
282 | // application-specific context that will be passed to each task
283 | _ ctx;
284 |
285 | switch (task) {
286 | .say => |msg| {std.debug.print("{s}\n", .{msg}),
287 | }
288 | }
289 | }
290 |
291 | ...
292 |
293 | // This example doesn't use a app-context, so we specify its
294 | // type as void
295 | var s = zul.Scheduler(Task, void).init(allocator);
296 | defer s.deinit();
297 |
298 | // Starts the scheduler, launching a new thread
299 | // We pass our context. Since we have a null context
300 | // we pass a null value, i.e. {}
301 | try s.start({});
302 |
303 | // will run the say task in 5 seconds
304 | try s.scheduleIn(.{.say = "world"}, std.time.ms_per_s * 5);
305 |
306 | // will run the say task in 100 milliseconds
307 | try s.schedule(.{.say = "hello"}, std.time.milliTimestamp() + 100);
308 | ```
309 |
310 | ## [zul.sort](https://www.goblgobl.com/zul/sort/)
311 | Helpers for sorting strings and integers
312 |
313 | ```zig
314 | // sorting strings based on their bytes
315 | var values = [_][]const u8{"ABC", "abc", "Dog", "Cat", "horse", "chicken"};
316 | zul.sort.strings(&values, .asc);
317 |
318 | // sort ASCII strings, ignoring case
319 | zul.sort.asciiIgnoreCase(&values, .desc);
320 |
321 | // sort integers or floats
322 | var numbers = [_]i32{10, -20, 33, 0, 2, 6};
323 | zul.sort.numbers(i32, &numbers, .asc);
324 | ```
325 |
326 | ## [zul.StringBuilder](https://www.goblgobl.com/zul/string_builder/)
327 | Efficiently create/concat strings or binary data, optionally using a thread-safe pool with pre-allocated static buffers.
328 |
329 | ```zig
330 | // StringBuilder can be used to efficiently concatenate strings
331 | // But it can also be used to craft binary payloads.
332 | var sb = zul.StringBuilder.init(allocator);
333 | defer sb.deinit();
334 |
335 | // We're going to generate a 4-byte length-prefixed message.
336 | // We don't know the length yet, so we'll skip 4 bytes
337 | // We get back a "view" which will let us backfill the length
338 | var view = try sb.skip(4);
339 |
340 | // Writes a single byte
341 | try sb.writeByte(10);
342 |
343 | // Writes a []const u8
344 | try sb.write("hello");
345 |
346 | // Using our view, which points to where the view was taken,
347 | // fill in the length.
348 | view.writeU32Big(@intCast(sb.len() - 4));
349 |
350 | std.debug.print("{any}\n", .{sb.string()});
351 | // []u8{0, 0, 0, 6, 10, 'h', 'e', 'l', 'l', 'o'}
352 | ```
353 |
354 | ## [zul.testing](https://www.goblgobl.com/zul/testing/)
355 | Helpers for writing tests.
356 |
357 | ```zig
358 | const t = zul.testing;
359 |
360 | test "memcpy" {
361 | // clear's the arena allocator
362 | defer t.reset();
363 |
364 | // In addition to exposing std.testing.allocator as zul.testing.allocator
365 | // zul.testing.arena is an ArenaAllocator. An ArenaAllocator can
366 | // make managing test-specific allocations a lot simpler.
367 | // Just stick a `defer zul.testing.reset()` atop your test.
368 | var buf = try t.arena.allocator().alloc(u8, 5);
369 |
370 | // unlike std.testing.expectEqual, zul's expectEqual
371 | // will coerce expected to actual's type, so this is valid:
372 | try t.expectEqual(5, buf.len);
373 |
374 | @memcpy(buf[0..5], "hello");
375 |
376 | // zul's expectEqual also works with strings.
377 | try t.expectEqual("hello", buf);
378 | }
379 | ```
380 |
381 | ## [zul.ThreadPool](https://www.goblgobl.com/zul/thread_pool/)
382 | Lightweight thread pool with back-pressure and zero allocations after initialization.
383 |
384 | ```zig
385 | var tp = try zul.ThreadPool(someTask).init(allocator, .{.count = 4, .backlog = 500});
386 | defer tp.deinit(allocator);
387 |
388 | // This will block if the threadpool has 500 pending jobs
389 | // where 500 is the configured backlog
390 | tp.spawn(.{1, true});
391 |
392 |
393 | fn someTask(i: i32, allow: bool) void {
394 | // process
395 | }
396 | ```
397 |
398 | ## [zul.UUID](https://www.goblgobl.com/zul/uuid/)
399 | Parse and generate version 4 and version 7 UUIDs.
400 |
401 | ```zig
402 | // v4() returns a zul.UUID
403 | const uuid1 = zul.UUID.v4();
404 |
405 | // toHex() returns a [36]u8
406 | const hex = uuid1.toHex(.lower);
407 |
408 | // returns a zul.UUID (or an error)
409 | const uuid2 = try zul.UUID.parse("761e3a9d-4f92-4e0d-9d67-054425c2b5c3");
410 | std.debug.print("{any}\n", uuid1.eql(uuid2));
411 |
412 | // create a UUIDv7
413 | const uuid3 = zul.UUID.v7();
414 |
415 | // zul.UUID can be JSON serialized
416 | try std.json.stringify(.{.id = uuid3}, .{}, writer);
417 | ```
418 |
419 |
420 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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.StringHashMap([]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).init(allocator);
29 | var lookup = std.StringHashMap([]const u8).init(allocator);
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(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(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(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(key[j .. j + 1], "");
73 | }
74 | try lookup.put(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 |
--------------------------------------------------------------------------------
/src/fs.zig:
--------------------------------------------------------------------------------
1 | const std = @import("std");
2 | const zul = @import("zul.zig");
3 |
4 | const Allocator = std.mem.Allocator;
5 |
6 | pub const LineIterator = LineIteratorSize(4096);
7 |
8 | // Made into a generic so that we can efficiently test files larger than buffer
9 | pub fn LineIteratorSize(comptime size: usize) type {
10 | return struct {
11 | out: []u8,
12 | delimiter: u8,
13 | file: std.fs.File,
14 | buffered: std.io.BufferedReader(size, std.fs.File.Reader),
15 |
16 | const Self = @This();
17 |
18 | pub const Opts = struct {
19 | open_flags: std.fs.File.OpenFlags = .{},
20 | delimiter: u8 = '\n',
21 | };
22 |
23 | pub fn deinit(self: Self) void {
24 | self.file.close();
25 | }
26 |
27 | pub fn next(self: *Self) !?[]u8 {
28 | const delimiter = self.delimiter;
29 |
30 | var out = self.out;
31 | var written: usize = 0;
32 |
33 | var buffered = &self.buffered;
34 | while (true) {
35 | const start = buffered.start;
36 | if (std.mem.indexOfScalar(u8, buffered.buf[start..buffered.end], delimiter)) |pos| {
37 | const written_end = written + pos;
38 | if (written_end > out.len) {
39 | return error.StreamTooLong;
40 | }
41 |
42 | const delimiter_pos = start + pos;
43 | if (written == 0) {
44 | // Optimization. We haven't written anything into `out` and we have
45 | // a line. We can return this directly from our buffer, no need to
46 | // copy it into `out`.
47 | buffered.start = delimiter_pos + 1;
48 | return buffered.buf[start..delimiter_pos];
49 | } else {
50 | @memcpy(out[written..written_end], buffered.buf[start..delimiter_pos]);
51 | buffered.start = delimiter_pos + 1;
52 | return out[0..written_end];
53 | }
54 | } else {
55 | // We didn't find the delimiter. That means we need to write the rest
56 | // of our buffered content to out, refill our buffer, and try again.
57 | const written_end = (written + buffered.end - start);
58 | if (written_end > out.len) {
59 | return error.StreamTooLong;
60 | }
61 | @memcpy(out[written..written_end], buffered.buf[start..buffered.end]);
62 | written = written_end;
63 |
64 | // fill our buffer
65 | const n = try buffered.unbuffered_reader.read(buffered.buf[0..]);
66 | if (n == 0) {
67 | return null;
68 | }
69 | buffered.start = 0;
70 | buffered.end = n;
71 | }
72 | }
73 | }
74 | };
75 | }
76 |
77 | pub fn readLines(file_path: []const u8, out: []u8, opts: LineIterator.Opts) !LineIterator {
78 | return readLinesSize(4096, file_path, out, opts);
79 | }
80 |
81 | pub fn readLinesSize(comptime size: usize, file_path: []const u8, out: []u8, opts: LineIterator.Opts) !LineIteratorSize(size) {
82 | const file = blk: {
83 | if (std.fs.path.isAbsolute(file_path)) {
84 | break :blk try std.fs.openFileAbsolute(file_path, opts.open_flags);
85 | } else {
86 | break :blk try std.fs.cwd().openFile(file_path, opts.open_flags);
87 | }
88 | };
89 |
90 | const buffered = std.io.bufferedReaderSize(size, file.reader());
91 | return .{
92 | .out = out,
93 | .file = file,
94 | .buffered = buffered,
95 | .delimiter = opts.delimiter,
96 | };
97 | }
98 |
99 | pub fn readJson(comptime T: type, allocator: Allocator, file_path: []const u8, opts: std.json.ParseOptions) !zul.Managed(T) {
100 | const file = blk: {
101 | if (std.fs.path.isAbsolute(file_path)) {
102 | break :blk try std.fs.openFileAbsolute(file_path, .{});
103 | } else {
104 | break :blk try std.fs.cwd().openFile(file_path, .{});
105 | }
106 | };
107 | defer file.close();
108 |
109 | var buffered = std.io.bufferedReader(file.reader());
110 | var reader = std.json.reader(allocator, buffered.reader());
111 | defer reader.deinit();
112 |
113 | var o = opts;
114 | o.allocate = .alloc_always;
115 | const parsed = try std.json.parseFromTokenSource(T, allocator, &reader, o);
116 | return zul.Managed(T).fromJson(parsed);
117 | }
118 |
119 | pub fn readDir(dir_path: []const u8) !Iterator {
120 | const dir = blk: {
121 | if (std.fs.path.isAbsolute(dir_path)) {
122 | break :blk try std.fs.openDirAbsolute(dir_path, .{ .iterate = true });
123 | } else {
124 | break :blk try std.fs.cwd().openDir(dir_path, .{ .iterate = true });
125 | }
126 | };
127 |
128 | return .{
129 | .dir = dir,
130 | .it = dir.iterate(),
131 | };
132 | }
133 |
134 | pub const Iterator = struct {
135 | dir: Dir,
136 | it: Dir.Iterator,
137 | arena: ?*std.heap.ArenaAllocator = null,
138 |
139 | const Dir = std.fs.Dir;
140 | const Entry = Dir.Entry;
141 |
142 | pub const Sort = enum {
143 | none,
144 | alphabetic,
145 | dir_first,
146 | dir_last,
147 | };
148 |
149 | pub fn deinit(self: *Iterator) void {
150 | self.dir.close();
151 | if (self.arena) |arena| {
152 | const allocator = arena.child_allocator;
153 | arena.deinit();
154 | allocator.destroy(arena);
155 | }
156 | }
157 |
158 | pub fn reset(self: *Iterator) void {
159 | self.it.reset();
160 | }
161 |
162 | pub fn next(self: *Iterator) !?std.fs.Dir.Entry {
163 | return self.it.next();
164 | }
165 |
166 | pub fn all(self: *Iterator, allocator: Allocator, sort: Sort) ![]std.fs.Dir.Entry {
167 | var arena = try allocator.create(std.heap.ArenaAllocator);
168 | errdefer allocator.destroy(arena);
169 |
170 | arena.* = std.heap.ArenaAllocator.init(allocator);
171 | errdefer arena.deinit();
172 |
173 | const aa = arena.allocator();
174 |
175 | var arr = std.ArrayList(Entry).init(aa);
176 |
177 | var it = self.it;
178 | while (try it.next()) |entry| {
179 | try arr.append(.{
180 | .kind = entry.kind,
181 | .name = try aa.dupe(u8, entry.name),
182 | });
183 | }
184 |
185 | self.arena = arena;
186 | const items = arr.items;
187 |
188 | switch (sort) {
189 | .alphabetic => std.sort.pdq(Entry, items, {}, sortEntriesAlphabetic),
190 | .dir_first => std.sort.pdq(Entry, items, {}, sortEntriesDirFirst),
191 | .dir_last => std.sort.pdq(Entry, items, {}, sortEntriesDirLast),
192 | .none => {},
193 | }
194 | return items;
195 | }
196 |
197 | fn sortEntriesAlphabetic(ctx: void, a: Entry, b: Entry) bool {
198 | _ = ctx;
199 | return std.ascii.lessThanIgnoreCase(a.name, b.name);
200 | }
201 | fn sortEntriesDirFirst(ctx: void, a: Entry, b: Entry) bool {
202 | _ = ctx;
203 | if (a.kind == b.kind) {
204 | return std.ascii.lessThanIgnoreCase(a.name, b.name);
205 | }
206 | return a.kind == .directory;
207 | }
208 | fn sortEntriesDirLast(ctx: void, a: Entry, b: Entry) bool {
209 | _ = ctx;
210 | if (a.kind == b.kind) {
211 | return std.ascii.lessThanIgnoreCase(a.name, b.name);
212 | }
213 | return a.kind != .directory;
214 | }
215 | };
216 |
217 | const t = zul.testing;
218 | test "fs.readLines: file not found" {
219 | var out = [_]u8{};
220 | try t.expectError(error.FileNotFound, readLines("tests/does_not_exist", &out, .{}));
221 | try t.expectError(error.FileNotFound, readLines("/tmp/zul/tests/does_not_exist", &out, .{}));
222 | }
223 |
224 | test "fs.readLines: empty" {
225 | defer t.reset();
226 | var out: [10]u8 = undefined;
227 | for (testAbsoluteAndRelative("tests/empty")) |file_path| {
228 | var it = try readLines(file_path, &out, .{});
229 | defer it.deinit();
230 | try t.expectEqual(null, try it.next());
231 | }
232 | }
233 |
234 | test "fs.readLines: single char" {
235 | defer t.reset();
236 | var out: [10]u8 = undefined;
237 | for (testAbsoluteAndRelative("tests/fs/single_char")) |file_path| {
238 | var it = try readLines(file_path, &out, .{});
239 | defer it.deinit();
240 | try t.expectEqual("l", (try it.next()).?);
241 | try t.expectEqual(null, try it.next());
242 | }
243 | }
244 |
245 | test "fs.readLines: larger than out" {
246 | defer t.reset();
247 | var out: [10]u8 = undefined;
248 | for (testAbsoluteAndRelative("tests/fs/long_line")) |file_path| {
249 | var it = try readLines(file_path, &out, .{});
250 | defer it.deinit();
251 | try t.expectError(error.StreamTooLong, it.next());
252 | }
253 | }
254 |
255 | test "fs.readLines: multiple lines" {
256 | defer t.reset();
257 | var out: [30]u8 = undefined;
258 | for (testAbsoluteAndRelative("tests/fs/lines")) |file_path| {
259 | var it = try readLinesSize(20, file_path, &out, .{});
260 | defer it.deinit();
261 | try t.expectEqual("Consider Phlebas", (try it.next()).?);
262 | try t.expectEqual("Old Man's War", (try it.next()).?);
263 | try t.expectEqual("Hyperion", (try it.next()).?);
264 | try t.expectEqual("Under Heaven", (try it.next()).?);
265 | try t.expectEqual("Project Hail Mary", (try it.next()).?);
266 | try t.expectEqual("Roadside Picnic", (try it.next()).?);
267 | try t.expectEqual("The Fifth Season", (try it.next()).?);
268 | try t.expectEqual("Sundiver", (try it.next()).?);
269 | try t.expectEqual(null, try it.next());
270 | }
271 | }
272 |
273 | test "fs.readJson: file not found" {
274 | try t.expectError(error.FileNotFound, readJson(TestStruct, t.allocator, "tests/does_not_exist", .{}));
275 | try t.expectError(error.FileNotFound, readJson(TestStruct, t.allocator, "/tmp/zul/tests/does_not_exist", .{}));
276 | }
277 |
278 | test "fs.readJson: invalid json" {
279 | try t.expectError(error.SyntaxError, readJson(TestStruct, t.allocator, "tests/fs/lines", .{}));
280 | }
281 |
282 | test "fs.readJson: success" {
283 | defer t.reset();
284 | for (testAbsoluteAndRelative("tests/fs/test_struct.json")) |file_path| {
285 | const s = try readJson(TestStruct, t.allocator, file_path, .{});
286 | defer s.deinit();
287 | try t.expectEqual(9001, s.value.id);
288 | try t.expectEqual("Goku", s.value.name);
289 | try t.expectEqual("c", s.value.tags[2]);
290 | }
291 | }
292 |
293 | test "fs.readDir: dir not found" {
294 | try t.expectError(error.FileNotFound, readDir("tests/fs/not_found"));
295 | try t.expectError(error.FileNotFound, readDir("/tmp/zul/tests/fs/not_found"));
296 | }
297 |
298 | test "fs.readDir: iterate" {
299 | defer t.reset();
300 |
301 | for (testAbsoluteAndRelative("tests/fs")) |dir_path| {
302 | var it = try readDir(dir_path);
303 | defer it.deinit();
304 |
305 | //loop twice, it.reset() should allow a re-iteration
306 | for (0..2) |_| {
307 | it.reset();
308 | var expected = testFsEntires();
309 |
310 | while (try it.next()) |entry| {
311 | const found = expected.fetchRemove(entry.name) orelse {
312 | std.debug.print("fs.iterate unknown entry: {s}", .{entry.name});
313 | return error.UnknownEntry;
314 | };
315 | try t.expectEqual(found.value, entry.kind);
316 | }
317 | try t.expectEqual(0, expected.count());
318 | }
319 | }
320 | }
321 |
322 | test "fs.readDir: all unsorted" {
323 | defer t.reset();
324 | for (testAbsoluteAndRelative("tests/fs")) |dir_path| {
325 | var expected = testFsEntires();
326 |
327 | var it = try readDir(dir_path);
328 | defer it.deinit();
329 | const entries = try it.all(t.allocator, .none);
330 | for (entries) |entry| {
331 | const found = expected.fetchRemove(entry.name) orelse {
332 | std.debug.print("fs.iterate unknown entry: {s}", .{entry.name});
333 | return error.UnknownEntry;
334 | };
335 | try t.expectEqual(found.value, entry.kind);
336 | }
337 | try t.expectEqual(0, expected.count());
338 | }
339 | }
340 |
341 | test "fs.readDir: sorted alphabetic" {
342 | defer t.reset();
343 | for (testAbsoluteAndRelative("tests/fs")) |dir_path| {
344 | var it = try readDir(dir_path);
345 | defer it.deinit();
346 |
347 | const entries = try it.all(t.allocator, .alphabetic);
348 | try t.expectEqual(6, entries.len);
349 | try t.expectEqual("lines", entries[0].name);
350 | try t.expectEqual("long_line", entries[1].name);
351 | try t.expectEqual("single_char", entries[2].name);
352 | try t.expectEqual("sub-1", entries[3].name);
353 | try t.expectEqual("sub-2", entries[4].name);
354 | try t.expectEqual("test_struct.json", entries[5].name);
355 | }
356 | }
357 |
358 | test "fs.readDir: sorted dir first" {
359 | defer t.reset();
360 | for (testAbsoluteAndRelative("tests/fs")) |dir_path| {
361 | var it = try readDir(dir_path);
362 | defer it.deinit();
363 |
364 | const entries = try it.all(t.allocator, .dir_first);
365 | try t.expectEqual(6, entries.len);
366 | try t.expectEqual("sub-1", entries[0].name);
367 | try t.expectEqual("sub-2", entries[1].name);
368 | try t.expectEqual("lines", entries[2].name);
369 | try t.expectEqual("long_line", entries[3].name);
370 | try t.expectEqual("single_char", entries[4].name);
371 | try t.expectEqual("test_struct.json", entries[5].name);
372 | }
373 | }
374 |
375 | test "fs.readDir: sorted dir last" {
376 | defer t.reset();
377 | for (testAbsoluteAndRelative("tests/fs")) |dir_path| {
378 | var it = try readDir(dir_path);
379 | defer it.deinit();
380 |
381 | const entries = try it.all(t.allocator, .dir_last);
382 | try t.expectEqual(6, entries.len);
383 | try t.expectEqual("lines", entries[0].name);
384 | try t.expectEqual("long_line", entries[1].name);
385 | try t.expectEqual("single_char", entries[2].name);
386 | try t.expectEqual("test_struct.json", entries[3].name);
387 | try t.expectEqual("sub-1", entries[4].name);
388 | try t.expectEqual("sub-2", entries[5].name);
389 | }
390 | }
391 |
392 | const TestStruct = struct {
393 | id: i32,
394 | name: []const u8,
395 | tags: [][]const u8,
396 | };
397 |
398 | fn testAbsoluteAndRelative(relative: []const u8) [2][]const u8 {
399 | const allocator = t.arena.allocator();
400 | return [2][]const u8{
401 | allocator.dupe(u8, relative) catch unreachable,
402 | std.fs.cwd().realpathAlloc(allocator, relative) catch unreachable,
403 | };
404 | }
405 |
406 | fn testFsEntires() std.StringHashMap(std.fs.File.Kind) {
407 | var map = std.StringHashMap(std.fs.File.Kind).init(t.arena.allocator());
408 | map.put("sub-1", .directory) catch unreachable;
409 | map.put("sub-2", .directory) catch unreachable;
410 | map.put("single_char", .file) catch unreachable;
411 | map.put("lines", .file) catch unreachable;
412 | map.put("long_line", .file) catch unreachable;
413 | map.put("test_struct.json", .file) catch unreachable;
414 | return map;
415 | }
416 |
--------------------------------------------------------------------------------
/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.time.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 |
--------------------------------------------------------------------------------
/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.time.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.time.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.time.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.time.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 |
--------------------------------------------------------------------------------
/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 |
129 | pub fn format(self: UUID, comptime layout: []const u8, options: fmt.FormatOptions, out: anytype) !void {
130 | _ = options;
131 |
132 | const casing: std.fmt.Case = blk: {
133 | if (layout.len == 0) break :blk .lower;
134 | break :blk switch (layout[0]) {
135 | 's', 'x' => .lower,
136 | 'X' => .upper,
137 | else => @compileError("Unsupported format specifier for UUID: " ++ layout),
138 | };
139 | };
140 |
141 | const hex = self.toHex(casing);
142 | return std.fmt.format(out, "{s}", .{hex});
143 | }
144 | };
145 |
146 | fn b2h(bin: []const u8, hex: []u8, case: std.fmt.Case) void {
147 | const alphabet = if (case == .lower) "0123456789abcdef" else "0123456789ABCDEF";
148 |
149 | hex[8] = '-';
150 | hex[13] = '-';
151 | hex[18] = '-';
152 | hex[23] = '-';
153 |
154 | inline for (encoded_pos, 0..) |i, j| {
155 | hex[i + 0] = alphabet[bin[j] >> 4];
156 | hex[i + 1] = alphabet[bin[j] & 0x0f];
157 | }
158 | }
159 |
160 | const encoded_pos = [16]u8{ 0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34 };
161 |
162 | const hex_to_nibble = [_]u8{0xff} ** 48 ++ [_]u8{
163 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
164 | 0x08, 0x09, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
165 | 0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0xff,
166 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
167 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
168 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
169 | 0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0xff,
170 | } ++ [_]u8{0xff} ** 152;
171 |
172 | const t = @import("zul.zig").testing;
173 | test "uuid: parse" {
174 | const lower_uuids = [_][]const u8{
175 | "d0cd8041-0504-40cb-ac8e-d05960d205ec",
176 | "3df6f0e4-f9b1-4e34-ad70-33206069b995",
177 | "f982cf56-c4ab-4229-b23c-d17377d000be",
178 | "6b9f53be-cf46-40e8-8627-6b60dc33def8",
179 | "c282ec76-ac18-4d4a-8a29-3b94f5c74813",
180 | "00000000-0000-0000-0000-000000000000",
181 | };
182 |
183 | for (lower_uuids) |hex| {
184 | const uuid = try UUID.parse(hex);
185 | try t.expectEqual(hex, uuid.toHex(.lower));
186 | }
187 |
188 | const upper_uuids = [_][]const u8{
189 | "D0CD8041-0504-40CB-AC8E-D05960D205EC",
190 | "3DF6F0E4-F9B1-4E34-AD70-33206069B995",
191 | "F982CF56-C4AB-4229-B23C-D17377D000BE",
192 | "6B9F53BE-CF46-40E8-8627-6B60DC33DEF8",
193 | "C282EC76-AC18-4D4A-8A29-3B94F5C74813",
194 | "00000000-0000-0000-0000-000000000000",
195 | };
196 |
197 | for (upper_uuids) |hex| {
198 | const uuid = try UUID.parse(hex);
199 | try t.expectEqual(hex, uuid.toHex(.upper));
200 | }
201 | }
202 |
203 | test "uuid: parse invalid" {
204 | const uuids = [_][]const u8{
205 | "3df6f0e4-f9b1-4e34-ad70-33206069b99", // too short
206 | "3df6f0e4-f9b1-4e34-ad70-33206069b9912", // too long
207 | "3df6f0e4-f9b1-4e34-ad70_33206069b9912", // missing or invalid group separator
208 | "zdf6f0e4-f9b1-4e34-ad70-33206069b995", // invalid character
209 | };
210 |
211 | for (uuids) |uuid| {
212 | try t.expectError(error.InvalidUUID, UUID.parse(uuid));
213 | }
214 | }
215 |
216 | test "uuid: v4" {
217 | defer t.reset();
218 | const allocator = t.arena.allocator();
219 | var seen = std.StringHashMap(void).init(allocator);
220 | try seen.ensureTotalCapacity(100);
221 |
222 | for (0..100) |_| {
223 | const uuid = UUID.v4();
224 | try t.expectEqual(@as(usize, 16), uuid.bin.len);
225 | try t.expectEqual(4, uuid.bin[6] >> 4);
226 | try t.expectEqual(0x80, uuid.bin[8] & 0xc0);
227 | seen.putAssumeCapacity(try uuid.toHexAlloc(allocator, .lower), {});
228 | }
229 | try t.expectEqual(100, seen.count());
230 | }
231 |
232 | test "uuid: v7" {
233 | defer t.reset();
234 | const allocator = t.arena.allocator();
235 | var seen = std.StringHashMap(void).init(allocator);
236 | try seen.ensureTotalCapacity(100);
237 |
238 | var last: u64 = 0;
239 | for (0..100) |_| {
240 | const uuid = UUID.v7();
241 | try t.expectEqual(@as(usize, 16), uuid.bin.len);
242 | try t.expectEqual(7, uuid.bin[6] >> 4);
243 | try t.expectEqual(0x80, uuid.bin[8] & 0xc0);
244 | seen.putAssumeCapacity(try uuid.toHexAlloc(allocator, .lower), {});
245 |
246 | const ts = std.mem.readInt(u64, uuid.bin[0..8], .big);
247 | try t.expectEqual(true, ts > last);
248 | last = ts;
249 | }
250 | try t.expectEqual(100, seen.count());
251 | }
252 |
253 | test "uuid: hex" {
254 | for (0..20) |_| {
255 | const uuid = UUID.random();
256 | const upper = uuid.toHex(.upper);
257 | const lower = uuid.toHex(.lower);
258 |
259 | try t.expectEqual(true, std.ascii.eqlIgnoreCase(&lower, &upper));
260 |
261 | for (upper, lower, 0..) |u, l, i| {
262 | if (i == 8 or i == 13 or i == 18 or i == 23) {
263 | try t.expectEqual('-', u);
264 | try t.expectEqual('-', l);
265 | } else {
266 | try t.expectEqual(true, (u >= '0' and u <= '9') or (u >= 'A' and u <= 'F'));
267 | try t.expectEqual(true, (l >= '0' and l <= '9') or (l >= 'a' and l <= 'f'));
268 | }
269 | }
270 | }
271 | }
272 |
273 | test "uuid: binToHex" {
274 | for (0..20) |_| {
275 | const uuid = UUID.random();
276 | try t.expectEqual(&(try UUID.binToHex(&uuid.bin, .lower)), uuid.toHex(.lower));
277 | }
278 | }
279 |
280 | test "uuid: json" {
281 | defer t.reset();
282 | const uuid = try UUID.parse("938b1cd2-f479-442b-9ba6-59ebf441e695");
283 | var out = std.ArrayList(u8).init(t.arena.allocator());
284 |
285 | try std.json.stringify(.{
286 | .uuid = uuid,
287 | }, .{}, out.writer());
288 |
289 | try t.expectEqual("{\"uuid\":\"938b1cd2-f479-442b-9ba6-59ebf441e695\"}", out.items);
290 |
291 | const S = struct{uuid: UUID};
292 | const parsed = try std.json.parseFromSlice(S, t.allocator, out.items, .{});
293 | defer parsed.deinit();
294 |
295 | try t.expectEqual("938b1cd2-f479-442b-9ba6-59ebf441e695", &parsed.value.uuid.toHex(.lower));
296 | }
297 |
298 | test "uuid: format" {
299 | const uuid = try UUID.parse("d543E371-a33d-4e68-87ba-7c9e3470a3be");
300 |
301 | var buf: [50]u8 = undefined;
302 |
303 | {
304 | const str = try std.fmt.bufPrint(&buf, "[{s}]", .{uuid});
305 | try t.expectEqual("[d543e371-a33d-4e68-87ba-7c9e3470a3be]", str);
306 | }
307 |
308 | {
309 | const str = try std.fmt.bufPrint(&buf, "[{x}]", .{uuid});
310 | try t.expectEqual("[d543e371-a33d-4e68-87ba-7c9e3470a3be]", str);
311 | }
312 |
313 | {
314 | const str = try std.fmt.bufPrint(&buf, "[{X}]", .{uuid});
315 | try t.expectEqual("[D543E371-A33D-4E68-87BA-7C9E3470A3BE]", str);
316 | }
317 | }
318 |
319 | test "uuid: eql" {
320 | const uuid1 = UUID.v4();
321 | const uuid2 = try UUID.parse("2a7af44c-3b7e-41f6-8764-1aff701a024a");
322 | const uuid3 = try UUID.parse("2a7af44c-3b7e-41f6-8764-1aff701a024a");
323 | const uuid4 = try UUID.parse("5cc75a16-8592-4de3-8215-89824a9c62c0");
324 |
325 | try t.expectEqual(false, uuid1.eql(uuid2));
326 | try t.expectEqual(false, uuid2.eql(uuid1));
327 |
328 | try t.expectEqual(false, uuid1.eql(uuid3));
329 | try t.expectEqual(false, uuid3.eql(uuid1));
330 |
331 | try t.expectEqual(false, uuid1.eql(uuid4));
332 | try t.expectEqual(false, uuid4.eql(uuid1));
333 |
334 | try t.expectEqual(false, uuid2.eql(uuid4));
335 | try t.expectEqual(false, uuid4.eql(uuid2));
336 |
337 | try t.expectEqual(false, uuid3.eql(uuid4));
338 | try t.expectEqual(false, uuid4.eql(uuid3));
339 |
340 | try t.expectEqual(true, uuid2.eql(uuid3));
341 | try t.expectEqual(true, uuid3.eql(uuid2));
342 | }
343 |
--------------------------------------------------------------------------------
/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.stringifyAlloc(t.allocator, .{
65 | .data = jsonString("{\"over\": 9000}"),
66 | }, .{});
67 | defer t.allocator.free(str);
68 | try t.expectEqual("{\"data\":{\"over\": 9000}}", str);
69 | }
70 |
--------------------------------------------------------------------------------
/test_runner.zig:
--------------------------------------------------------------------------------
1 | // in your build.zig, you can specify a custom test runner:
2 | // const tests = b.addTest(.{
3 | // .target = target,
4 | // .optimize = optimize,
5 | // .test_runner = .{ .path = b.path("test_runner.zig"), .mode = .simple }, // add this line
6 | // .root_source_file = b.path("src/main.zig"),
7 | // });
8 |
9 | const std = @import("std");
10 | const builtin = @import("builtin");
11 |
12 | const Allocator = std.mem.Allocator;
13 |
14 | const BORDER = "=" ** 80;
15 |
16 | // use in custom panic handler
17 | var current_test: ?[]const u8 = null;
18 |
19 | pub fn main() !void {
20 | var mem: [8192]u8 = undefined;
21 | var fba = std.heap.FixedBufferAllocator.init(&mem);
22 |
23 | const allocator = fba.allocator();
24 |
25 | const env = Env.init(allocator);
26 | defer env.deinit(allocator);
27 |
28 | var slowest = SlowTracker.init(allocator, 5);
29 | defer slowest.deinit();
30 |
31 | var pass: usize = 0;
32 | var fail: usize = 0;
33 | var skip: usize = 0;
34 | var leak: usize = 0;
35 |
36 | const printer = Printer.init();
37 | printer.fmt("\r\x1b[0K", .{}); // beginning of line and clear to end of line
38 |
39 | for (builtin.test_functions) |t| {
40 | if (isSetup(t)) {
41 | t.func() catch |err| {
42 | printer.status(.fail, "\nsetup \"{s}\" failed: {}\n", .{ t.name, err });
43 | return err;
44 | };
45 | }
46 | }
47 |
48 | for (builtin.test_functions) |t| {
49 | if (isSetup(t) or isTeardown(t)) {
50 | continue;
51 | }
52 |
53 | var status = Status.pass;
54 | slowest.startTiming();
55 |
56 | const is_unnamed_test = isUnnamed(t);
57 | if (env.filter) |f| {
58 | if (!is_unnamed_test and std.mem.indexOf(u8, t.name, f) == null) {
59 | continue;
60 | }
61 | }
62 |
63 | const friendly_name = blk: {
64 | const name = t.name;
65 | var it = std.mem.splitScalar(u8, name, '.');
66 | while (it.next()) |value| {
67 | if (std.mem.eql(u8, value, "test")) {
68 | const rest = it.rest();
69 | break :blk if (rest.len > 0) rest else name;
70 | }
71 | }
72 | break :blk name;
73 | };
74 |
75 | current_test = friendly_name;
76 | std.testing.allocator_instance = .{};
77 | const result = t.func();
78 | current_test = null;
79 |
80 | const ns_taken = slowest.endTiming(friendly_name);
81 |
82 | if (std.testing.allocator_instance.deinit() == .leak) {
83 | leak += 1;
84 | printer.status(.fail, "\n{s}\n\"{s}\" - Memory Leak\n{s}\n", .{ BORDER, friendly_name, BORDER });
85 | }
86 |
87 | if (result) |_| {
88 | pass += 1;
89 | } else |err| switch (err) {
90 | error.SkipZigTest => {
91 | skip += 1;
92 | status = .skip;
93 | },
94 | else => {
95 | status = .fail;
96 | fail += 1;
97 | printer.status(.fail, "\n{s}\n\"{s}\" - {s}\n{s}\n", .{ BORDER, friendly_name, @errorName(err), BORDER });
98 | if (@errorReturnTrace()) |trace| {
99 | std.debug.dumpStackTrace(trace.*);
100 | }
101 | if (env.fail_first) {
102 | break;
103 | }
104 | },
105 | }
106 |
107 | if (env.verbose) {
108 | const ms = @as(f64, @floatFromInt(ns_taken)) / 1_000_000.0;
109 | printer.status(status, "{s} ({d:.2}ms)\n", .{ friendly_name, ms });
110 | } else {
111 | printer.status(status, ".", .{});
112 | }
113 | }
114 |
115 | for (builtin.test_functions) |t| {
116 | if (isTeardown(t)) {
117 | t.func() catch |err| {
118 | printer.status(.fail, "\nteardown \"{s}\" failed: {}\n", .{ t.name, err });
119 | return err;
120 | };
121 | }
122 | }
123 |
124 | const total_tests = pass + fail;
125 | const status = if (fail == 0) Status.pass else Status.fail;
126 | printer.status(status, "\n{d} of {d} test{s} passed\n", .{ pass, total_tests, if (total_tests != 1) "s" else "" });
127 | if (skip > 0) {
128 | printer.status(.skip, "{d} test{s} skipped\n", .{ skip, if (skip != 1) "s" else "" });
129 | }
130 | if (leak > 0) {
131 | printer.status(.fail, "{d} test{s} leaked\n", .{ leak, if (leak != 1) "s" else "" });
132 | }
133 | printer.fmt("\n", .{});
134 | try slowest.display(printer);
135 | printer.fmt("\n", .{});
136 | std.posix.exit(if (fail == 0) 0 else 1);
137 | }
138 |
139 | const Printer = struct {
140 | out: std.fs.File.Writer,
141 |
142 | fn init() Printer {
143 | return .{
144 | .out = std.io.getStdErr().writer(),
145 | };
146 | }
147 |
148 | fn fmt(self: Printer, comptime format: []const u8, args: anytype) void {
149 | std.fmt.format(self.out, format, args) catch unreachable;
150 | }
151 |
152 | fn status(self: Printer, s: Status, comptime format: []const u8, args: anytype) void {
153 | const color = switch (s) {
154 | .pass => "\x1b[32m",
155 | .fail => "\x1b[31m",
156 | .skip => "\x1b[33m",
157 | else => "",
158 | };
159 | const out = self.out;
160 | out.writeAll(color) catch @panic("writeAll failed?!");
161 | std.fmt.format(out, format, args) catch @panic("std.fmt.format failed?!");
162 | self.fmt("\x1b[0m", .{});
163 | }
164 | };
165 |
166 | const Status = enum {
167 | pass,
168 | fail,
169 | skip,
170 | text,
171 | };
172 |
173 | const SlowTracker = struct {
174 | const SlowestQueue = std.PriorityDequeue(TestInfo, void, compareTiming);
175 | max: usize,
176 | slowest: SlowestQueue,
177 | timer: std.time.Timer,
178 |
179 | fn init(allocator: Allocator, count: u32) SlowTracker {
180 | const timer = std.time.Timer.start() catch @panic("failed to start timer");
181 | var slowest = SlowestQueue.init(allocator, {});
182 | slowest.ensureTotalCapacity(count) catch @panic("OOM");
183 | return .{
184 | .max = count,
185 | .timer = timer,
186 | .slowest = slowest,
187 | };
188 | }
189 |
190 | const TestInfo = struct {
191 | ns: u64,
192 | name: []const u8,
193 | };
194 |
195 | fn deinit(self: SlowTracker) void {
196 | self.slowest.deinit();
197 | }
198 |
199 | fn startTiming(self: *SlowTracker) void {
200 | self.timer.reset();
201 | }
202 |
203 | fn endTiming(self: *SlowTracker, test_name: []const u8) u64 {
204 | var timer = self.timer;
205 | const ns = timer.lap();
206 |
207 | var slowest = &self.slowest;
208 |
209 | if (slowest.count() < self.max) {
210 | // Capacity is fixed to the # of slow tests we want to track
211 | // If we've tracked fewer tests than this capacity, than always add
212 | slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing");
213 | return ns;
214 | }
215 |
216 | {
217 | // Optimization to avoid shifting the dequeue for the common case
218 | // where the test isn't one of our slowest.
219 | const fastest_of_the_slow = slowest.peekMin() orelse unreachable;
220 | if (fastest_of_the_slow.ns > ns) {
221 | // the test was faster than our fastest slow test, don't add
222 | return ns;
223 | }
224 | }
225 |
226 | // the previous fastest of our slow tests, has been pushed off.
227 | _ = slowest.removeMin();
228 | slowest.add(TestInfo{ .ns = ns, .name = test_name }) catch @panic("failed to track test timing");
229 | return ns;
230 | }
231 |
232 | fn display(self: *SlowTracker, printer: Printer) !void {
233 | var slowest = self.slowest;
234 | const count = slowest.count();
235 | printer.fmt("Slowest {d} test{s}: \n", .{ count, if (count != 1) "s" else "" });
236 | while (slowest.removeMinOrNull()) |info| {
237 | const ms = @as(f64, @floatFromInt(info.ns)) / 1_000_000.0;
238 | printer.fmt(" {d:.2}ms\t{s}\n", .{ ms, info.name });
239 | }
240 | }
241 |
242 | fn compareTiming(context: void, a: TestInfo, b: TestInfo) std.math.Order {
243 | _ = context;
244 | return std.math.order(a.ns, b.ns);
245 | }
246 | };
247 |
248 | const Env = struct {
249 | verbose: bool,
250 | fail_first: bool,
251 | filter: ?[]const u8,
252 |
253 | fn init(allocator: Allocator) Env {
254 | return .{
255 | .verbose = readEnvBool(allocator, "TEST_VERBOSE", true),
256 | .fail_first = readEnvBool(allocator, "TEST_FAIL_FIRST", false),
257 | .filter = readEnv(allocator, "TEST_FILTER"),
258 | };
259 | }
260 |
261 | fn deinit(self: Env, allocator: Allocator) void {
262 | if (self.filter) |f| {
263 | allocator.free(f);
264 | }
265 | }
266 |
267 | fn readEnv(allocator: Allocator, key: []const u8) ?[]const u8 {
268 | const v = std.process.getEnvVarOwned(allocator, key) catch |err| {
269 | if (err == error.EnvironmentVariableNotFound) {
270 | return null;
271 | }
272 | std.log.warn("failed to get env var {s} due to err {}", .{ key, err });
273 | return null;
274 | };
275 | return v;
276 | }
277 |
278 | fn readEnvBool(allocator: Allocator, key: []const u8, deflt: bool) bool {
279 | const value = readEnv(allocator, key) orelse return deflt;
280 | defer allocator.free(value);
281 | return std.ascii.eqlIgnoreCase(value, "true");
282 | }
283 | };
284 |
285 | pub const panic = std.debug.FullPanic(struct {
286 | pub fn panicFn(msg: []const u8, first_trace_addr: ?usize) noreturn {
287 | if (current_test) |ct| {
288 | std.debug.print("\x1b[31m{s}\npanic running \"{s}\"\n{s}\x1b[0m\n", .{ BORDER, ct, BORDER });
289 | }
290 | std.debug.defaultPanic(msg, first_trace_addr);
291 | }
292 | }.panicFn);
293 |
294 | fn isUnnamed(t: std.builtin.TestFn) bool {
295 | const marker = ".test_";
296 | const test_name = t.name;
297 | const index = std.mem.indexOf(u8, test_name, marker) orelse return false;
298 | _ = std.fmt.parseInt(u32, test_name[index + marker.len ..], 10) catch return false;
299 | return true;
300 | }
301 |
302 | fn isSetup(t: std.builtin.TestFn) bool {
303 | return std.mem.endsWith(u8, t.name, "tests:beforeAll");
304 | }
305 |
306 | fn isTeardown(t: std.builtin.TestFn) bool {
307 | return std.mem.endsWith(u8, t.name, "tests:afterAll");
308 | }
309 |
--------------------------------------------------------------------------------
/tests/empty:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/karlseguin/zul/007aa7c4ed77272416c8111f4ce403e86abd56d3/tests/empty
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/fs/single_char:
--------------------------------------------------------------------------------
1 | l
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/fs/test_struct.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 9001,
3 | "name": "Goku",
4 | "tags": ["a", "b", "c"]
5 | }
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------