add provider test
This commit is contained in:
parent
50e493aad5
commit
6a6101c495
2 changed files with 61 additions and 17 deletions
48
src/cache/Cache.zig
vendored
48
src/cache/Cache.zig
vendored
|
|
@ -7,17 +7,19 @@ const log = std.log.scoped(.cache);
|
||||||
|
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
lru: Lru,
|
lru: Lru,
|
||||||
cache_dir: []const u8,
|
/// Cache directory for L2 persistent cache
|
||||||
|
cache_dir: ?[]const u8,
|
||||||
|
|
||||||
pub const Config = struct {
|
pub const Config = struct {
|
||||||
max_entries: usize = 1_000,
|
max_entries: usize = 1_000,
|
||||||
cache_dir: []const u8,
|
cache_dir: ?[]const u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, config: Config) !*Cache {
|
pub fn init(allocator: std.mem.Allocator, config: Config) !*Cache {
|
||||||
std.fs.makeDirAbsolute(config.cache_dir) catch |err| {
|
if (config.cache_dir) |d|
|
||||||
if (err != error.PathAlreadyExists) return err;
|
std.fs.makeDirAbsolute(d) catch |err| {
|
||||||
};
|
if (err != error.PathAlreadyExists) return err;
|
||||||
|
};
|
||||||
|
|
||||||
const cache = try allocator.create(Cache);
|
const cache = try allocator.create(Cache);
|
||||||
errdefer allocator.destroy(cache);
|
errdefer allocator.destroy(cache);
|
||||||
|
|
@ -25,15 +27,17 @@ pub fn init(allocator: std.mem.Allocator, config: Config) !*Cache {
|
||||||
cache.* = Cache{
|
cache.* = Cache{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.lru = try Lru.init(allocator, config.max_entries),
|
.lru = try Lru.init(allocator, config.max_entries),
|
||||||
.cache_dir = try allocator.dupe(u8, config.cache_dir),
|
.cache_dir = if (config.cache_dir) |d| try allocator.dupe(u8, d) else null,
|
||||||
};
|
};
|
||||||
|
|
||||||
cache.lru.setEvictionCallback(cache, evictCallback);
|
cache.lru.setEvictionCallback(cache, evictCallback);
|
||||||
|
|
||||||
// Clean up expired files and populate L1 cache from L2
|
// Clean up expired files and populate L1 cache from L2
|
||||||
cache.loadFromDir() catch |err| {
|
if (config.cache_dir) |d| {
|
||||||
std.log.warn("Failed to load cache from {s}: {}", .{ config.cache_dir, err });
|
cache.loadFromDir() catch |err| {
|
||||||
};
|
std.log.warn("Failed to load cache from {s}: {}", .{ d, err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
@ -61,8 +65,10 @@ pub fn put(self: *Cache, key: []const u8, value: []const u8, ttl_seconds: u64) !
|
||||||
const now = std.time.milliTimestamp();
|
const now = std.time.milliTimestamp();
|
||||||
const expires = now + @as(i64, @intCast(ttl_seconds * 1000));
|
const expires = now + @as(i64, @intCast(ttl_seconds * 1000));
|
||||||
|
|
||||||
// Write to L2 (disk) first
|
// Write to L2 (disk) first if cache_dir is set
|
||||||
try self.saveToFile(key, value, expires);
|
if (self.cache_dir != null) {
|
||||||
|
try self.saveToFile(key, value, expires);
|
||||||
|
}
|
||||||
|
|
||||||
// Add to L1 (memory)
|
// Add to L1 (memory)
|
||||||
try self.lru.put(key, value, expires);
|
try self.lru.put(key, value, expires);
|
||||||
|
|
@ -70,7 +76,7 @@ pub fn put(self: *Cache, key: []const u8, value: []const u8, ttl_seconds: u64) !
|
||||||
|
|
||||||
pub fn deinit(self: *Cache) void {
|
pub fn deinit(self: *Cache) void {
|
||||||
self.lru.deinit();
|
self.lru.deinit();
|
||||||
self.allocator.free(self.cache_dir);
|
if (self.cache_dir) |d| self.allocator.free(d);
|
||||||
self.allocator.destroy(self);
|
self.allocator.destroy(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,10 +102,12 @@ const CacheEntry = struct {
|
||||||
/// if the file access fails OR if the data has expired.
|
/// if the file access fails OR if the data has expired.
|
||||||
/// If the data has expired, the file will be deleted
|
/// If the data has expired, the file will be deleted
|
||||||
fn loadFromFile(self: *Cache, key: []const u8) !CacheEntry {
|
fn loadFromFile(self: *Cache, key: []const u8) !CacheEntry {
|
||||||
|
if (self.cache_dir == null) return error.NoCacheDir;
|
||||||
|
|
||||||
const filename = try self.getCacheFilename(key);
|
const filename = try self.getCacheFilename(key);
|
||||||
defer self.allocator.free(filename);
|
defer self.allocator.free(filename);
|
||||||
|
|
||||||
const file_path = try std.fs.path.join(self.allocator, &.{ self.cache_dir, filename });
|
const file_path = try std.fs.path.join(self.allocator, &.{ self.cache_dir.?, filename });
|
||||||
defer self.allocator.free(file_path);
|
defer self.allocator.free(file_path);
|
||||||
|
|
||||||
return self.loadFromFilePath(file_path);
|
return self.loadFromFilePath(file_path);
|
||||||
|
|
@ -155,10 +163,12 @@ fn deserialize(allocator: std.mem.Allocator, reader: *std.Io.Reader) !CacheEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
fn saveToFile(self: *Cache, key: []const u8, value: []const u8, expires: i64) !void {
|
fn saveToFile(self: *Cache, key: []const u8, value: []const u8, expires: i64) !void {
|
||||||
|
if (self.cache_dir == null) return error.NoCacheDir;
|
||||||
|
|
||||||
const filename = try self.getCacheFilename(key);
|
const filename = try self.getCacheFilename(key);
|
||||||
defer self.allocator.free(filename);
|
defer self.allocator.free(filename);
|
||||||
|
|
||||||
const file_path = try std.fs.path.join(self.allocator, &.{ self.cache_dir, filename });
|
const file_path = try std.fs.path.join(self.allocator, &.{ self.cache_dir.?, filename });
|
||||||
defer self.allocator.free(file_path);
|
defer self.allocator.free(file_path);
|
||||||
|
|
||||||
const file = try std.fs.cwd().createFile(file_path, .{});
|
const file = try std.fs.cwd().createFile(file_path, .{});
|
||||||
|
|
@ -172,14 +182,16 @@ fn saveToFile(self: *Cache, key: []const u8, value: []const u8, expires: i64) !v
|
||||||
}
|
}
|
||||||
|
|
||||||
fn loadFromDir(self: *Cache) !void {
|
fn loadFromDir(self: *Cache) !void {
|
||||||
var dir = try std.fs.cwd().openDir(self.cache_dir, .{ .iterate = true });
|
if (self.cache_dir == null) return error.NoCacheDir;
|
||||||
|
|
||||||
|
var dir = try std.fs.cwd().openDir(self.cache_dir.?, .{ .iterate = true });
|
||||||
defer dir.close();
|
defer dir.close();
|
||||||
|
|
||||||
var it = dir.iterate();
|
var it = dir.iterate();
|
||||||
while (try it.next()) |entry| {
|
while (try it.next()) |entry| {
|
||||||
if (entry.kind != .file) continue;
|
if (entry.kind != .file) continue;
|
||||||
|
|
||||||
const file_path = try std.fs.path.join(self.allocator, &.{ self.cache_dir, entry.name });
|
const file_path = try std.fs.path.join(self.allocator, &.{ self.cache_dir.?, entry.name });
|
||||||
defer self.allocator.free(file_path);
|
defer self.allocator.free(file_path);
|
||||||
|
|
||||||
const cached = self.loadFromFilePath(file_path) catch continue;
|
const cached = self.loadFromFilePath(file_path) catch continue;
|
||||||
|
|
@ -191,10 +203,12 @@ fn loadFromDir(self: *Cache) !void {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deleteFile(self: *Cache, key: []const u8) void {
|
fn deleteFile(self: *Cache, key: []const u8) void {
|
||||||
|
if (self.cache_dir == null) return;
|
||||||
|
|
||||||
const filename = self.getCacheFilename(key) catch @panic("OOM");
|
const filename = self.getCacheFilename(key) catch @panic("OOM");
|
||||||
defer self.allocator.free(filename);
|
defer self.allocator.free(filename);
|
||||||
|
|
||||||
const file_path = std.fs.path.join(self.allocator, &.{ self.cache_dir, filename }) catch @panic("OOM");
|
const file_path = std.fs.path.join(self.allocator, &.{ self.cache_dir.?, filename }) catch @panic("OOM");
|
||||||
defer self.allocator.free(file_path);
|
defer self.allocator.free(file_path);
|
||||||
|
|
||||||
std.fs.cwd().deleteFile(file_path) catch |e| {
|
std.fs.cwd().deleteFile(file_path) catch |e| {
|
||||||
|
|
|
||||||
|
|
@ -45,3 +45,33 @@ pub fn fetch(self: WeatherProvider, allocator: std.mem.Allocator, coords: Coordi
|
||||||
pub fn deinit(self: WeatherProvider) void {
|
pub fn deinit(self: WeatherProvider) void {
|
||||||
self.vtable.deinit(self.ptr);
|
self.vtable.deinit(self.ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "provider fetch" {
|
||||||
|
const Mock = @import("Mock.zig");
|
||||||
|
const MetNo = @import("MetNo.zig");
|
||||||
|
|
||||||
|
const cache = try Cache.init(std.testing.allocator, .{ .max_entries = 10, .cache_dir = null });
|
||||||
|
defer cache.deinit();
|
||||||
|
|
||||||
|
var mock = try Mock.init(std.testing.allocator);
|
||||||
|
defer mock.deinit();
|
||||||
|
|
||||||
|
mock.parse_fn = MetNo.parse;
|
||||||
|
|
||||||
|
const test_data = @embedFile("../tests/metno_test_data.json");
|
||||||
|
try mock.addResponse(.{ .latitude = 33.4484, .longitude = -112.0740 }, test_data);
|
||||||
|
|
||||||
|
const provider = mock.provider(cache);
|
||||||
|
const coords = Coordinates{ .latitude = 33.4484, .longitude = -112.0740 };
|
||||||
|
|
||||||
|
// First fetch - should allocate and cache
|
||||||
|
const weather1 = try provider.fetch(std.testing.allocator, coords);
|
||||||
|
defer weather1.deinit();
|
||||||
|
|
||||||
|
// Second fetch - should use cache
|
||||||
|
const weather2 = try provider.fetch(std.testing.allocator, coords);
|
||||||
|
defer weather2.deinit();
|
||||||
|
|
||||||
|
try std.testing.expect(weather1.forecast.len > 0);
|
||||||
|
try std.testing.expect(weather2.forecast.len > 0);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue