complete mock implementation, add tests/fix forecast calc (part 1)

This commit is contained in:
Emil Lerch 2026-01-03 12:23:38 -08:00
parent 06d25df997
commit 62bae1fb99
Signed by: lobo
GPG key ID: A7B62D657EF764F8
4 changed files with 75 additions and 30 deletions

File diff suppressed because one or more lines are too long

View file

@ -116,7 +116,7 @@ fn fetchRaw(ptr: *anyopaque, allocator: std.mem.Allocator, coords: Coordinates)
return try allocator.dupe(u8, response_body);
}
fn parse(ptr: *anyopaque, allocator: std.mem.Allocator, raw: []const u8) !types.WeatherData {
pub fn parse(ptr: *anyopaque, allocator: std.mem.Allocator, raw: []const u8) !types.WeatherData {
_ = ptr;
// Parse JSON response
@ -268,31 +268,29 @@ fn parseForecastDays(allocator: std.mem.Allocator, timeseries: []std.json.Value)
break :blk hrs;
}
clearHourlyForecast(allocator, &day_all_hours);
if (day_all_hours.items.len < 4)
if (day_all_hours.items.len < 4) {
clearHourlyForecast(allocator, &day_hourly);
break :blk try day_all_hours.toOwnedSlice(allocator);
// Pick 4 evenly spaced entries from day_all_hours
if (day_all_hours.items.len >= 4) {
const step = day_all_hours.items.len / 4;
var selected: std.ArrayList(types.HourlyForecast) = .empty;
try selected.append(allocator, day_all_hours.items[0]);
try selected.append(allocator, day_all_hours.items[step]);
try selected.append(allocator, day_all_hours.items[step * 2]);
try selected.append(allocator, day_all_hours.items[step * 3]);
const hrs = try selected.toOwnedSlice(allocator);
// Free the rest
for (day_all_hours.items, 0..) |h, i| {
if (i != 0 and i != step and i != step * 2 and i != step * 3) {
allocator.free(h.time);
allocator.free(h.condition);
}
}
day_all_hours.clearRetainingCapacity();
break :blk hrs;
}
break :blk try day_all_hours.toOwnedSlice(allocator);
// Pick 4 evenly spaced entries from day_all_hours
const step = day_all_hours.items.len / 4;
var selected: std.ArrayList(types.HourlyForecast) = .empty;
try selected.append(allocator, day_all_hours.items[0]);
try selected.append(allocator, day_all_hours.items[step]);
try selected.append(allocator, day_all_hours.items[step * 2]);
try selected.append(allocator, day_all_hours.items[step * 3]);
const hrs = try selected.toOwnedSlice(allocator);
// Free the rest
for (day_all_hours.items, 0..) |h, i| {
if (i != 0 and i != step and i != step * 2 and i != step * 3) {
allocator.free(h.time);
allocator.free(h.condition);
}
}
day_all_hours.clearRetainingCapacity();
clearHourlyForecast(allocator, &day_hourly);
break :blk hrs;
};
try days.append(allocator, .{
@ -310,6 +308,7 @@ fn parseForecastDays(allocator: std.mem.Allocator, timeseries: []std.json.Value)
// Start new day
current_date = date;
day_temps.clearRetainingCapacity();
day_hourly.clearRetainingCapacity();
day_all_hours.clearRetainingCapacity();
day_symbol = null;
}
@ -476,3 +475,29 @@ test "parseForecastDays handles empty timeseries" {
defer allocator.free(forecast);
try std.testing.expectEqual(@as(usize, 0), forecast.len);
}
test "hourly forecasts should have 4 entries per day" {
const allocator = std.testing.allocator;
const json_data = @embedFile("../tests/metno_test_data.json");
const weather_data = try parse(undefined, allocator, json_data);
defer weather_data.deinit();
// Skip first day if incomplete, check remaining days have 4 hourly entries
var checked: usize = 0;
for (weather_data.forecast) |day| {
if (day.hourly.len < 4) continue; // Skip incomplete days
try std.testing.expectEqual(@as(usize, 4), day.hourly.len);
// None should be "Unknown"
for (day.hourly) |hour| {
try std.testing.expect(!std.mem.eql(u8, hour.condition, "Unknown"));
}
checked += 1;
if (checked >= 3) break; // Check 3 complete days
}
try std.testing.expect(checked >= 2); // At least 2 complete days
}

View file

@ -8,11 +8,13 @@ const Mock = @This();
allocator: std.mem.Allocator,
responses: std.StringHashMap([]const u8),
parse_fn: ?*const fn (ptr: *anyopaque, allocator: std.mem.Allocator, raw: []const u8) anyerror!types.WeatherData = null,
pub fn init(allocator: std.mem.Allocator) !Mock {
return Mock{
.allocator = allocator,
.responses = std.StringHashMap([]const u8).init(allocator),
.parse_fn = null,
};
}
@ -43,9 +45,10 @@ fn fetchRaw(ptr: *anyopaque, allocator: std.mem.Allocator, coords: Coordinates)
}
fn parse(ptr: *anyopaque, allocator: std.mem.Allocator, raw: []const u8) !types.WeatherData {
_ = ptr;
_ = allocator;
_ = raw;
const self: *Mock = @ptrCast(@alignCast(ptr));
if (self.parse_fn) |parse_fn| {
return parse_fn(ptr, allocator, raw);
}
return error.NotImplemented;
}
@ -63,7 +66,21 @@ pub fn deinit(self: *Mock) void {
self.responses.deinit();
}
test "mock weather provider" {
// TODO: Implement Mock.parse to enable this test
return error.SkipZigTest;
test "mock weather provider with MetNo parse" {
const MetNo = @import("MetNo.zig");
const allocator = std.testing.allocator;
const test_data = @embedFile("../tests/metno_test_data.json");
var mock = try Mock.init(allocator);
defer mock.deinit();
mock.parse_fn = MetNo.parse;
// Parse directly - no fetching
const weather = try MetNo.parse(&mock, allocator, test_data);
defer weather.deinit();
// Verify we got valid weather data
try std.testing.expect(weather.forecast.len > 0);
}

View file

@ -89,6 +89,8 @@ pub const WeatherData = struct {
pub fn deinit(self: WeatherData) void {
self.allocator.free(self.location);
self.allocator.free(self.current.condition);
self.allocator.free(self.current.wind_dir);
for (self.forecast) |day| {
self.allocator.free(day.date);
self.allocator.free(day.condition);