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); 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; _ = ptr;
// Parse JSON response // Parse JSON response
@ -268,31 +268,29 @@ fn parseForecastDays(allocator: std.mem.Allocator, timeseries: []std.json.Value)
break :blk hrs; break :blk hrs;
} }
clearHourlyForecast(allocator, &day_all_hours); if (day_all_hours.items.len < 4) {
clearHourlyForecast(allocator, &day_hourly);
if (day_all_hours.items.len < 4)
break :blk try day_all_hours.toOwnedSlice(allocator); 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, .{ try days.append(allocator, .{
@ -310,6 +308,7 @@ fn parseForecastDays(allocator: std.mem.Allocator, timeseries: []std.json.Value)
// Start new day // Start new day
current_date = date; current_date = date;
day_temps.clearRetainingCapacity(); day_temps.clearRetainingCapacity();
day_hourly.clearRetainingCapacity();
day_all_hours.clearRetainingCapacity(); day_all_hours.clearRetainingCapacity();
day_symbol = null; day_symbol = null;
} }
@ -476,3 +475,29 @@ test "parseForecastDays handles empty timeseries" {
defer allocator.free(forecast); defer allocator.free(forecast);
try std.testing.expectEqual(@as(usize, 0), forecast.len); 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, allocator: std.mem.Allocator,
responses: std.StringHashMap([]const u8), 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 { pub fn init(allocator: std.mem.Allocator) !Mock {
return Mock{ return Mock{
.allocator = allocator, .allocator = allocator,
.responses = std.StringHashMap([]const u8).init(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 { fn parse(ptr: *anyopaque, allocator: std.mem.Allocator, raw: []const u8) !types.WeatherData {
_ = ptr; const self: *Mock = @ptrCast(@alignCast(ptr));
_ = allocator; if (self.parse_fn) |parse_fn| {
_ = raw; return parse_fn(ptr, allocator, raw);
}
return error.NotImplemented; return error.NotImplemented;
} }
@ -63,7 +66,21 @@ pub fn deinit(self: *Mock) void {
self.responses.deinit(); self.responses.deinit();
} }
test "mock weather provider" { test "mock weather provider with MetNo parse" {
// TODO: Implement Mock.parse to enable this test const MetNo = @import("MetNo.zig");
return error.SkipZigTest; 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 { pub fn deinit(self: WeatherData) void {
self.allocator.free(self.location); self.allocator.free(self.location);
self.allocator.free(self.current.condition);
self.allocator.free(self.current.wind_dir);
for (self.forecast) |day| { for (self.forecast) |day| {
self.allocator.free(day.date); self.allocator.free(day.date);
self.allocator.free(day.condition); self.allocator.free(day.condition);