make art tests cover all formats more holistically

This commit is contained in:
Emil Lerch 2026-01-04 10:22:44 -08:00
parent a314fc39d4
commit ff571626db
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 215 additions and 86 deletions

View file

@ -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("<span style=\"color:{s}\">{s}</span> {s}\n", .{ cloud_color, art[0], current.condition });
try w.print("<span style=\"color:{s}\">{s}</span> <span style=\"color:{s}\">{c}{d:.0}({c}{d:.0})</span> {s}\n", .{ cloud_color, art[1], temp_color, sign, abs_temp, fl_sign, abs_fl, temp_unit });
try w.print("<span style=\"color:{s}\">{s}</span> {s} <span style=\"color:{s}\">{d:.0}</span> {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} <span style=\"color:{s}\">{c}{d:.0}({c}{d:.0})</span> {s}\n", .{ art[1], temp_color, sign, abs_temp, fl_sign, abs_fl, temp_unit });
try w.print("{s} {s} <span style=\"color:{s}\">{d:.0}</span> {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("<span style=\"color:{s}\">{s}</span> {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("<span style=\"color:{s}\">{s}</span>\n", .{ cloud_color, std.mem.trimRight(u8, art[3], " ") });
try w.print("{s}\n", .{std.mem.trimRight(u8, art[3], " ")});
}
try w.print("<span style=\"color:{s}\">{s}</span> {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("<span style=\"color:#bcbcbc\">{s}</span> ", .{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
"<span style=\"color:#ffff00\"> \\ / </span>",
"<span style=\"color:#ffff00\"> .-. </span>",
"<span style=\"color:#ffff00\"> ― ( ) ― </span>",
"<span style=\"color:#ffff00\"> `-' </span>",
"<span style=\"color:#ffff00\"> / \\ </span>",
},
801, 802 => .{ // Partly cloudy
"<span style=\"color:#ffff00\"> \\ / </span>",
"<span style=\"color:#bcbcbc\"> _ /\"\".-. </span>",
"<span style=\"color:#bcbcbc\"> \\_( ). </span>",
"<span style=\"color:#bcbcbc\"> /(___(__) </span>",
" ",
},
803, 804 => .{ // Cloudy
" ",
"<span style=\"color:#bcbcbc\"> .--. </span>",
"<span style=\"color:#bcbcbc\"> .-( ). </span>",
"<span style=\"color:#bcbcbc\"> (___.__)__) </span>",
" ",
},
300...321, 500...531 => .{ // Drizzle/Rain
"<span style=\"color:#bcbcbc\"> .-. </span>",
"<span style=\"color:#bcbcbc\"> ( ). </span>",
"<span style=\"color:#bcbcbc\"> (___(__) </span>",
"<span style=\"color:#87afff\"> ʻ ʻ ʻ ʻ </span>",
"<span style=\"color:#87afff\"> ʻ ʻ ʻ ʻ </span>",
},
200...232 => .{ // Thunderstorm
"<span style=\"color:#bcbcbc\"> .-. </span>",
"<span style=\"color:#bcbcbc\"> ( ). </span>",
"<span style=\"color:#bcbcbc\"> (___(__) </span>",
"<span style=\"color:#ffff87\"> ⚡</span><span style=\"color:#87afff\">ʻ</span><span style=\"color:#ffff87\">⚡</span><span style=\"color:#87afff\">ʻ </span>",
"<span style=\"color:#87afff\"> ʻ ʻ ʻ </span>",
},
600...610, 617...622 => .{ // Snow
"<span style=\"color:#bcbcbc\"> .-. </span>",
"<span style=\"color:#bcbcbc\"> ( ). </span>",
"<span style=\"color:#bcbcbc\"> (___(__) </span>",
"<span style=\"color:#eeeeee\"> * * * </span>",
"<span style=\"color:#eeeeee\"> * * * </span>",
},
611...616 => .{ // Sleet
"<span style=\"color:#bcbcbc\"> .-. </span>",
"<span style=\"color:#bcbcbc\"> ( ). </span>",
"<span style=\"color:#bcbcbc\"> (___(__) </span>",
"<span style=\"color:#87afff\"> ʻ </span><span style=\"color:#eeeeee\">*</span><span style=\"color:#87afff\"> ʻ </span><span style=\"color:#eeeeee\">* </span>",
"<span style=\"color:#eeeeee\"> * </span><span style=\"color:#87afff\">ʻ</span><span style=\"color:#eeeeee\"> * </span><span style=\"color:#87afff\">ʻ </span>",
},
701, 741 => .{ // Fog
" ",
"<span style=\"color:#c6c6c6\"> _ - _ - _ - </span>",
"<span style=\"color:#c6c6c6\"> _ - _ - _ </span>",
"<span style=\"color:#c6c6c6\"> _ - _ - _ - </span>",
" ",
},
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" {

View file

@ -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% │
└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘