├── .gitignore ├── LICENSE ├── Makefile ├── build.zig ├── build.zig.zon ├── docs ├── .eleventy.js ├── .gitignore ├── package-lock.json ├── package.json └── src │ ├── _data │ └── env.js │ ├── _includes │ └── site.njk │ ├── assets │ └── favicon.png │ ├── benchmark.html │ ├── command_line_args.html │ ├── datetime.html │ ├── fs │ ├── readdir.html │ ├── readjson.html │ └── readlines.html │ ├── http │ └── client.html │ ├── index.njk │ ├── json_string.html │ ├── pool.html │ ├── readme.njk │ ├── scheduler.html │ ├── sort.html │ ├── string_builder.html │ ├── testing.html │ ├── thread_pool.html │ └── uuid.html ├── readme.md ├── src ├── arc.zig ├── benchmark.zig ├── command_line_args.zig ├── datetime.zig ├── fs.zig ├── http.zig ├── pool.zig ├── scheduler.zig ├── sort.zig ├── string_builder.zig ├── testing.zig ├── thread_pool.zig ├── uuid.zig └── zul.zig ├── test_runner.zig └── tests ├── empty ├── fs ├── lines ├── long_line ├── single_char ├── sub-1 │ └── file-1 ├── sub-2 │ └── file-2 └── test_struct.json └── large /.gitignore: -------------------------------------------------------------------------------- 1 | zig-out/ 2 | .zig-cache/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (Expat) 2 | 3 | Copyright (c) Karl Seguin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | F= 2 | 3 | .PHONY: t 4 | t: 5 | TEST_FILTER="${F}" zig build test --summary all -freference-trace 6 | 7 | .phony: d 8 | d: 9 | cd docs && npx @11ty/eleventy --serve --port 5300 10 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) !void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | // Expose this as a module that others can import 7 | _ = b.addModule("zul", .{ 8 | .root_source_file = b.path("src/zul.zig"), 9 | }); 10 | 11 | { 12 | // test step 13 | const lib_test = b.addTest(.{ 14 | .root_source_file = b.path("src/zul.zig"), 15 | .target = target, 16 | .optimize = optimize, 17 | .test_runner = .{ .path = b.path("test_runner.zig"), .mode = .simple }, 18 | }); 19 | 20 | const run_test = b.addRunArtifact(lib_test); 21 | run_test.has_side_effects = true; 22 | 23 | const test_step = b.step("test", "Run unit tests"); 24 | test_step.dependOn(&run_test.step); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .zul, 3 | .paths = .{""}, 4 | .version = "0.0.0", 5 | .fingerprint = 0xb06b9454b7e880d6, 6 | .dependencies = .{ 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /docs/.eleventy.js: -------------------------------------------------------------------------------- 1 | const sass = require('sass'); 2 | const cheerio = require('cheerio'); 3 | const pluginSass = require('@grimlink/eleventy-plugin-sass'); 4 | const syntaxHighlight = require('@11ty/eleventy-plugin-syntaxhighlight'); 5 | 6 | const env = require('./src/_data/env.js'); 7 | const package = require('./package.json'); 8 | 9 | module.exports = function(config) { 10 | config.addPassthroughCopy('src/assets/docs.js'); 11 | config.addPassthroughCopy('src/assets/favicon.png'); 12 | config.setTemplateFormats(['html', 'njk']); 13 | 14 | config.addCollection("sorted", function(collectionApi) { 15 | return collectionApi.getAll().sort(function(a, b) { 16 | return a.url.localeCompare(b.url); 17 | }); 18 | }); 19 | 20 | config.addPlugin(syntaxHighlight); 21 | config.addPlugin(pluginSass, { 22 | sass: sass, 23 | outputPath: '/assets/', 24 | outputStyle: (env.prod) ? 'compressed' : 'expanded', 25 | }); 26 | 27 | config.addNunjucksGlobal('postMeta', function(post) { 28 | const $ = cheerio.load(post.content); 29 | const example = $('pre').eq(0); 30 | return { 31 | desc: $('p').eq(0).text(), 32 | example: {raw: example.text(), html: example.prop('outerHTML')}, 33 | }; 34 | }); 35 | 36 | config.addAsyncFilter('asset_url', async function(url) { 37 | return env.baseURL + '/assets/' + url + '?v=' + package.version; 38 | }); 39 | 40 | return { 41 | dir: { 42 | input: 'src' 43 | } 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zul html documentation", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "author": "", 7 | "devDependencies": { 8 | "@11ty/eleventy": "^2.0.1", 9 | "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0", 10 | "@grimlink/eleventy-plugin-sass": "^1.0.3", 11 | "cheerio": "^1.0.0-rc.12", 12 | "markdown-it-anchor": "^8.6.7", 13 | "markdown-it-attrs": "^4.1.6", 14 | "sass": "^1.69.5" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/src/_data/env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | prod: process.env.ENV == 'prod', 3 | baseURL: process.env.BASE_URL || '', 4 | } 5 | -------------------------------------------------------------------------------- /docs/src/_includes/site.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | 60 | {% block header %}{% endblock %} 61 |
62 | 66 |
67 | 68 | {{ content | safe }} 69 | -------------------------------------------------------------------------------- /docs/src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlseguin/zul/007aa7c4ed77272416c8111f4ce403e86abd56d3/docs/src/assets/favicon.png -------------------------------------------------------------------------------- /docs/src/benchmark.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: site.njk 3 | title: zul.benchmark.run 4 | --- 5 | 6 |

7 |

Simple benchmarking function.

8 | 9 |

10 | {% highlight zig %} 11 | const HAYSTACK = "abcdefghijklmnopqrstvuwxyz0123456789"; 12 | 13 | pub fn main() !void { 14 | (try zul.benchmark.run(indexOfScalar, .{})).print("indexOfScalar"); 15 | (try zul.benchmark.run(lastIndexOfScalar, .{})).print("lastIndexOfScalar"); 16 | } 17 | 18 | fn indexOfScalar(_: Allocator, _: *std.time.Timer) !void { 19 | const i = std.mem.indexOfScalar(u8, HAYSTACK, '9').?; 20 | if (i != 35) { 21 | @panic("fail"); 22 | } 23 | } 24 | 25 | fn lastIndexOfScalar(_: Allocator, _: *std.time.Timer) !void { 26 | const i = std.mem.lastIndexOfScalar(u8, HAYSTACK, 'a').?; 27 | if (i != 0) { 28 | @panic("fail"); 29 | } 30 | } 31 | 32 | // indexOfScalar 33 | // 49882322 iterations 59.45ns per iterations 34 | // worst: 167ns median: 42ns stddev: 20.66ns 35 | // 36 | // lastIndexOfScalar 37 | // 20993066 iterations 142.15ns per iterations 38 | // worst: 292ns median: 125ns stddev: 23.13ns 39 | {% endhighlight %} 40 | 41 |

42 |
43 |
44 | allocator: std.mem.Allocator 45 |
46 |

Provided for any allocation the function must make. When used, the Result will contain the requested_bytes.

47 |
48 |
49 |
50 | timer: *std.time.Timer 51 |
52 |

In some cases, the function under benchmark might require setup that should not count towards the execution time. Use timer.reset() to reset the execution time to 0.

53 | 54 | {% highlight zig %} 55 | fn myfunc(_: Allocator, timer: *std.time.Timer) !void { 56 | // some expensive setup 57 | timer.reset(); 58 | // code to benchmark 59 | } 60 | {% endhighlight %} 61 | 62 |

In most cases, it is better to use runC and provide a context.

63 |
64 |
65 |
66 | opts: zul.benchmark.Opts 67 |
68 |

Options that control how the benchmark is run. Must be comptime-known.

69 |
    70 |
  • samples: u32 - The maximum number of samples to take for calculating metrics. Defaults to 10_000 71 |
  • runtime: usize - The time, in milliseconds, to run the benchmark for. Defaults ot 3000 (3 seconds). 72 |
73 |
74 |
75 |
76 | 77 |

variant

78 |

A variant of run that passes arbitrary data to the benchmark function. For example, rather than relying on a global INPUT, our above example could leverage runC:

79 | 80 | {% highlight zig %} 81 | pub fn main() !void { 82 | const ctx = Context{ 83 | .input = "abcdefghijklmnopqrstvuwxyz0123456789", 84 | }; 85 | 86 | (try zul.benchmark.runC(ctx, indexOfScalar, .{})).print("indexOfScalar"); 87 | (try zul.benchmark.runC(ctx, lastIndexOfScalar, .{})).print("lastIndexOfScalar"); 88 | } 89 | 90 | const Context = struct{ 91 | input: []const u8, 92 | }; 93 | 94 | fn indexOfScalar(ctx: Context, _: Allocator, _: *std.time.Timer) !void { 95 | const i = std.mem.indexOfScalar(u8, ctx.input, '9').?; 96 | if (i != 35) { 97 | @panic("fail"); 98 | } 99 | } 100 | 101 | fn lastIndexOfScalar(ctx: Context, _: Allocator, _: *std.time.Timer) !void { 102 | const i = std.mem.lastIndexOfScalar(u8, ctx.input, 'a').?; 103 | if (i != 0) { 104 | @panic("fail"); 105 | } 106 | } 107 | {% endhighlight %} 108 | 109 |

110 |

A zul.benchmark.Result(sample_size) is returned by the call to run or runC.

111 | 112 |

113 |
114 |
115 | total: u64 116 |
117 |

Total time, in nanosecond, that the benchmark ran for. This can be greater than the sum of values in samples().

118 |
119 |
120 |
121 | iterations: u64 122 |
123 |

Number of times the benchmarked function was called. This can be greater than samples().len.

124 |
125 |
126 |
127 | requested_bytes: usize 128 |
129 |

Total number of bytes allocated by the allocator.

130 |
131 |
132 |
133 | 134 |

135 |
136 |
137 | 138 |
139 |

Outputs a summary to stderr (using std.debug.print)

140 |
141 |
142 |
143 | 144 |
145 |

Returns a sorted list of sample. The value is the time each sample took in nanoseconds.

146 |
147 |
148 |
149 | 150 |
151 |

Returns the worst (slowest) sample time.

152 |
153 |
154 |
155 | 156 |
157 |

Returns the mean of samples().

158 |
159 |
160 |
161 | 162 |
163 |

Returns the median of samples().

164 |
165 |
166 |
167 | 168 |
169 |

Returns the stdDev of samples().

170 |
171 |
172 |
173 | -------------------------------------------------------------------------------- /docs/src/command_line_args.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: site.njk 3 | title: zul.CommandLineArgs 4 | --- 5 | 6 |

7 |

A simple command line parser.

8 | 9 |

10 | {% highlight zig %} 11 | var args = try zul.CommandLineArgs.parse(allocator); 12 | defer args.deinit(); 13 | 14 | if (args.contains("version")) { 15 | //todo: print the version 16 | os.exit(0); 17 | } 18 | 19 | // Values retrieved from args.get are valid until args.deinit() 20 | // is called. Dupe the value if needed. 21 | const host = args.get("host") orelse "127.0.0.1"; 22 | ... 23 | {% endhighlight %} 24 | 25 |

26 |

zul.CommandLineArgs is a thin wrapper around std.process.argsWithAllocator which applies simple logic to parse key=value pairs into a StringHashMap([]const u8.

27 | 28 |

7 argument types are supported:

29 | 30 |
    31 |
  1. --key value 32 |
  2. -k value 33 |
  3. --key=value 34 |
  4. -k=value 35 |
  5. --key 36 |
  6. -k 37 |
  7. -xvf value 38 |
39 | 40 |

A key without a value, such as ./run --version or ./run -v will be given a empty string value ("").

41 | 42 |

Parsing is simple. Keys begin with one or two dashes. If the parser sees ./run --key1 --key2 value2, it will load "key1" with a value of "", and "key2" with a value of "value2".

43 | 44 |

Single-dash keys can be grouped together, with the last key being given the value, so parameters like ./run -xvf file.tar.gz work like you (probably) expect.

45 | 46 |

Once the parser runs into a non key+value combo, all following arguments are treated as a "tail", which is a list of []const u8

47 | 48 |

49 |
50 |
51 | exe: []const u8 52 |
53 |

The first command line argument is the path to the running executable.

54 |
55 |
56 |
57 | tail: [][]const u8 58 |
59 |

A list of arguments starting from the first non key+value pair.

60 |
61 |
62 |
63 | list: [][]const u8 64 |
65 |

A list of arguments. The first argument in the list is the path to the running executable. This is an exact collection of std.process.argsWithAllocator.

66 |
67 |
68 |
69 | 70 |

71 |
72 |
73 | 74 |
75 |

Parses the command line arguments This can only fail in exceptional cases (e.g. out of memory).

76 |
77 |
78 |
79 | 80 |
81 |

Releases all memory related to the parsing. This includes any string returned by get or in the list or tail slice.

82 |
83 |
84 |
85 | 86 |
87 |

Gets the value associated with the key. The returned value's lifetime is tied to the CommandLineArg.

88 |
89 |
90 |
91 | 92 |
93 |

Whether or not the key was present.

94 |
95 |
96 |
97 | 98 |
99 |

The number of key value pairs. args.count() + args.tail.len + 1 == args.list.len. The + 1 is for the path to the running executable which is included in list.

100 |
101 |
102 |
103 | -------------------------------------------------------------------------------- /docs/src/datetime.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: site.njk 3 | title: zul.DateTime 4 | --- 5 | 6 |

7 |

Simple (no leap seconds, UTC-only), DateTime, Date and Time types.

8 | 9 |

10 | {% highlight zig %} 11 | // Currently only supports RFC3339 12 | const dt = try zul.DateTime.parse("2028-11-05T23:29:10Z", .rfc3339); 13 | const next_week = try dt.add(7, .days); 14 | std.debug.assert(next_week.order(dt) == .gt); 15 | 16 | // 1857079750000 == 2028-11-05T23:29:10Z 17 | std.debug.print("{d} == {s}", .{dt.unix(.milliseconds), dt}); 18 | {% endhighlight %} 19 | 20 |

21 |

zul.DateTime is a very basic DateTime struct designed for the main purpose of allowing RFC3339 dates to be read and written. It is simply an i64 representing the number of microseconds since unix epoch (Jan 1 1970). It supports valus ranging from Jan 1, -4712 to Dec 31, 9999. It does not support leap seconds and only supports UTC timezones.

22 | 23 |

When serialized and parsed with JSON, or formatted using std.fmt, the RFC3339 representation is used.

24 | 25 |

26 |
27 |
28 | micros: i64 29 |
30 |

Microseconds since unix epoch. A negative value indicates a DateTime before Jan 1, 1970.

31 |
32 |
33 |
34 | 35 |

36 |
37 |
38 | 39 | {% highlight zig %} 40 | fn initUTC( 41 | year: i16, 42 | month: u8, 43 | day: u8, 44 | hour: u8, 45 | min: u8, 46 | sec: u8, 47 | micros: u32 48 | ) !DateTime 49 | {% endhighlight %} 50 | 51 |
52 |

Creates a DateTime. This will fail if the date is outside of the supported range (i.e. year < -4712 or year 9999), or if the date is invalid (e.g. Nov 31).

53 |
54 |
55 |
56 | 57 | 58 |
59 |

Creates a DateTime. precision can be one of: .seconds, 60 | .milliseconds or .microseconds.

61 |
62 |
63 |
64 | 65 |
66 |

Creates a DateTime for the current date and time.

67 |
68 |
69 |
70 | 71 |
72 |

Parses the string representation of a date + time. Currently, the only supported format is .rfc3339. Trying to parse a non-UTC date will result in an error.

73 |
74 |
75 |
76 | 77 |
78 |

Creates a new DateTime. unit can be one of .microseconds, .milliseconds, .seconds, .minutes, .hours or .days.

79 |
80 |
81 |
82 | 83 |
84 |

Returns the Date portion of the DateTime.

85 |
86 |
87 |
88 | 89 |
90 |

Returns the Time portion of the DateTime.

91 |
92 |
93 |
94 | 95 |
96 |

Returns number of .seconds, .milliseconds or .microseconds since unix epoch.

97 |
98 |
99 |
100 | 101 |
102 |

Compares two values.

103 |
104 |
105 |
106 | 107 |

108 |

zul.Date represents a year, month and day. It is serialized and parsed to JSON or using std.fmt using the YYYY-MM-DD format (i.e. RFC3339).

109 | 110 |

111 |
112 |
113 | year: i16 114 |
115 |
116 | month: u8 117 |
1-based (i.e. Jan == 1)
118 |
119 |
120 | day: u8 121 |
122 |
123 | 124 |

125 |
126 |
127 | 128 |
129 |

Creates a Date. This will fail if the date is invalid (e.g. Nov 31).

130 |
131 |
132 |
133 | 134 |
135 |

Returns true if the date is valid, false otherwise.

136 |
137 |
138 |
139 | 140 |
141 |

Parses the string representation of a date. Supported formats are .rfc3339 and .iso8601.

142 |
143 |
144 |
145 | 146 |
147 |

Compares two values.

148 |
149 |
150 |
151 | 152 |

153 |

zul.Time represents a hour, minute, second and microsecond. It is serialized and parsed to JSON or using std.fmt using the HH:MM:SS[.000000] format (i.e. RFC3339).

154 | 155 |

156 |
157 |
158 | hour: u8 159 |
160 |
161 | min: u8 162 |
163 |
164 | sec: u8 165 |
166 |
167 | micros: u32 168 |
169 |
170 | 171 |

172 |
173 |
174 | 175 |
176 |

Creates a Time. This will fail if the time is invalid (e.g. min == 62).

177 |
178 |
179 |
180 | 181 |
182 |

Returns true if the time is valid, false otherwise.

183 |
184 |
185 |
186 | 187 |
188 |

Parses the string representation of a time. Currently, the only supported format is .rfc3339.

189 |
190 |
191 |
192 | 193 |
194 |

Compares two values.

195 |
196 |
197 |
198 | -------------------------------------------------------------------------------- /docs/src/fs/readdir.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: site.njk 3 | title: zul.fs.readDir 4 | --- 5 | 6 |

7 |

Iterates, non-recursively, through a directory.

8 | 9 |

This is a thin abstraction over the standard libraries IterableDir.iterate() behavior. The main benefit of zul.fs.readDir is the ability collect all entries, sorted or not, in a slice.

10 | 11 |

12 | {% highlight zig %} 13 | // Parameters: 14 | // 1- Absolute or relative directory path 15 | var it = try zul.fs.readDir("/tmp/dir"); 16 | defer it.deinit(); 17 | 18 | // can iterate through the files 19 | while (try it.next()) |entry| { 20 | std.debug.print("{s} {any}\n", .{entry.name, entry.kind}); 21 | } 22 | 23 | // reset the iterator 24 | it.reset(); 25 | 26 | // or can collect them into a slice, optionally sorted: 27 | const sorted_entries = try it.all(allocator, .dir_first); 28 | for (sorted_entries) |entry| { 29 | std.debug.print("{s} {any}\n", .{entry.name, entry.kind}); 30 | } 31 | {% endhighlight %} 32 | 33 |

34 |
35 |
36 | path: []const u8 37 |

Absolute or relative path to the directory.

38 |
39 |
40 | 41 |

42 |

On success, readDir returns an Iterator.

43 | 44 |

45 |
46 |
47 | 48 |
49 |

Releases the underlying operating system resources. Frees memory allocated by all.

50 |
51 |
52 |
53 | 54 |
55 |

Gets all remaining directory entries. sort can be one of four values:

56 |
    57 |
  • none - no sorting. 58 |
  • alphabetic - sorted alphabetically. 59 |
  • dir_first - sorted alphabetically with directories first. 60 |
  • dir_last - sorted alphabetically with directories last. 61 |
62 | 63 |

Normally, the entry.name is only valid up until the next iteration. In order to collect all entries, this function clones the names. Internally this, along with the std.ArrayList used to collect the entries, are managed by an ArenaAllocator, which is cleared on deinit. Compared to simply iterating through the entries one at a time, this adds considerable overhead. But, if you need all entries, sorted or otherwise, this cloning is necessary. If you don't, prefer using the standard libraries std.fs.IterableDir directly.

64 |
65 |
66 |
67 | 68 |
69 |

Returns the next directory entry, or null if there are no more entries.

70 | 71 |

The returned entry is only valid until the next call to next, deinit or reset.

72 | 73 |

The order of iteration depends on the file system, but generally no guarantee is made. Whether or not entries added/removed during iteration are seen by the iterator depends also depends on the file system.

74 |
75 |
76 |
77 | 78 |
79 |

Resets the iterator. Once reset, the iterator can be iterated again from the start.

80 |
81 |
82 |
83 | 84 |

85 |

Entry is an std.fs.IterableDir.Entry which has two fields: name: []const u8 and kind: std.fs.File.Kind.

86 | -------------------------------------------------------------------------------- /docs/src/fs/readjson.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: site.njk 3 | title: zul.fs.readJson 4 | --- 5 | 6 |

7 |

Reads and parses a JSON file.

8 | 9 |

10 | {% highlight zig %} 11 | // Parameters: 12 | // 1- The type to parse the JSON data into 13 | // 2- An allocator 14 | // 3- Absolute or relative path 15 | // 4- std.json.ParseOptions 16 | const managed_user = try zul.fs.readJson(User, allocator, "/tmp/data.json", .{}); 17 | 18 | // readJson returns a zul.Managed(T) 19 | // managed_user.value is valid until managed_user.deinit() is called 20 | defer managed_user.deinit(); 21 | const user = managed_user.value; 22 | {% endhighlight %} 23 | 24 |

25 |
26 |
27 | comptime T: type 28 |

The type to parse to.

29 |
30 |
31 | allocator: std.mem.Allocator 32 |

The allocator to use for any memory allocations needed to parse the JSON or create T.

33 |
34 |
35 | path: []const u8 36 |

Absolute or relative path to the file.

37 |
38 |
39 | opts: std.json.ParseOptions 40 |
41 |

Options that control the parsing. The allocate field will be forced to alloc_always.

42 |
43 |
44 |
45 | 46 |

47 |

On success, readJson(T, ...) returns a zul.Managed(T) which exposes a deinit() method as well as the the parsed value in the value: T field.

48 | 49 |

Parsing JSON and creating T likely requires memory allocations. These allocations are done within an std.heap.ArenaAllocator. Thus, the parsed value: T has a lifetime tied to the arena. When zul.Manage(T).deinit() is called, the arena is cleared and freed.

50 | 51 |

zul.Manage is a renamed std.json.Parsed(T) (I dislike the name std.json.Parsed(T) because it represents data and behavior that has nothing to with with JSON or parsing).

52 | -------------------------------------------------------------------------------- /docs/src/fs/readlines.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: site.njk 3 | title: zul.fs.readLines 4 | --- 5 | 6 |

7 |

Iterate over the lines in a file.

8 | 9 |

10 | {% highlight zig %} 11 | // create a buffer large enough to hold the longest valid line 12 | var line_buffer: [1024]u8 = undefined; 13 | 14 | // Parameters: 15 | // 1- an absolute or relative path to the file 16 | // 2- the line buffer 17 | // 3- options (here we're using the default) 18 | var it = try zul.fs.readLines("/tmp/data.txt", &line_buffer, .{}); 19 | defer it.deinit(); 20 | 21 | while (try it.next()) |line| { 22 | // line is only valid until the next call to 23 | // it.next() or it.deinit() 24 | std.debug.print("line: {s}\n", .{line}); 25 | } 26 | {% endhighlight %} 27 | 28 |

29 |
30 |
31 | path: []const u8 32 |

Absolute or relative path to the file.

33 |
34 |
35 | buf: []const u8 36 |
37 |

Buffer to write the line into, the buffer length represents the maximum allowed line. If a line is longer than buf.len next() will return error.StreamTooLong.

38 |
39 |
40 | opts: zul.fs.LineIterator.Opts 41 |
42 |

Options that control the iterator.

43 |
    44 |
  • delimiter: u8 - The delimiter to split on, defaults to '\n' 45 |
  • open_flags: std.fs.File.OpenFlags - Flags to pass to the underlying std openFile call. 46 |
47 |
48 |
49 |
50 | 51 |

52 |

On success, readLines returns this a LineIterator.

53 | 54 |

55 |
56 |
57 | 58 |
59 |

Releases the resources associated with the iterator (i.e. it closes the file). This must be called the the iterator is no longer needed.

60 |
61 |
62 |
63 | 64 |
65 |

Returns the next line, or null if the the end of the file has been reached . The return value is only valid until the next call to next or deinit.

66 | 67 |

The return []u8 value may be a slice of the buf parameter passed to readLines or it may be a slice of the internal buffer used for reading the file. In either case the lifetime is the same. When possible, a slice to the internal buffer is used to avoid a copy.

68 |
69 |
70 |
71 | 72 |

73 |

Due to issue 17985, readLines should performance considerably better than the typical std solution involving a std.io.File wrapped in a std.io.BufferedReader exposed as an std.io.Reader.

74 | 75 |

76 |

By default, readLines will read from the file using a 4096 byte buffer. A different size can be specified via the readLinesSize function:

77 | 78 | {% highlight zig %} 79 | var it = try zul.fs.readlinesSized(8192, "/tmp/data.txt", &line_buffer, .{}); 80 | {% endhighlight %} 81 | 82 |

The size must be comptime-known (i.e, a constant). The rest of the API is exactly the same.

83 | -------------------------------------------------------------------------------- /docs/src/http/client.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: site.njk 3 | title: zul.http.Client 4 | --- 5 | 6 |

7 |

A wrapper around std.http.Client to make it easier to create requests and consume responses.

8 | 9 |

10 | {% highlight zig %} 11 | // The client is thread-safe 12 | var client = zul.http.Client.init(allocator); 13 | defer client.deinit(); 14 | 15 | // Not thread safe, method defaults to .GET 16 | var req = try client.request("https://api.github.com/search/topics"); 17 | defer req.deinit(); 18 | 19 | // Set the querystring, can also be set in the URL passed to client.request 20 | // or a mix of setting in client.request and programmatically via req.query 21 | try req.query("q", "zig"); 22 | 23 | try req.header("Authorization", "Your Token"); 24 | 25 | // The lifetime of res is tied to req 26 | var res = try req.getResponse(.{}); 27 | if (res.status != 200) { 28 | // TODO: handle error 29 | return; 30 | } 31 | 32 | // On success, this is a zul.Managed(SearchResult), its lifetime is detached 33 | // from the req, allowing it to outlive req. 34 | const managed = try res.json(SearchResult, allocator, .{}); 35 | 36 | // Parsing the JSON and creating SearchResult [probably] required some allocations. 37 | // Internally an arena was created to manage this from the allocator passed to 38 | // res.json. 39 | defer managed.deinit(); 40 | 41 | const search_result = managed.value; 42 | {% endhighlight %} 43 | 44 |

45 |

zul.http.Client is wrapper around std.http.Client. Is is thread-safe and its only purpose is to create zul.http.Request objects.

46 | 47 |

48 |
49 |
50 | client: std.http.Client 51 |
52 |

Should only be used if tweaks to the underlying std.http.Client are needed.

53 |
54 |
55 |
56 | 57 |

58 |
59 |
60 | 61 |
62 |

Creates a Client.

63 |
64 |
65 |
66 | 67 |
68 |

Releases all memory associated with the client. The client should not be used after this is called.

69 |
70 |
71 |
72 | 73 |
74 |

Creates a Request object, using the provided url. The Request will use the Client's allocator. If a querystring is provided as part of the url, it must be properly encoded. Use query(name, value) to add or append a querystring which the library will encode.

75 |
76 |
77 |
78 | 79 | {% highlight zig %} 80 | fn allocRequest( 81 | self: *Client, 82 | 83 | // With the plain request() method, the client's allocator is used. A different 84 | // allocator can be used with this variant. 85 | allocator: Allocator, 86 | 87 | url: []const u8, 88 | 89 | ) !Request 90 | {% endhighlight %} 91 |
92 |

Creates a Request object, using the provided url. The Request will use the provided Allocator.

93 |
94 |
95 |
96 | 97 |

98 |

zul.http.Request is used to build the request (querystring, headers, body) and issue the request to get a Response. A Request is not thread-safe. To get a request, use the client.request method.

99 | 100 |

101 |
102 |
103 | headers: std.http.Headers 104 |
105 |

Gives direct access to the request headers. To add headers, prefer using the header method.

106 |
107 |
108 |
109 | method: std.http.Method 110 |
111 |

Defaults to .GET.

112 |
113 |
114 |
115 | url: zul.StringBuilder 116 |
117 |

Gives direct access to the URL. For manipulating the querystring, prefer using the query method.

118 |
119 |
120 |
121 | 122 |

123 |
124 |
125 | 126 |
127 |

Releases all memory associated with the request as well as any generated response.

128 |
129 |
130 |
131 | 132 |
133 |

Sets the request body to the given value.

134 |
135 |
136 |
137 | 138 |
139 |

Builds a URL encoded body. Can be called multiple times. The first call will se the Content-Typeheader to application/x-www-form-urlencoded.

140 |
141 |
142 |
143 | 144 |
145 |

Adds the given name-value pair to the request headers.

146 |
147 |
148 |
149 | 150 |
151 |

Appends the given name-value pair to the request's querystring. Both the name and value will be automatically encoded if needed. The querystring can also be set when first creating the request as part of the specified URL. It is allowed to set some querystring values via the original URLs (which must be encoded by the caller) and others via this method.

152 |
153 |
154 |
155 | 156 |
157 |

Will send the contents of the file as the body of the request. file_path can be absolute or relative.

158 |
159 |
160 |
161 | 162 | {% highlight zig %} 163 | fn getResponse( 164 | req: *Request, 165 | 166 | opts: .{ 167 | // whether or not to parse the respons headers 168 | .response_headers: bool = true, 169 | 170 | .write_progress_state: *anyopaque = undefined, 171 | 172 | .write_progress: ?*const fn(total: usize, written: usize, state: *anyopaque) void = null, 173 | } 174 | ) !Response 175 | {% endhighlight %} 176 |
177 |

Issues the requests and, on success, returns the Response.

178 | 179 |

The write_progress option field is a callback that will be called as the file body is uploaded. An optional state can be specified via the write_progress_state the option field which is passed into the callback.

180 | 181 | {% highlight zig %} 182 | var res = try req.getResponse(.{ 183 | .write_progress = uploadProgress 184 | }); 185 | 186 | // ... 187 | 188 | fn uploadProgress(total: usize, written: usize, state: *anyopaque) void { 189 | // It is an undefined behavior to try to access the state 190 | // when `write_progress_state` was not specified. 191 | _ = state; 192 | 193 | std.fmt.print("Written {d} of {d}", {written, total}); 194 | } 195 | {% endhighlight %} 196 | 197 |

Or, with state:

198 | 199 | {% highlight zig %} 200 | // ProgressTracker can be anything, it's specific to your app 201 | var tracker = ProgressTracker{}; 202 | 203 | var res = try req.getResponse(.{ 204 | .write_progress = uploadProgress 205 | .write_progress_state = &tracker, 206 | }); 207 | 208 | // ... 209 | 210 | fn uploadProgress(total: usize, written: usize, state: *anyopaque) void { 211 | var tracker: *ProgressTracker = @alignCast(@ptrCast(state)); 212 | // use tracker however you want, it's your class! 213 | } 214 | {% endhighlight %} 215 |
216 |
217 |
218 | 219 |

220 |

zul.http.Response lifetime is tied to the initiating request. Therefore, it has no deinit method. When request.deinit is called, the response is no longer valid. Note however that the methods for reading the body detach the body from this lifetime.

221 | 222 |

223 |
224 |
225 | headers: std.StringHashMap([]const u8) 226 |
227 |

The response headrs. Only populated if the response_headers option is specified in getResponse (this option defaults to true).

228 |
229 |
230 |
231 | req: *std.http.Client.Request 232 |
233 |

The underlying request.

234 |
235 |
236 |
237 | res: *std.http.Client.Response 238 |
239 |

The underlying response.

240 |
241 |
242 |
243 | status: u16 244 |
245 |

The response's HTTP status code.

246 |
247 |
248 |
249 | 250 |

251 |
252 |
253 | 254 |
255 |

Returns the value associated with the given header name, if any. The name is lower-case. Only populated if the response_headers option is specified in getResponse (this option defaults to true).

256 |
257 |
258 |
259 | 260 |
261 |

Returns an iterator to iterate over values of a given header. Useful for headers which may appear multiple times (i.e. set-cookie).

262 |

263 | {% highlight zig %} 264 | var it = res.headerIterator("set-cookie"); 265 | while (it.next()) |value| { 266 | // ... value 267 | } 268 | {% endhighlight %}

269 |

To iterate over all values, use the headerIterator of the underlying *std.http.Client.Response, i.e.: var it = res.res.headerIterator()

270 |
271 |
272 |
273 | 274 | {% highlight zig %} 275 | fn json( 276 | self: Response, 277 | 278 | // The type to parse into 279 | comptime T: type, 280 | 281 | // An arena allocator will be created from this allocator for any memory needed 282 | // to parse the JSON and create T 283 | allocator: std.mem.Allocator, 284 | 285 | // Consider setting ignore_unknown_fields = true 286 | // and the max_value_len 287 | opts: std.json.ParseOptions 288 | 289 | ) !zul.Managed(T) 290 | {% endhighlight %} 291 |
292 |

Attempts to parse the body as JSON. On success, the returned object has its own lifetime, independent of the initiating request or response.

293 | 294 |

zul.Manage is a renamed std.json.Parsed(T) (I dislike the name std.json.Parsed(T) because it represents data and behavior that has nothing to with with JSON or parsing).

295 |
296 |
297 |
298 | 299 | {% highlight zig %} 300 | fn allocBody ( 301 | self: *Response, 302 | 303 | // Allocator will be used to create the []const u8 that will hold the body 304 | // If the response has a content-length, then exactly $content_length bytes will 305 | // be allocated 306 | allocator: std.mem.Allocator, 307 | 308 | // {.max_size = usize, .buffer_size: usize} 309 | opts: zul.StringBuilder.FromReaderOpts, 310 | 311 | ) !zul.StringBuilder 312 | {% endhighlight %} 313 |
314 |

Reads the body into a zul.StringBuilder. Consider setting the max_size field of opts to a reasonable value.

315 | 316 |

This method returns a zul.StringBuilder to support chunked-encoding responses where the length isn't known ahead of time and a growable buffer is needed. In such cases, a correctly sized []const u8 cannot be returned without doing an additional copy. zul.StringBuilder is preferred over std.ArrayList(u8) because of its more efficient ability to read from a std.io.Reader.

317 |
318 |
319 |
320 | -------------------------------------------------------------------------------- /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 | 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 | 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 |
94 | 95 |
96 |

This is method thread-safe.

97 |
98 |
99 |
100 | 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 |
106 | 107 |
108 |

Releases *T back into the pool. If the pool is full, t.deinit() is called and then discarded.

109 |
110 |
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 |
61 | 62 |
63 |

Run when a task is ready to be executed. ctx is the arbitrary data passed to start. at is the timestamp, in millisecond, that the task was supposed to run at.

64 |
65 | 66 | 67 |
68 |

Same as above, but receives an additional parameter: the scheduler itself. This can be useful if you want to have recurring tasks: run can re-schedule the task.

69 |
70 |
71 | 72 |

73 |
74 |
75 | 76 |
77 |

Creates a Scheduler.

78 |
79 |
80 |
81 | 82 |
83 |

Stops the task scheduler thread (if it was started) and deallocates the scheduler. This method is thread-safe.

84 |
85 |
86 |
87 | 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 | 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 |
101 | 102 |
103 |

Schedules a task to be run at the specified time. The time is given as a unix timestamp in milliseconds. The time can be in the past.

104 |
105 |
106 |
107 | 108 |
109 |

Schedules a task to be run in the specified milliseconds from now. This is the same as calling schedule with std.time.milliTimestamp() + ms.

110 |
111 |
112 |
113 | 114 |
115 |

Schedules a task to be run at the specified DateTime.

116 |
117 |
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 |
53 | 54 |
55 |

Creates a StringBuilder.

56 |
57 |
58 |
59 | 60 |
61 |

Releases all allocated memory. The StringBuilder should not be used after this is called.

62 |
63 |
64 |
65 | 66 |
67 |

Resets the StringBuilder's length to 0 without freeing the underlying buffer. It is safe to re-use the StringBuilder.

68 |
69 |
70 |
71 | 72 |
73 |

Creates a copy of the written data using the provided allocator.

74 |
75 |
76 |
77 | 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 | 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 | 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 |
128 | 129 |
130 |

Returns the length of the written data.

131 |
132 |
133 |
134 | 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 | 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 | 148 |
149 |

Returns the bytes written. Subsequent modifications to the StringBuilder may invalidate the returned data

150 |
151 |
152 |
153 | 154 |
155 |

Returns the bytes written as a null-terminated string. Subsequent modifications to the StringBuilder may invalidate the returned data

156 |
157 |
158 |
159 | 160 |
161 |

Truncates the data by n bytes. If n is greater than the length of the buffer, the length is set to 0. Does not free memory.

162 |
163 |
164 |
165 | 166 |
167 |

Appends data, growing the underlying buffer if needed

168 |
169 |
170 |
171 | 172 |
173 |

Appends data assuming that the underlying buffer has enough free space. This method is slightly faster than write but is unsafe and should only be used in conjunction with ensureUnusedCapacity.

174 |
175 |
176 |
177 | 178 |
179 |

Appends the byte, growing the underlying buffer if needed

180 |
181 |
182 |
183 | 184 |
185 |

Appends the byte assuming that the underlying buffer has enough free space. This method is slightly faster than writeByte but is unsafe and should only be used in conjunction with ensureUnusedCapacity.

186 |
187 |
188 |
189 | 190 |
191 |

Appends the byte n times, growing the underlying buffer if needed.

192 |
193 |
194 |
195 | 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 |
207 | 208 |
209 |

Writes an unsigned integer using the encoding defined by sb.endian.

210 |

Variants: writeU32 and writeU64

211 |
212 |
213 |
214 | 215 |
216 |

Writes an signed integer using the encoding defined by sb.endian.

217 |

Variants: writeI32 and writeI64

218 |
219 |
220 |
221 | 222 |
223 |

Writes an unsigned integer using little endian encoding.

224 |

Variants: writeU32Little and writeU64Little

225 |
226 |
227 |
228 | 229 |
230 |

Writes an signed integer using little endian encoding.

231 |

Variants: writeI32Little and writeI64Little

232 |
233 |
234 |
235 | 236 |
237 |

Writes an unsigned integer using big endian encoding.

238 |

Variants: writeU32Big and writeU64Big

239 |
240 |
241 |
242 | 243 |
244 |

Writes an signed integer using big endian encoding.

245 |

Variants: writeI32Big and writeI64Big

246 |
247 |
248 |
249 | 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 |
256 | 257 |
258 |

Writes an integer using the specified endianness. Type type of value determines how the value is encoded.

259 |

It is a compile-time error to pass a comptime_int. You must cast the constant.

260 |
261 |
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 |
290 | 291 |
292 |

Creates a StringBuilder.Pool. The pool will have pool_size StringBuilders, and each StringBuilder will maintain a static buffer of static_size bytes.

293 |
294 |
295 |
296 | 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 | 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 | 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 |
87 | 88 |
89 |

Asserts that expected is within delta of actual. Unlike the std.testing.expectApproxEq* functions, expectDelta works with both integers and floats. 90 |

91 |
92 |
93 | 94 |
95 |

Similar to std.testing.expectEqual, but will coerce expected to actual and can be used to compare strings.

96 |
97 |
98 |
99 | 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 | 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 | 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 | 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 |
149 | 150 |
151 |

Returns an integer of type T that is between min and max inclusive. This is a wrapper around std.RandomintRangeAtMost.

152 |
153 |
154 |
155 | 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 | 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 |
55 | 56 |
57 |

This is method thread-safe. The threads will be stopped and cleaned up.

58 |
59 |
60 |
61 | 62 |
63 |

Enqueues the arguments to be processed by Fn in a worker thread. This call blocks if the number of pending jobs has reached its configured backlog.

64 |
65 |
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 | 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 |
79 | 80 |
81 |

Hex-encodes the UUID into a heap-allocated buffer. The caller must free the returned value when it is no longer in use.

82 |
83 |
84 |
85 | 86 |
87 |

Hex-encodes the UUID into buf, which must have a length equal or greater than 36.

88 |
89 |
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 | --------------------------------------------------------------------------------