From ff571626db2d40c4c39c0632054072f7f2a22a14 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Sun, 4 Jan 2026 10:22:44 -0800 Subject: [PATCH] make art tests cover all formats more holistically --- src/render/formatted.zig | 265 +++++++++++++++++++++++-------- src/tests/metno-phoenix-tmp.ansi | 36 ++--- 2 files changed, 215 insertions(+), 86 deletions(-) diff --git a/src/render/formatted.zig b/src/render/formatted.zig index 1d38a13..c8c6f30 100644 --- a/src/render/formatted.zig +++ b/src/render/formatted.zig @@ -90,7 +90,7 @@ fn renderCurrent(w: *std.Io.Writer, current: types.CurrentCondition, options: Re const precip_unit = if (options.use_imperial) "in" else "mm"; const precip = if (options.use_imperial) current.precip_mm * 0.0393701 else current.precip_mm; - const art = getWeatherArt(current.weather_code); + const art = getWeatherArt(current.weather_code, options.format); const sign: u8 = if (temp >= 0) '+' else '-'; const abs_temp = @abs(temp); const fl_sign: u8 = if (feels_like >= 0) '+' else '-'; @@ -113,37 +113,35 @@ fn renderCurrent(w: *std.Io.Writer, current: types.CurrentCondition, options: Re .ansi => { const temp_color_code = tempColor(current.temp_c); const wind_color_code = windColor(current.wind_kph); - const cloud_color = "\x1b[38;5;250m"; const reset = "\x1b[0m"; - try w.print("{s}{s}{s} {s}\n", .{ cloud_color, art[0], reset, current.condition }); - try w.print("{s}{s}{s} \x1b[38;5;{d}m{c}{d:.0}({c}{d:.0}){s} {s}\n", .{ cloud_color, art[1], reset, temp_color_code, sign, abs_temp, fl_sign, abs_fl, reset, temp_unit }); - try w.print("{s}{s}{s} {s} \x1b[38;5;{d}m{d:.0}{s} {s}\n", .{ cloud_color, art[2], reset, degreeToArrow(current.wind_deg), wind_color_code, wind_speed, reset, wind_unit }); + try w.print("{s} {s}\n", .{ art[0], current.condition }); + try w.print("{s} \x1b[38;5;{d}m{c}{d:.0}({c}{d:.0}){s} {s}\n", .{ art[1], temp_color_code, sign, abs_temp, fl_sign, abs_fl, reset, temp_unit }); + try w.print("{s} {s} \x1b[38;5;{d}m{d:.0}{s} {s}\n", .{ art[2], degreeToArrow(current.wind_deg), wind_color_code, wind_speed, reset, wind_unit }); if (current.visibility_km) |_| { const visibility = if (options.use_imperial) current.visiblityMph().? else current.visibility_km.?; const vis_unit = if (options.use_imperial) "mi" else "km"; - try w.print("{s}{s}{s} {d:.0} {s}\n", .{ cloud_color, art[3], reset, visibility, vis_unit }); + try w.print("{s} {d:.0} {s}\n", .{ art[3], visibility, vis_unit }); } else { - try w.print("{s}{s}{s}\n", .{ cloud_color, std.mem.trimRight(u8, art[3], " "), reset }); + try w.print("{s}\n", .{std.mem.trimRight(u8, art[3], " ")}); } - try w.print("{s}{s}{s} {d:.1} {s}\n", .{ cloud_color, art[4], reset, precip, precip_unit }); + try w.print("{s} {d:.1} {s}\n", .{ art[4], precip, precip_unit }); }, .html => { const temp_color = ansiToHex(tempColor(current.temp_c)); const wind_color = ansiToHex(windColor(current.wind_kph)); - const cloud_color = "#bcbcbc"; // 250 - try w.print("{s} {s}\n", .{ cloud_color, art[0], current.condition }); - try w.print("{s} {c}{d:.0}({c}{d:.0}) {s}\n", .{ cloud_color, art[1], temp_color, sign, abs_temp, fl_sign, abs_fl, temp_unit }); - try w.print("{s} {s} {d:.0} {s}\n", .{ cloud_color, art[2], degreeToArrow(current.wind_deg), wind_color, wind_speed, wind_unit }); + try w.print("{s} {s}\n", .{ art[0], current.condition }); + try w.print("{s} {c}{d:.0}({c}{d:.0}) {s}\n", .{ art[1], temp_color, sign, abs_temp, fl_sign, abs_fl, temp_unit }); + try w.print("{s} {s} {d:.0} {s}\n", .{ art[2], degreeToArrow(current.wind_deg), wind_color, wind_speed, wind_unit }); if (current.visibility_km) |_| { const visibility = if (options.use_imperial) current.visiblityMph().? else current.visibility_km.?; const vis_unit = if (options.use_imperial) "mi" else "km"; - try w.print("{s} {d:.0} {s}\n", .{ cloud_color, art[3], visibility, vis_unit }); + try w.print("{s} {d:.0} {s}\n", .{ art[3], visibility, vis_unit }); } else { - try w.print("{s}\n", .{ cloud_color, std.mem.trimRight(u8, art[3], " ") }); + try w.print("{s}\n", .{std.mem.trimRight(u8, art[3], " ")}); } - try w.print("{s} {d:.1} {s}\n", .{ cloud_color, art[4], precip, precip_unit }); + try w.print("{s} {d:.1} {s}\n", .{ art[4], precip, precip_unit }); }, } } @@ -154,7 +152,7 @@ fn renderForecastDay(w: *std.Io.Writer, day: types.ForecastDay, options: RenderO const max_temp = if (options.use_imperial) day.maxTempFahrenheit() else day.max_temp_c; const min_temp = if (options.use_imperial) day.minTempFahrenheit() else day.min_temp_c; const temp_unit = if (options.use_imperial) "°F" else "°C"; - const art = getWeatherArt(day.weather_code); + const art = getWeatherArt(day.weather_code, options.format); _ = try formatDate(day.date, .compressed, &date_str); try w.print("\n{s}\n", .{std.mem.trimEnd(u8, date_str[0..], " ")}); @@ -199,7 +197,7 @@ fn renderHourlyCell(w: *std.Io.Writer, hour: types.HourlyForecast, line: usize, precipitation = 4, }; - const art = getWeatherArt(hour.weather_code); + const art = getWeatherArt(hour.weather_code, options.format); const total_width = 28; const art_width = 14; // includes spacer between art and data. This is display width, not actual @@ -207,17 +205,7 @@ fn renderHourlyCell(w: *std.Io.Writer, hour: types.HourlyForecast, line: usize, var cell_writer = std.Io.Writer.fixed(&buf); const cw = &cell_writer; - switch (options.format) { - .ansi => { - try w.print("\x1b[38;5;250m{s}\x1b[0m ", .{art[line]}); - }, - .html => { - try w.print("{s} ", .{art[line]}); - }, - .plain_text => { - try w.print("{s} ", .{art[line]}); - }, - } + try w.print("{s} ", .{art[line]}); switch (@as(Line, @enumFromInt(line))) { .condition => { @@ -408,7 +396,15 @@ fn ansiToHex(code: u8) []const u8 { }; } -fn getWeatherArt(code: types.WeatherCode) [5][]const u8 { +fn getWeatherArt(code: types.WeatherCode, format: Format) [5][]const u8 { + return switch (format) { + .plain_text => getWeatherArtPlain(code), + .ansi => getWeatherArtAnsi(code), + .html => getWeatherArtHtml(code), + }; +} + +fn getWeatherArtPlain(code: types.WeatherCode) [5][]const u8 { return switch (@intFromEnum(code)) { 800 => .{ // Clear " \\ / ", @@ -476,6 +472,142 @@ fn getWeatherArt(code: types.WeatherCode) [5][]const u8 { }; } +fn getWeatherArtAnsi(code: types.WeatherCode) [5][]const u8 { + return switch (@intFromEnum(code)) { + 800 => .{ // Clear + "\x1b[38;5;226m \\ / \x1b[0m", + "\x1b[38;5;226m .-. \x1b[0m", + "\x1b[38;5;226m ― ( ) ― \x1b[0m", + "\x1b[38;5;226m `-' \x1b[0m", + "\x1b[38;5;226m / \\ \x1b[0m", + }, + 801, 802 => .{ // Partly cloudy + "\x1b[38;5;226m \\ /\x1b[0m ", + "\x1b[38;5;226m _ /\"\"\x1b[38;5;250m.-. \x1b[0m", + "\x1b[38;5;226m \\_\x1b[38;5;250m( ). \x1b[0m", + "\x1b[38;5;226m /\x1b[38;5;250m(___(__) \x1b[0m", + " ", + }, + 803, 804 => .{ // Cloudy + " ", + "\x1b[38;5;250m .--. \x1b[0m", + "\x1b[38;5;250m .-( ). \x1b[0m", + "\x1b[38;5;250m (___.__)__) \x1b[0m", + " ", + }, + 300...321, 500...531 => .{ // Drizzle/Rain + "\x1b[38;5;250m .-. \x1b[0m", + "\x1b[38;5;250m ( ). \x1b[0m", + "\x1b[38;5;250m (___(__) \x1b[0m", + "\x1b[38;5;111m ʻ ʻ ʻ ʻ \x1b[0m", + "\x1b[38;5;111m ʻ ʻ ʻ ʻ \x1b[0m", + }, + 200...232 => .{ // Thunderstorm + "\x1b[38;5;250m .-. \x1b[0m", + "\x1b[38;5;250m ( ). \x1b[0m", + "\x1b[38;5;250m (___(__) \x1b[0m", + "\x1b[38;5;228;5m ⚡\x1b[38;5;111;25mʻʻ\x1b[38;5;228;5m⚡\x1b[38;5;111;25mʻʻ\x1b[0m", + "\x1b[38;5;111m ʻ ʻ ʻ ʻ \x1b[0m", + }, + 600...610, 617...622 => .{ // Snow + "\x1b[38;5;250m .-. \x1b[0m", + "\x1b[38;5;250m ( ). \x1b[0m", + "\x1b[38;5;250m (___(__) \x1b[0m", + "\x1b[38;5;255m * * * \x1b[0m", + "\x1b[38;5;255m * * * \x1b[0m", + }, + 611...616 => .{ // Sleet + "\x1b[38;5;250m .-. \x1b[0m", + "\x1b[38;5;250m ( ). \x1b[0m", + "\x1b[38;5;250m (___(__) \x1b[0m", + "\x1b[38;5;111m ʻ \x1b[38;5;255m*\x1b[38;5;111m ʻ \x1b[38;5;255m* \x1b[0m", + "\x1b[38;5;255m *\x1b[38;5;111m ʻ \x1b[38;5;255m*\x1b[38;5;111m ʻ \x1b[0m", + }, + 701, 741 => .{ // Fog + " ", + "\x1b[38;5;251m _ - _ - _ - \x1b[0m", + "\x1b[38;5;251m _ - _ - _ \x1b[0m", + "\x1b[38;5;251m _ - _ - _ - \x1b[0m", + " ", + }, + else => .{ // Unknown + " ", + " ? ", + " ¯\\_(ツ)_/¯ ", + " ", + " ", + }, + }; +} + +fn getWeatherArtHtml(code: types.WeatherCode) [5][]const u8 { + return switch (@intFromEnum(code)) { + 800 => .{ // Clear + " \\ / ", + " .-. ", + " ― ( ) ― ", + " `-' ", + " / \\ ", + }, + 801, 802 => .{ // Partly cloudy + " \\ / ", + " _ /\"\".-. ", + " \\_( ). ", + " /(___(__) ", + " ", + }, + 803, 804 => .{ // Cloudy + " ", + " .--. ", + " .-( ). ", + " (___.__)__) ", + " ", + }, + 300...321, 500...531 => .{ // Drizzle/Rain + " .-. ", + " ( ). ", + " (___(__) ", + " ʻ ʻ ʻ ʻ ", + " ʻ ʻ ʻ ʻ ", + }, + 200...232 => .{ // Thunderstorm + " .-. ", + " ( ). ", + " (___(__) ", + "ʻʻ ", + " ʻ ʻ ʻ ", + }, + 600...610, 617...622 => .{ // Snow + " .-. ", + " ( ). ", + " (___(__) ", + " * * * ", + " * * * ", + }, + 611...616 => .{ // Sleet + " .-. ", + " ( ). ", + " (___(__) ", + " ʻ * ʻ * ", + " * ʻ * ʻ ", + }, + 701, 741 => .{ // Fog + " ", + " _ - _ - _ - ", + " _ - _ - _ ", + " _ - _ - _ - ", + " ", + }, + else => .{ // Unknown + " ", + " ? ", + " ¯\\_(ツ)_/¯ ", + " ", + " ", + }, + }; +} + test "render with imperial units" { const data = types.WeatherData{ .location = "Chicago", @@ -523,11 +655,7 @@ test "clear weather art" { .allocator = std.testing.allocator, }; - const output = try render(std.testing.allocator, data, .{}); - defer std.testing.allocator.free(output); - - try std.testing.expect(std.mem.indexOf(u8, output, "\\ /") != null); - try std.testing.expect(std.mem.indexOf(u8, output, "( )") != null); + try testArt(data); } test "partly cloudy weather art" { @@ -549,11 +677,35 @@ test "partly cloudy weather art" { .allocator = std.testing.allocator, }; - const output = try render(std.testing.allocator, data, .{}); - defer std.testing.allocator.free(output); + try testArt(data); +} - try std.testing.expect(std.mem.indexOf(u8, output, "\"\".-.") != null); - try std.testing.expect(std.mem.indexOf(u8, output, "\\_( )") != null); +/// Tests to make sure the weather art shows up in the rendering for all formats +fn testArt(data: types.WeatherData) !void { + inline for (std.meta.fields(Format)) |f| { + const format: Format = @enumFromInt(f.value); + const output = try render( + std.testing.allocator, + data, + .{ .format = format }, + ); + defer std.testing.allocator.free(output); + + const target = getWeatherArt( + data.current.weather_code, + format, + ); + for (target, 1..) |line, i| { + const trimmed = std.mem.trimRight(u8, line, " "); + std.testing.expect(std.mem.indexOf(u8, output, trimmed) != null) catch |e| { + std.log.err( + "Test failure, weather code {}, format {}, line {d}. Line '{s}', Output:\n{s}\n", + .{ data.current.weather_code, format, i, line, output }, + ); + return e; + }; + } + } } test "cloudy weather art" { @@ -575,11 +727,7 @@ test "cloudy weather art" { .allocator = std.testing.allocator, }; - const output = try render(std.testing.allocator, data, .{}); - defer std.testing.allocator.free(output); - - try std.testing.expect(std.mem.indexOf(u8, output, ".--.") != null); - try std.testing.expect(std.mem.indexOf(u8, output, "(___.__)__)") != null); + try testArt(data); } test "rain weather art" { @@ -601,10 +749,7 @@ test "rain weather art" { .allocator = std.testing.allocator, }; - const output = try render(std.testing.allocator, data, .{}); - defer std.testing.allocator.free(output); - - try std.testing.expect(std.mem.indexOf(u8, output, "ʻ ʻ ʻ ʻ") != null); + try testArt(data); } test "thunderstorm weather art" { @@ -626,10 +771,7 @@ test "thunderstorm weather art" { .allocator = std.testing.allocator, }; - const output = try render(std.testing.allocator, data, .{}); - defer std.testing.allocator.free(output); - - try std.testing.expect(std.mem.indexOf(u8, output, "⚡") != null); + try testArt(data); } test "snow weather art" { @@ -651,10 +793,7 @@ test "snow weather art" { .allocator = std.testing.allocator, }; - const output = try render(std.testing.allocator, data, .{}); - defer std.testing.allocator.free(output); - - try std.testing.expect(std.mem.indexOf(u8, output, "* * *") != null); + try testArt(data); } test "sleet weather art" { @@ -676,10 +815,7 @@ test "sleet weather art" { .allocator = std.testing.allocator, }; - const output = try render(std.testing.allocator, data, .{}); - defer std.testing.allocator.free(output); - - try std.testing.expect(std.mem.indexOf(u8, output, "ʻ * ʻ *") != null); + try testArt(data); } test "fog weather art" { @@ -701,10 +837,7 @@ test "fog weather art" { .allocator = std.testing.allocator, }; - const output = try render(std.testing.allocator, data, .{}); - defer std.testing.allocator.free(output); - - try std.testing.expect(std.mem.indexOf(u8, output, "_ - _ - _ -") != null); + try testArt(data); } test "unknown weather code art" { @@ -726,11 +859,7 @@ test "unknown weather code art" { .allocator = std.testing.allocator, }; - const output = try render(std.testing.allocator, data, .{}); - defer std.testing.allocator.free(output); - - try std.testing.expect(std.mem.indexOf(u8, output, "?") != null); - try std.testing.expect(std.mem.indexOf(u8, output, "¯\\_(ツ)_/¯") != null); + try testArt(data); } test "temperature matches between ansi and custom format" { diff --git a/src/tests/metno-phoenix-tmp.ansi b/src/tests/metno-phoenix-tmp.ansi index 5d612e7..1b87c82 100644 --- a/src/tests/metno-phoenix-tmp.ansi +++ b/src/tests/metno-phoenix-tmp.ansi @@ -1,38 +1,38 @@ Weather report: 33.4484,-112.0741 -  Cloudy + Cloudy  .--.  +61(+61) °F  .-( ).  ↘ 2 mph - (___.__)__) -  0.0 in + (___.__)__)  + 0.0 in ┌─────────────┐ ┌──────────────────────────────┬───────────────────────┤ Sun 4 Jan ├───────────────────────┬──────────────────────────────┐ │ Morning │ Noon └──────┬──────┘ Evening │ Night │ ├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤ -│   Cloudy │  \ /  Partly cloudy │  \ /  Partly cloudy │  \ /  Clear │ -│  .--.  +61(+61) °F │  _ /"".-.  +56(+56) °F │  _ /"".-.  +50(+50) °F │  .-.  +65(+65) °F │ -│  .-( ).  ↘ 2 mph │  \_( ).  ↖ 1 mph │  \_( ).  ↖ 3 mph │  ― ( ) ―  ↖ 3 mph │ -│  (___.__)__)  │  /(___(__)  │  /(___(__)  │  `-'  │ -│   0.0 in | 0% │   0.0 in | 0% │   0.0 in | 0% │  / \  0.0 in | 0% │ +│ Cloudy │  \ / Partly cloudy │  \ / Partly cloudy │  \ /  Clear │ +│  .--.  +61(+61) °F │  _ /"".-.  +56(+56) °F │  _ /"".-.  +50(+50) °F │  .-.  +65(+65) °F │ +│  .-( ).  ↘ 2 mph │  \_( ).  ↖ 1 mph │  \_( ).  ↖ 3 mph │  ― ( ) ―  ↖ 3 mph │ +│  (___.__)__)  │  /(___(__)  │  /(___(__)  │  `-'  │ +│ 0.0 in | 0% │ 0.0 in | 0% │ 0.0 in | 0% │  / \  0.0 in | 0% │ └──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘ ┌─────────────┐ ┌──────────────────────────────┬───────────────────────┤ Mon 5 Jan ├───────────────────────┬──────────────────────────────┐ │ Morning │ Noon └──────┬──────┘ Evening │ Night │ ├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤ -│  \ /  Fair │  \ /  Partly cloudy │  \ /  Partly cloudy │  \ /  Partly cloudy │ -│  _ /"".-.  +68(+68) °F │  _ /"".-.  +57(+57) °F │  _ /"".-.  +53(+53) °F │  _ /"".-.  +61(+61) °F │ -│  \_( ).  ↙ 2 mph │  \_( ).  ↖ 3 mph │  \_( ).  ↘ 2 mph │  \_( ).  ← 3 mph │ -│  /(___(__)  │  /(___(__)  │  /(___(__)  │  /(___(__)  │ -│   0.0 in | 0% │   0.0 in | 0% │   0.0 in | 0% │   0.0 in | 0% │ +│  \ / Fair │  \ / Partly cloudy │  \ / Partly cloudy │  \ / Partly cloudy │ +│  _ /"".-.  +68(+68) °F │  _ /"".-.  +57(+57) °F │  _ /"".-.  +53(+53) °F │  _ /"".-.  +61(+61) °F │ +│  \_( ).  ↙ 2 mph │  \_( ).  ↖ 3 mph │  \_( ).  ↘ 2 mph │  \_( ).  ← 3 mph │ +│  /(___(__)  │  /(___(__)  │  /(___(__)  │  /(___(__)  │ +│ 0.0 in | 0% │ 0.0 in | 0% │ 0.0 in | 0% │ 0.0 in | 0% │ └──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘ ┌─────────────┐ ┌──────────────────────────────┬───────────────────────┤ Tue 6 Jan ├───────────────────────┬──────────────────────────────┐ │ Morning │ Noon └──────┬──────┘ Evening │ Night │ ├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤ -│   Cloudy │  \ /  Partly cloudy │  \ /  Partly cloudy │  \ /  Partly cloudy │ -│  .--.  +66(+66) °F │  _ /"".-.  +58(+58) °F │  _ /"".-.  +53(+53) °F │  _ /"".-.  +49(+49) °F │ -│  .-( ).  ↘ 2 mph │  \_( ).  ← 3 mph │  \_( ).  ← 2 mph │  \_( ).  ← 2 mph │ -│  (___.__)__)  │  /(___(__)  │  /(___(__)  │  /(___(__)  │ -│   0.0 in | 0% │   0.0 in | 0% │   0.0 in | 0% │   0.0 in | 0% │ +│ Cloudy │  \ / Partly cloudy │  \ / Partly cloudy │  \ / Partly cloudy │ +│  .--.  +66(+66) °F │  _ /"".-.  +58(+58) °F │  _ /"".-.  +53(+53) °F │  _ /"".-.  +49(+49) °F │ +│  .-( ).  ↘ 2 mph │  \_( ).  ← 3 mph │  \_( ).  ← 2 mph │  \_( ).  ← 2 mph │ +│  (___.__)__)  │  /(___(__)  │  /(___(__)  │  /(___(__)  │ +│ 0.0 in | 0% │ 0.0 in | 0% │ 0.0 in | 0% │ 0.0 in | 0% │ └──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘