tools

various tools I have been using throughout the years
Log | Files | Refs | README | LICENSE

commit ca38ee2b26db7caaae46c8293277af836e493786
parent 62d17e4ba1e58d38398a5c57859f8251e735e9c3
Author: mtmn <miro@haravara.org>
Date:   Tue,  9 Jun 2026 22:24:41 +0200

speediness: add cli flags and justfile

Diffstat:
Mspeediness/.gitignore | 1+
Mspeediness/README.md | 30++++++++++++++++++++++++------
Aspeediness/justfile | 12++++++++++++
Mspeediness/src/main.zig | 125++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
4 files changed, 149 insertions(+), 19 deletions(-)

diff --git a/speediness/.gitignore b/speediness/.gitignore @@ -1,2 +1,3 @@ +build zig-out .zig-cache diff --git a/speediness/README.md b/speediness/README.md @@ -1,6 +1,6 @@ # speediness -A network speed test. Downloads 100 MB from OVH and uploads 20 MB to Cloudflare, printing live throughput and a final summary for each. +A network speed test. Downloads 100 MB from OVH and uploads 50 MB to Cloudflare, printing live throughput and a final summary for each. ## Building @@ -10,16 +10,34 @@ zig build ## Usage +Running without arguments prints the help: + ```sh speediness ``` -Output example: +Pass one or more options to run the test. Each unspecified option falls back to +its default, so the original behaviour is `--upload-mb 50` with the default URLs: +```sh +speediness --download-url <url> --upload-url <url> --upload-mb <n> ``` - 100.0 MB 11.23 MB/s 89.8 Mbps -100.0 MB in 8.90s => 11.23 MB/s (89.8 Mbps) - 20.0 MB 47.81 MB/s 382.5 Mbps -20.0 MB in 0.42s => 47.81 MB/s (382.5 Mbps) +| Option | Description | Default | +| ---------------- | --------------------- | ---------------------------------------- | +| `--download-url` | URL to download from | `https://proof.ovh.net/files/100Mb.dat` | +| `--upload-url` | URL to upload to | `https://speed.cloudflare.com/__upload` | +| `--upload-mb` | Megabytes to upload | `50` | +| `--both` | Run both tests | | +| `--download-only` | Run the download test only | | +| `--upload-only` | Run the upload test only | | +| `-h`, `--help` | Show help and exit | | + +Output example: + +``` + 104.9 MB 4.18 MB/s 33.4 Mbps +104.9 MB in 23.92s => 4.18 MB/s (33.4 Mbps) + 52.4 MB 2.22 MB/s 17.7 Mbps +52.4 MB in 22.57s => 2.22 MB/s (17.7 Mbps) ``` diff --git a/speediness/justfile b/speediness/justfile @@ -0,0 +1,12 @@ +run opt="Debug": + zig build run -Doptimize={{opt}} + +build opt="Debug": + zig build -Doptimize={{opt}} -p build/{{opt}} + +freebsd target="x86_64-freebsd": + zig build -Doptimize=ReleaseSafe -Dtarget={{target}} -p build/{{target}} + +openbsd target="x86_64-openbsd": + zig build -Doptimize=ReleaseSafe -Dtarget={{target}} -p build/{{target}} + diff --git a/speediness/src/main.zig b/speediness/src/main.zig @@ -1,14 +1,42 @@ const std = @import("std"); +const default_download_url = "https://proof.ovh.net/files/100Mb.dat"; +const default_upload_url = "https://speed.cloudflare.com/__upload"; +const default_upload_mb: usize = 50; + +const Config = struct { + download_url: []const u8 = default_download_url, + upload_url: []const u8 = default_upload_url, + upload_bytes: usize = default_upload_mb * 1024 * 1024, + run_download: bool = true, + run_upload: bool = true, +}; + +const usage = + \\Usage: speediness [options] + \\ + \\Options: + \\ --download-url <url> URL to download from (default: {s}) + \\ --upload-url <url> URL to upload to (default: {s}) + \\ --upload-mb <n> Megabytes to upload (default: {d}) + \\ --both Run both tests + \\ --download-only Run the download test only + \\ --upload-only Run the upload test only + \\ -h, --help Show this help and exit + \\ +; + +fn printUsage() void { + std.debug.print(usage, .{ default_download_url, default_upload_url, default_upload_mb }); +} + fn mbps(bytes: usize, ns: u64) f64 { const secs = @as(f64, @floatFromInt(ns)) / 1e9; if (secs < 0.001) return 0; return (@as(f64, @floatFromInt(bytes)) / secs) / (1024.0 * 1024.0); } -fn downloadTest(io: std.Io, allocator: std.mem.Allocator) !void { - const url = "https://proof.ovh.net/files/100Mb.dat"; - +fn downloadTest(io: std.Io, allocator: std.mem.Allocator, url: []const u8) !void { var client: std.http.Client = .{ .allocator = allocator, .io = io, @@ -68,10 +96,7 @@ fn downloadTest(io: std.Io, allocator: std.mem.Allocator) !void { }); } -fn uploadTest(io: std.Io, allocator: std.mem.Allocator) !void { - const url = "https://speed.cloudflare.com/__upload"; - const upload_bytes: usize = 20 * 1024 * 1024; - +fn uploadTest(io: std.Io, allocator: std.mem.Allocator, url: []const u8, upload_bytes: usize) !void { var client: std.http.Client = .{ .allocator = allocator, .io = io, @@ -132,14 +157,88 @@ fn uploadTest(io: std.Io, allocator: std.mem.Allocator) !void { }); } +/// Returns the value for an option, taking it either from the rest of the +/// current argument after '=' or from the next argument. +fn optionValue(args: []const [:0]const u8, i: *usize, eq: ?usize) ![]const u8 { + if (eq) |pos| return args[i.*][pos + 1 ..]; + if (i.* + 1 >= args.len) { + std.debug.print("missing value for '{s}'\n", .{args[i.*]}); + return error.MissingValue; + } + i.* += 1; + return args[i.*]; +} + +fn parseArgs(args: []const [:0]const u8) !?Config { + // With no arguments, show usage instead of running the tests. + if (args.len <= 1) { + printUsage(); + return null; + } + + var cfg: Config = .{}; + + // Tracks which of the mutually exclusive mode flags has been seen. + var mode: ?[]const u8 = null; + + var i: usize = 1; + while (i < args.len) : (i += 1) { + const arg = args[i]; + + // Split "--flag=value" into name and value position. + const eq = std.mem.indexOfScalar(u8, arg, '='); + const name = if (eq) |pos| arg[0..pos] else arg; + + if (std.mem.eql(u8, name, "-h") or std.mem.eql(u8, name, "--help")) { + printUsage(); + return null; + } else if (std.mem.eql(u8, name, "--download-url")) { + cfg.download_url = try optionValue(args, &i, eq); + } else if (std.mem.eql(u8, name, "--upload-url")) { + cfg.upload_url = try optionValue(args, &i, eq); + } else if (std.mem.eql(u8, name, "--upload-mb")) { + const value = try optionValue(args, &i, eq); + const mb = std.fmt.parseInt(usize, value, 10) catch { + std.debug.print("invalid --upload-mb value: '{s}'\n", .{value}); + return error.InvalidArgument; + }; + cfg.upload_bytes = mb * 1024 * 1024; + } else if (std.mem.eql(u8, name, "--both") or + std.mem.eql(u8, name, "--download-only") or + std.mem.eql(u8, name, "--upload-only")) + { + if (mode) |prev| { + std.debug.print("{s} and {s} are mutually exclusive\n", .{ prev, name }); + return error.InvalidArgument; + } + mode = name; + cfg.run_download = !std.mem.eql(u8, name, "--upload-only"); + cfg.run_upload = !std.mem.eql(u8, name, "--download-only"); + } else { + std.debug.print("unknown argument: '{s}'\n", .{arg}); + printUsage(); + return error.UnknownArgument; + } + } + + return cfg; +} + pub fn main(init: std.process.Init) !void { const allocator = init.gpa; - downloadTest(init.io, allocator) catch |err| { - std.debug.print("Download error: {}\n", .{err}); - }; + const args = try init.minimal.args.toSlice(init.arena.allocator()); + const cfg = parseArgs(args) catch std.process.exit(2) orelse return; - uploadTest(init.io, allocator) catch |err| { - std.debug.print("Upload error: {}\n", .{err}); - }; + if (cfg.run_download) { + downloadTest(init.io, allocator, cfg.download_url) catch |err| { + std.debug.print("Download error: {}\n", .{err}); + }; + } + + if (cfg.run_upload) { + uploadTest(init.io, allocator, cfg.upload_url, cfg.upload_bytes) catch |err| { + std.debug.print("Upload error: {}\n", .{err}); + }; + } }