From 2bf37843f3310df06639947a25160c4bd108adcc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 20 Jan 2024 15:07:32 -0800 Subject: [PATCH] config: tests for all custom formatEntry calls --- src/config/Config.zig | 143 ++++++++++++++++++++++- src/config/formatter.zig | 239 ++++++++++++++++++++------------------- 2 files changed, 265 insertions(+), 117 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index b888c42fd..da46351dd 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -15,6 +15,7 @@ const internal_os = @import("../os/main.zig"); const cli = @import("../cli.zig"); const Command = @import("../Command.zig"); +const formatterpkg = @import("formatter.zig"); const url = @import("url.zig"); const Key = @import("key.zig").Key; const KeyValue = @import("key.zig").Value; @@ -2231,6 +2232,16 @@ pub const Color = packed struct(u24) { test "parseCLI from name" { try std.testing.expectEqual(Color{ .r = 0, .g = 0, .b = 0 }, try Color.parseCLI("black")); } + + test "formatConfig" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var color: Color = .{ .r = 10, .g = 11, .b = 12 }; + try color.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = #0a0b0c\n", buf.items); + } }; /// Palette is the 256 color palette for 256-color mode. This is still @@ -2295,6 +2306,16 @@ pub const Palette = struct { var p: Self = .{}; try testing.expectError(error.Overflow, p.parseCLI("256=#AABBCC")); } + + test "formatConfig" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var list: Self = .{}; + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = 0=#1d1f21\n", buf.items[0..14]); + } }; /// RepeatableString is a string value that can be repeated to accumulate @@ -2369,6 +2390,47 @@ pub const RepeatableString = struct { try list.parseCLI(alloc, ""); try testing.expectEqual(@as(usize, 0), list.list.items.len); } + + test "formatConfig empty" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var list: Self = .{}; + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = \n", buf.items); + } + + test "formatConfig single item" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "A"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = A\n", buf.items); + } + + test "formatConfig multiple items" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "A"); + try list.parseCLI(alloc, "B"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = A\na = B\n", buf.items); + } }; /// RepeatablePath is like repeatable string but represents a path value. @@ -2549,6 +2611,21 @@ pub const RepeatableFontVariation = struct { .value = -15, }, list.list.items[1]); } + + test "formatConfig single" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "wght = 200"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = wght=200\n", buf.items); + } }; /// Stores a set of keybinds. @@ -2660,6 +2737,21 @@ pub const Keybinds = struct { try set.parseCLI(alloc, "shift+a=copy_to_clipboard"); try set.parseCLI(alloc, "shift+a=csi:hello"); } + + test "formatConfig single" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Keybinds = .{}; + try list.parseCLI(alloc, "shift+a=csi:hello"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = shift+a=csi:hello\n", buf.items); + } }; /// See "font-codepoint-map" for documentation. @@ -2738,7 +2830,7 @@ pub const RepeatableCodepointMap = struct { []const u8, std.fmt.bufPrint( &buf, - "U+{X:0>4}-U{X:0>4}={s}", + "U+{X:0>4}-U+{X:0>4}={s}", .{ range[0], range[1], @@ -2883,6 +2975,55 @@ pub const RepeatableCodepointMap = struct { try testing.expectEqualStrings("Courier", entry.descriptor.family.?); } } + + test "formatConfig single" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "U+ABCD=Comic Sans"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = U+ABCD=Comic Sans\n", buf.items); + } + + test "formatConfig range" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "U+0001 - U+0005=Verdana"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = U+0001-U+0005=Verdana\n", buf.items); + } + + test "formatConfig multiple" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "U+0006-U+0009, U+ABCD=Courier"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, + \\a = U+0006-U+0009=Courier + \\a = U+ABCD=Courier + \\ + , buf.items); + } }; pub const FontStyle = union(enum) { diff --git a/src/config/formatter.zig b/src/config/formatter.zig index 26be9a816..e6a5887ad 100644 --- a/src/config/formatter.zig +++ b/src/config/formatter.zig @@ -1,6 +1,127 @@ +const formatter = @This(); const std = @import("std"); const Config = @import("Config.zig"); +/// Returns a single entry formatter for the given field name and writer. +pub fn entryFormatter( + name: []const u8, + writer: anytype, +) EntryFormatter(@TypeOf(writer)) { + return .{ .name = name, .writer = writer }; +} + +/// The entry formatter type for a given writer. +pub fn EntryFormatter(comptime WriterType: type) type { + return struct { + name: []const u8, + writer: WriterType, + + pub fn formatEntry( + self: @This(), + comptime T: type, + value: T, + ) !void { + return formatter.formatEntry( + T, + self.name, + value, + self.writer, + ); + } + }; +} + +/// Format a single type with the given name and value. +pub fn formatEntry( + comptime T: type, + name: []const u8, + value: T, + writer: anytype, +) !void { + switch (@typeInfo(T)) { + .Bool, .Int => { + try writer.print("{s} = {}\n", .{ name, value }); + return; + }, + + .Float => { + try writer.print("{s} = {d}\n", .{ name, value }); + return; + }, + + .Enum => { + try writer.print("{s} = {s}\n", .{ name, @tagName(value) }); + return; + }, + + .Void => { + try writer.print("{s} = \n", .{name}); + return; + }, + + .Optional => |info| { + if (value) |inner| { + try formatEntry( + info.child, + name, + inner, + writer, + ); + } else { + try writer.print("{s} = \n", .{name}); + } + + return; + }, + + .Pointer => switch (T) { + []const u8, + [:0]const u8, + => { + try writer.print("{s} = {s}\n", .{ name, value }); + return; + }, + + else => {}, + }, + + // Structs of all types require a "formatEntry" function + // to be defined which will be called to format the value. + // This is given the formatter in use so that they can + // call BACK to our formatEntry to write each primitive + // value. + .Struct => |info| if (@hasDecl(T, "formatEntry")) { + try value.formatEntry(entryFormatter(name, writer)); + return; + } else switch (info.layout) { + // Packed structs we special case. + .Packed => { + try writer.print("{s} = ", .{name}); + inline for (info.fields, 0..) |field, i| { + if (i > 0) try writer.print(",", .{}); + try writer.print("{s}{s}", .{ + if (!@field(value, field.name)) "no-" else "", + field.name, + }); + } + try writer.print("\n", .{}); + return; + }, + + else => {}, + }, + + // TODO + .Union => return, + + else => {}, + } + + // Compile error so that we can catch missing cases. + @compileLog(T); + @compileError("missing case for type"); +} + /// FileFormatter is a formatter implementation that outputs the /// config in a file-like format. This uses more generous whitespace, /// can include comments, etc. @@ -19,7 +140,7 @@ pub const FileFormatter = struct { inline for (@typeInfo(Config).Struct.fields) |field| { if (field.name[0] == '_') continue; - try self.formatEntry( + try formatEntry( field.type, field.name, @field(self.config, field.name), @@ -27,120 +148,6 @@ pub const FileFormatter = struct { ); } } - - pub fn formatEntry( - self: FileFormatter, - comptime T: type, - name: []const u8, - value: T, - writer: anytype, - ) !void { - const EntryFormatter = struct { - parent: *const FileFormatter, - name: []const u8, - writer: @TypeOf(writer), - - pub fn formatEntry( - self_entry: @This(), - comptime EntryT: type, - value_entry: EntryT, - ) !void { - return self_entry.parent.formatEntry( - EntryT, - self_entry.name, - value_entry, - self_entry.writer, - ); - } - }; - - switch (@typeInfo(T)) { - .Bool, .Int => { - try writer.print("{s} = {}\n", .{ name, value }); - return; - }, - - .Float => { - try writer.print("{s} = {d}\n", .{ name, value }); - return; - }, - - .Enum => { - try writer.print("{s} = {s}\n", .{ name, @tagName(value) }); - return; - }, - - .Void => { - try writer.print("{s} = \n", .{name}); - return; - }, - - .Optional => |info| { - if (value) |inner| { - try self.formatEntry( - info.child, - name, - inner, - writer, - ); - } else { - try writer.print("{s} = \n", .{name}); - } - - return; - }, - - .Pointer => switch (T) { - []const u8, - [:0]const u8, - => { - try writer.print("{s} = {s}\n", .{ name, value }); - return; - }, - - else => {}, - }, - - // Structs of all types require a "formatEntry" function - // to be defined which will be called to format the value. - // This is given the formatter in use so that they can - // call BACK to our formatEntry to write each primitive - // value. - .Struct => |info| if (@hasDecl(T, "formatEntry")) { - try value.formatEntry(EntryFormatter{ - .parent = &self, - .name = name, - .writer = writer, - }); - return; - } else switch (info.layout) { - // Packed structs we special case. - .Packed => { - try writer.print("{s} = ", .{name}); - inline for (info.fields, 0..) |field, i| { - if (i > 0) try writer.print(",", .{}); - try writer.print("{s}{s}", .{ - if (!@field(value, field.name)) "no-" else "", - field.name, - }); - } - try writer.print("\n", .{}); - return; - }, - - else => {}, - }, - - // TODO - .Union => return, - - else => {}, - } - - // Compile error so that we can catch missing cases. - @compileLog(T); - @compileError("missing case for type"); - } }; test "format default config" { @@ -155,5 +162,5 @@ test "format default config" { const fmt: FileFormatter = .{ .config = &cfg }; try std.fmt.format(buf.writer(), "{}", .{fmt}); - std.log.warn("{s}", .{buf.items}); + //std.log.warn("{s}", .{buf.items}); }