milestone 24: persist and surface the unsupported-line count
This commit is contained in:
@@ -11,8 +11,8 @@
|
||||
//! Runtime columns are deliberately absent. `clients.first_seen`,
|
||||
//! `clients.last_seen`, `rules.created_at` and
|
||||
//! `blocklist_sources.{last_updated, domain_count, wildcard_count,
|
||||
//! exception_count, skipped_regex_count, checksum}` are facts a running server
|
||||
//! produces, not configuration. Including them would make two exports taken
|
||||
//! exception_count, skipped_regex_count, skipped_unsupported_count, checksum}`
|
||||
//! are facts a running server produces, not configuration. Including them would make two exports taken
|
||||
//! minutes apart differ, which would make the byte-stable round trip untestable
|
||||
//! against a live server.
|
||||
//!
|
||||
|
||||
@@ -1102,6 +1102,7 @@ fn seedSourceStats(database: *db.Db, id: i64) !void {
|
||||
.wildcard_count = 21,
|
||||
.exception_count = 9,
|
||||
.skipped_regex_count = 7,
|
||||
.skipped_unsupported_count = 33,
|
||||
.checksum = "a" ** 64,
|
||||
});
|
||||
}
|
||||
@@ -1171,6 +1172,8 @@ test "a source keeps its id, its checksum and its counters across a reconcile" {
|
||||
try testing.expectEqual(@as(?i64, 1_700_000_000), row.last_updated);
|
||||
try testing.expectEqual(@as(i64, 4321), row.domain_count);
|
||||
try testing.expectEqual(@as(i64, 9), row.exception_count);
|
||||
try testing.expectEqual(@as(i64, 7), row.skipped_regex_count);
|
||||
try testing.expectEqual(@as(i64, 33), row.skipped_unsupported_count);
|
||||
try testing.expectEqualStrings("a" ** 64, row.checksum.?);
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,27 @@ const http_domains: i64 = 2;
|
||||
const http_wildcards: i64 = 1;
|
||||
const http_exceptions: i64 = 0;
|
||||
const http_regex: i64 = 1;
|
||||
/// Zero, and asserted rather than assumed: a hosts list carries no line a DNS
|
||||
/// sinkhole cannot translate, which is what makes the abp fixture below a
|
||||
/// separate source instead of two more lines in this one.
|
||||
const http_unsupported: i64 = 0;
|
||||
|
||||
/// An ABP-format list: one element-hiding rule and one `$`-modifier rule that
|
||||
/// nxdns counts and skips, beside two names it blocks. `detectFormat` assigns
|
||||
/// one format to a whole source, so these lines cannot join `http_body` — a
|
||||
/// single `##` there would re-parse every hosts line as ABP.
|
||||
const abp_body =
|
||||
"! small abp list\n" ++
|
||||
"##.ad-banner\n" ++
|
||||
"||ads.example^$third-party\n" ++
|
||||
"||blocked.example^\n" ++
|
||||
"tracker.example\n";
|
||||
|
||||
/// `||blocked.example^` covers its own apex, so it writes one `.list` entry
|
||||
/// beside its `.wild` one; `tracker.example` writes the second.
|
||||
const abp_domains: i64 = 2;
|
||||
const abp_wildcards: i64 = 1;
|
||||
const abp_unsupported: i64 = 2;
|
||||
|
||||
/// Compiles `text` into `<base>.list`, `<base>.wild` and `<base>.allow` under
|
||||
/// `dir`, exactly as the manager's compile stage does, and returns the
|
||||
@@ -393,7 +414,7 @@ const Env = struct {
|
||||
// fixtures: the loopback http server
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const Route = enum(u8) { body, redirect, not_found, oversize, chunked, stall };
|
||||
const Route = enum(u8) { body, redirect, not_found, oversize, chunked, stall, changed };
|
||||
|
||||
/// How long the `stall` route holds a reply open when nothing releases it.
|
||||
///
|
||||
@@ -440,6 +461,11 @@ const oversize_length = "104857600";
|
||||
const HttpFixture = struct {
|
||||
server: net.Server,
|
||||
body: []const u8,
|
||||
/// What the `changed` route serves: the same list after its author edited
|
||||
/// it. A test sets this before the serving task starts and reaches it by
|
||||
/// switching the route, so the two bodies are read through the atomic that
|
||||
/// selects them and never written beside a request in flight.
|
||||
changed_body: []const u8,
|
||||
route: std.atomic.Value(u8),
|
||||
/// Connections accepted, whatever came over them. A test that claims a pass
|
||||
/// downloaded nothing reads this rather than the route counters: a refetch
|
||||
@@ -466,6 +492,7 @@ const HttpFixture = struct {
|
||||
return .{
|
||||
.server = try local.listen(io, .{ .reuse_address = true }),
|
||||
.body = body,
|
||||
.changed_body = "",
|
||||
.route = .init(@intFromEnum(Route.body)),
|
||||
.accepted = .init(0),
|
||||
.flushed_parts = .init(0),
|
||||
@@ -514,6 +541,7 @@ const HttpFixture = struct {
|
||||
fn respond(self: *HttpFixture, io: std.Io, request: *std.http.Server.Request) !void {
|
||||
switch (@as(Route, @enumFromInt(self.route.load(.acquire)))) {
|
||||
.body => try request.respond(self.body, .{ .keep_alive = false }),
|
||||
.changed => try request.respond(self.changed_body, .{ .keep_alive = false }),
|
||||
.redirect => if (std.mem.eql(u8, request.head.target, redirect_path))
|
||||
try request.respond(self.body, .{ .keep_alive = false })
|
||||
else
|
||||
@@ -824,6 +852,7 @@ test "3: a damaged compiled file never replaces a serving snapshot with a worse
|
||||
.wildcard_count = 1,
|
||||
.exception_count = 0,
|
||||
.skipped_regex_count = 0,
|
||||
.skipped_unsupported_count = 0,
|
||||
.checksum = &bodyChecksum(good_list, good_wild, ""),
|
||||
});
|
||||
|
||||
@@ -856,6 +885,7 @@ test "3: a damaged compiled file never replaces a serving snapshot with a worse
|
||||
.wildcard_count = 1,
|
||||
.exception_count = 0,
|
||||
.skipped_regex_count = 0,
|
||||
.skipped_unsupported_count = 0,
|
||||
.checksum = &bodyChecksum(unsorted_list, good_wild, ""),
|
||||
});
|
||||
|
||||
@@ -913,6 +943,7 @@ test "4: a 200 response is fetched, compiled, recorded and served" {
|
||||
try testing.expectEqual(http_domains, row.domain_count);
|
||||
try testing.expectEqual(http_wildcards, row.wildcard_count);
|
||||
try testing.expectEqual(http_regex, row.skipped_regex_count);
|
||||
try testing.expectEqual(http_unsupported, row.skipped_unsupported_count);
|
||||
try testing.expectEqual(@as(usize, 64), (row.checksum orelse return error.TestNoChecksum).len);
|
||||
try testing.expect(row.last_updated != null);
|
||||
|
||||
@@ -1073,6 +1104,7 @@ test "8: refetching identical content skips the rewrite and still moves last_upd
|
||||
.wildcard_count = http_wildcards,
|
||||
.exception_count = http_exceptions,
|
||||
.skipped_regex_count = http_regex,
|
||||
.skipped_unsupported_count = http_unsupported,
|
||||
.checksum = blk: {
|
||||
var rows = try listRows(&env.database);
|
||||
defer rows.deinit();
|
||||
@@ -1143,6 +1175,7 @@ fn publishFixtureFiles(env: *Env, id: i64, list_body: []const u8, wild_body: []c
|
||||
.wildcard_count = 0,
|
||||
.exception_count = 0,
|
||||
.skipped_regex_count = 0,
|
||||
.skipped_unsupported_count = 0,
|
||||
.checksum = &bodyChecksum(list_body, wild_body, ""),
|
||||
});
|
||||
}
|
||||
@@ -1255,6 +1288,7 @@ test "10b: the scheduler sweeps orphans on its own, with no operator call" {
|
||||
.wildcard_count = 0,
|
||||
.exception_count = 0,
|
||||
.skipped_regex_count = 0,
|
||||
.skipped_unsupported_count = 0,
|
||||
.checksum = &bodyChecksum(list_body, "", ""),
|
||||
});
|
||||
|
||||
@@ -2156,6 +2190,7 @@ test "20: a data directory written before exceptions existed loads with no check
|
||||
.wildcard_count = 1,
|
||||
.exception_count = 0,
|
||||
.skipped_regex_count = 0,
|
||||
.skipped_unsupported_count = 0,
|
||||
.checksum = &legacy_checksum,
|
||||
});
|
||||
|
||||
@@ -2170,3 +2205,139 @@ test "20: a data directory written before exceptions existed loads with no check
|
||||
try testing.expect(decision.blocked);
|
||||
try testing.expect((try env.evaluate("x.ccc.example.com"))[0].blocked);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 21–22: the unsupported count, from the compile to the database and back
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test "21: an abp list's unsupported lines are counted, persisted and read back" {
|
||||
if (!build_options.integration) return error.SkipZigTest;
|
||||
|
||||
const gpa = testing.allocator;
|
||||
const env = try Env.create(gpa);
|
||||
defer env.destroy();
|
||||
const io = env.io();
|
||||
|
||||
var fixture = try HttpFixture.init(io, abp_body);
|
||||
defer fixture.deinit(io);
|
||||
var group: std.Io.Group = .init;
|
||||
defer group.cancel(io);
|
||||
try group.concurrent(io, HttpFixture.serve, .{ &fixture, io });
|
||||
|
||||
var url_buf: [64]u8 = undefined;
|
||||
const url = try fixture.url(&url_buf);
|
||||
const id = try seedSource(&env.database, url);
|
||||
|
||||
try testing.expect(try refreshOnce(env, url));
|
||||
try env.mgr.reload(io);
|
||||
|
||||
var rows = try listRows(&env.database);
|
||||
defer rows.deinit();
|
||||
const row = try rows.byUrl(url);
|
||||
try testing.expectEqual(abp_domains, row.domain_count);
|
||||
try testing.expectEqual(abp_wildcards, row.wildcard_count);
|
||||
try testing.expectEqual(@as(i64, 0), row.skipped_regex_count);
|
||||
try testing.expectEqual(abp_unsupported, row.skipped_unsupported_count);
|
||||
|
||||
const status = try env.status(id);
|
||||
try testing.expectEqual(manager.State.ok, status.state);
|
||||
try testing.expectEqual(@as(u32, @intCast(abp_unsupported)), status.counts.skipped_unsupported);
|
||||
|
||||
// What the number costs the operator: the `$`-modifier rule named a domain
|
||||
// and blocked nothing, while the two lines nxdns could translate did block.
|
||||
try testing.expect(!(try env.evaluate("ads.example"))[0].blocked);
|
||||
try testing.expect((try env.evaluate("blocked.example"))[0].blocked);
|
||||
try testing.expect((try env.evaluate("tracker.example"))[0].blocked);
|
||||
}
|
||||
|
||||
/// The same list before and after its author edited only lines nxdns skips.
|
||||
/// The written entries are identical in both, so the two compiles produce one
|
||||
/// checksum and the refresh takes the unchanged-checksum path.
|
||||
const churn_before =
|
||||
"! churn fixture\n" ++
|
||||
"##.ad-one\n" ++
|
||||
"||blocked.example^\n" ++
|
||||
"tracker.example\n";
|
||||
|
||||
const churn_after =
|
||||
"! churn fixture\n" ++
|
||||
"##.ad-one\n" ++
|
||||
"##.ad-two\n" ++
|
||||
"/ads[0-9]+/\n" ++
|
||||
"||blocked.example^\n" ++
|
||||
"tracker.example\n";
|
||||
|
||||
test "22: a list that changed only its skipped lines still updates both skip counters" {
|
||||
if (!build_options.integration) return error.SkipZigTest;
|
||||
|
||||
const gpa = testing.allocator;
|
||||
const env = try Env.create(gpa);
|
||||
defer env.destroy();
|
||||
const io = env.io();
|
||||
|
||||
var fixture = try HttpFixture.init(io, churn_before);
|
||||
fixture.changed_body = churn_after;
|
||||
defer fixture.deinit(io);
|
||||
var group: std.Io.Group = .init;
|
||||
defer group.cancel(io);
|
||||
try group.concurrent(io, HttpFixture.serve, .{ &fixture, io });
|
||||
|
||||
var url_buf: [64]u8 = undefined;
|
||||
const url = try fixture.url(&url_buf);
|
||||
const id = try seedSource(&env.database, url);
|
||||
|
||||
try testing.expect(try refreshOnce(env, url));
|
||||
|
||||
var first_checksum: [64]u8 = undefined;
|
||||
{
|
||||
var rows = try listRows(&env.database);
|
||||
defer rows.deinit();
|
||||
const row = try rows.byUrl(url);
|
||||
try testing.expectEqual(@as(i64, 0), row.skipped_regex_count);
|
||||
try testing.expectEqual(@as(i64, 1), row.skipped_unsupported_count);
|
||||
@memcpy(&first_checksum, row.checksum orelse return error.TestNoChecksum);
|
||||
}
|
||||
|
||||
// The edited list. Two more skipped lines and not one written entry moved,
|
||||
// so the refresh finds its stored checksum and rewrites nothing on disk.
|
||||
fixture.setRoute(.changed);
|
||||
try testing.expect(!try refreshOnce(env, url));
|
||||
|
||||
{
|
||||
var rows = try listRows(&env.database);
|
||||
defer rows.deinit();
|
||||
const row = try rows.byUrl(url);
|
||||
try testing.expectEqualStrings(&first_checksum, row.checksum orelse return error.TestNoChecksum);
|
||||
// The three written counts are the ones an unchanged checksum vouches
|
||||
// for; the two skip counts are the ones it says nothing about.
|
||||
try testing.expectEqual(abp_domains, row.domain_count);
|
||||
try testing.expectEqual(abp_wildcards, row.wildcard_count);
|
||||
try testing.expectEqual(@as(i64, 1), row.skipped_regex_count);
|
||||
try testing.expectEqual(@as(i64, 2), row.skipped_unsupported_count);
|
||||
}
|
||||
|
||||
const live = try env.status(id);
|
||||
try testing.expectEqual(@as(u32, 1), live.counts.skipped_regex);
|
||||
try testing.expectEqual(@as(u32, 2), live.counts.skipped_unsupported);
|
||||
|
||||
// The restart. A new manager over the same database and the same files
|
||||
// carries nothing across in memory, so the status it publishes is what
|
||||
// rehydration read out of the row — which is the only reason writing the
|
||||
// fresh counts above matters.
|
||||
env.mgr.deinit(io);
|
||||
env.mgr = try manager.Manager.init(
|
||||
gpa,
|
||||
&env.database,
|
||||
.{ .dir = env.tmp.dir },
|
||||
&env.f,
|
||||
.{ .enabled = false },
|
||||
budget,
|
||||
);
|
||||
try env.mgr.reload(io);
|
||||
|
||||
const restored = try env.status(id);
|
||||
try testing.expectEqual(manager.State.ok, restored.state);
|
||||
try testing.expect(restored.loaded);
|
||||
try testing.expectEqual(@as(u32, 1), restored.counts.skipped_regex);
|
||||
try testing.expectEqual(@as(u32, 2), restored.counts.skipped_unsupported);
|
||||
}
|
||||
|
||||
+19
-2
@@ -822,12 +822,21 @@ pub const Manager = struct {
|
||||
if (std.mem.eql(u8, stored, &compiled.result.checksum) and
|
||||
self.diskBodiesMatch(io, dir, row.id, stored))
|
||||
{
|
||||
// The three written counts come from the row: the checksum
|
||||
// covers the three bodies, so an unchanged checksum means an
|
||||
// unchanged number of entries in each. The two skip counts do
|
||||
// not: a skipped line lands in no body, so a list that changed
|
||||
// only its regex or browser-syntax lines arrives here with a
|
||||
// stale row and a fresh compile. Taking them from the row would
|
||||
// show the operator one number and restore another after a
|
||||
// restart.
|
||||
try sources_repo.updateSourceStats(self.database, row.id, .{
|
||||
.last_updated = now,
|
||||
.domain_count = row.domain_count,
|
||||
.wildcard_count = row.wildcard_count,
|
||||
.exception_count = row.exception_count,
|
||||
.skipped_regex_count = row.skipped_regex_count,
|
||||
.skipped_regex_count = compiled.result.counts.skipped_regex,
|
||||
.skipped_unsupported_count = compiled.result.counts.skipped_unsupported,
|
||||
.checksum = stored,
|
||||
});
|
||||
status.succeed(now, compiled.result.counts);
|
||||
@@ -857,6 +866,7 @@ pub const Manager = struct {
|
||||
.wildcard_count = compiled.result.counts.wildcards,
|
||||
.exception_count = compiled.result.counts.exceptions,
|
||||
.skipped_regex_count = compiled.result.counts.skipped_regex,
|
||||
.skipped_unsupported_count = compiled.result.counts.skipped_unsupported,
|
||||
.checksum = &compiled.result.checksum,
|
||||
});
|
||||
status.succeed(now, compiled.result.counts);
|
||||
@@ -1537,7 +1547,7 @@ fn applyLoadOutcomes(
|
||||
entry.loaded = true;
|
||||
// Two states survive a successful load. `.ok`, because a
|
||||
// refresh in this process already filled the counters the
|
||||
// compile produced and the three database columns are a subset
|
||||
// compile produced and the five database columns are a subset
|
||||
// of them. And any refresh failure, because the files that just
|
||||
// loaded are exactly the ones the failed refresh could not
|
||||
// replace, so the operator must still see why.
|
||||
@@ -1547,6 +1557,7 @@ fn applyLoadOutcomes(
|
||||
.wildcards = countOf(row.wildcard_count),
|
||||
.exceptions = countOf(row.exception_count),
|
||||
.skipped_regex = countOf(row.skipped_regex_count),
|
||||
.skipped_unsupported = countOf(row.skipped_unsupported_count),
|
||||
});
|
||||
},
|
||||
.failed => |reason| {
|
||||
@@ -1926,6 +1937,7 @@ test "a canceled compiled-file read cancels the reload instead of recording it"
|
||||
.domain_count = 1,
|
||||
.wildcard_count = 0,
|
||||
.skipped_regex_count = 0,
|
||||
.skipped_unsupported_count = 0,
|
||||
.exception_count = 0,
|
||||
.checksum = &bodyChecksum(list_body, wild_body, allow_body),
|
||||
});
|
||||
@@ -2049,6 +2061,7 @@ test "the log label names a source without printing what its url carries" {
|
||||
.domain_count = 0,
|
||||
.wildcard_count = 0,
|
||||
.skipped_regex_count = 0,
|
||||
.skipped_unsupported_count = 0,
|
||||
.checksum = null,
|
||||
};
|
||||
const printed = try std.fmt.bufPrint(&buf, "blocklist {f}: download failed: {s}", .{
|
||||
@@ -2223,6 +2236,7 @@ fn testRow(id: i64, enabled: bool) sources_repo.SourceRow {
|
||||
.wildcard_count = 4,
|
||||
.exception_count = 2,
|
||||
.skipped_regex_count = 1,
|
||||
.skipped_unsupported_count = 5,
|
||||
.checksum = "0" ** 64,
|
||||
};
|
||||
}
|
||||
@@ -2333,6 +2347,9 @@ test "a load of a source this process never refreshed takes the row counters" {
|
||||
try testing.expectEqual(@as(u32, 9), statuses[0].counts.domains);
|
||||
try testing.expectEqual(@as(u32, 4), statuses[0].counts.wildcards);
|
||||
try testing.expectEqual(@as(u32, 1), statuses[0].counts.skipped_regex);
|
||||
// Rehydration: a restart reads this from the row and nowhere else, because
|
||||
// no path reparses a compiled file's header.
|
||||
try testing.expectEqual(@as(u32, 5), statuses[0].counts.skipped_unsupported);
|
||||
}
|
||||
|
||||
test "a status borrows nothing, so a copy outlives the table it came from" {
|
||||
|
||||
@@ -61,6 +61,7 @@ pub const ddl_v1: [:0]const u8 =
|
||||
\\ wildcard_count INTEGER NOT NULL DEFAULT 0,
|
||||
\\ exception_count INTEGER NOT NULL DEFAULT 0,
|
||||
\\ skipped_regex_count INTEGER NOT NULL DEFAULT 0,
|
||||
\\ skipped_unsupported_count INTEGER NOT NULL DEFAULT 0,
|
||||
\\ checksum TEXT
|
||||
\\);
|
||||
\\
|
||||
|
||||
@@ -271,6 +271,7 @@ test "a fresh database reaches the baseline with every v1 column and rule kind"
|
||||
try testing.expectEqual(@as(u32, 1), target_version);
|
||||
try testing.expect(try columnExists(&database, "upstreams", "tls_name"));
|
||||
try testing.expect(try columnExists(&database, "blocklist_sources", "exception_count"));
|
||||
try testing.expect(try columnExists(&database, "blocklist_sources", "skipped_unsupported_count"));
|
||||
|
||||
try database.exec(
|
||||
\\INSERT INTO rules (group_id, pattern, kind, action, created_at)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
//! `blocklist_sources`.
|
||||
//!
|
||||
//! Only the four configuration columns are read and written. `last_updated`,
|
||||
//! `domain_count`, `wildcard_count`, `exception_count`, `skipped_regex_count`
|
||||
//! and `checksum` are facts a running server produces; an insert leaves them at
|
||||
//! their column defaults so two exports taken minutes apart stay identical.
|
||||
//! `domain_count`, `wildcard_count`, `exception_count`, `skipped_regex_count`,
|
||||
//! `skipped_unsupported_count` and `checksum` are facts a running server
|
||||
//! produces; an insert leaves them at their column defaults so two exports taken
|
||||
//! minutes apart stay identical.
|
||||
//!
|
||||
//! The import path is list / insert / deleteAll / count; the runtime columns and
|
||||
//! the REST surface follow it, both keyed by row id.
|
||||
@@ -93,6 +94,12 @@ pub const SourceRow = struct {
|
||||
/// builds `SourceRow` values from the refresh columns alone.
|
||||
exception_count: i64 = 0,
|
||||
skipped_regex_count: i64,
|
||||
/// Lines the compiler read and could not translate into a DNS decision:
|
||||
/// cosmetic element hiding, `$`-modifier rules (save the tolerated
|
||||
/// `$important` exception suffix, which lands in `exception_count`), scheme
|
||||
/// anchors. Counted and not written, like `skipped_regex_count` and unlike
|
||||
/// the three counts above.
|
||||
skipped_unsupported_count: i64,
|
||||
checksum: ?[]const u8,
|
||||
};
|
||||
|
||||
@@ -102,6 +109,7 @@ pub const SourceStats = struct {
|
||||
wildcard_count: i64,
|
||||
exception_count: i64,
|
||||
skipped_regex_count: i64,
|
||||
skipped_unsupported_count: i64,
|
||||
/// Lowercase hex sha256 over the `.list` body, then the `.wild` body, then
|
||||
/// the `.allow` body. The allow body is hashed last so an empty one leaves
|
||||
/// the digest of a two-body compile unchanged, which is what keeps a
|
||||
@@ -112,7 +120,7 @@ pub const SourceStats = struct {
|
||||
const row_columns_sql =
|
||||
\\SELECT id, url, name, enabled, last_updated,
|
||||
\\ domain_count, wildcard_count, skipped_regex_count, checksum,
|
||||
\\ is_suggested, exception_count
|
||||
\\ is_suggested, exception_count, skipped_unsupported_count
|
||||
\\ FROM blocklist_sources
|
||||
;
|
||||
|
||||
@@ -143,6 +151,7 @@ fn readSourceRow(stmt: *db.Stmt, gpa: Allocator) db.Error!SourceRow {
|
||||
.wildcard_count = stmt.columnInt(6),
|
||||
.exception_count = stmt.columnInt(10),
|
||||
.skipped_regex_count = stmt.columnInt(7),
|
||||
.skipped_unsupported_count = stmt.columnInt(11),
|
||||
.checksum = checksum,
|
||||
};
|
||||
}
|
||||
@@ -159,7 +168,8 @@ pub fn freeSourceRows(gpa: Allocator, items: []const SourceRow) void {
|
||||
const update_stats_sql =
|
||||
\\UPDATE blocklist_sources
|
||||
\\ SET last_updated = ?2, domain_count = ?3, wildcard_count = ?4,
|
||||
\\ skipped_regex_count = ?5, checksum = ?6, exception_count = ?7
|
||||
\\ skipped_regex_count = ?5, checksum = ?6, exception_count = ?7,
|
||||
\\ skipped_unsupported_count = ?8
|
||||
\\ WHERE id = ?1
|
||||
;
|
||||
|
||||
@@ -175,6 +185,7 @@ pub fn updateSourceStats(database: *db.Db, id: i64, stats: SourceStats) db.Error
|
||||
try stmt.bindInt(5, stats.skipped_regex_count);
|
||||
try stmt.bindText(6, stats.checksum);
|
||||
try stmt.bindInt(7, stats.exception_count);
|
||||
try stmt.bindInt(8, stats.skipped_unsupported_count);
|
||||
try stmt.exec();
|
||||
}
|
||||
|
||||
@@ -315,8 +326,11 @@ test "insertBlocklistSource leaves the runtime columns at their defaults" {
|
||||
try testing.expectEqual(
|
||||
@as(i64, 0),
|
||||
try database.queryInt(
|
||||
"SELECT sum(domain_count + wildcard_count + exception_count + skipped_regex_count)" ++
|
||||
" FROM blocklist_sources",
|
||||
// Every runtime counter, summed only because this asserts they are
|
||||
// all 0. No production query may add the two skip counters to the
|
||||
// three written ones: a skipped line was never written.
|
||||
"SELECT sum(domain_count + wildcard_count + exception_count + skipped_regex_count" ++
|
||||
" + skipped_unsupported_count) FROM blocklist_sources",
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -370,6 +384,7 @@ test "listSourceRows returns row ids and the runtime columns in url order" {
|
||||
try testing.expectEqual(@as(i64, 0), row.wildcard_count);
|
||||
try testing.expectEqual(@as(i64, 0), row.exception_count);
|
||||
try testing.expectEqual(@as(i64, 0), row.skipped_regex_count);
|
||||
try testing.expectEqual(@as(i64, 0), row.skipped_unsupported_count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,6 +404,7 @@ test "updateSourceStats writes the runtime columns of one source only" {
|
||||
.wildcard_count = 21,
|
||||
.exception_count = 9,
|
||||
.skipped_regex_count = 7,
|
||||
.skipped_unsupported_count = 15,
|
||||
.checksum = "a" ** 64,
|
||||
});
|
||||
|
||||
@@ -406,6 +422,7 @@ test "updateSourceStats writes the runtime columns of one source only" {
|
||||
try testing.expectEqual(@as(i64, 21), updated.wildcard_count);
|
||||
try testing.expectEqual(@as(i64, 9), updated.exception_count);
|
||||
try testing.expectEqual(@as(i64, 7), updated.skipped_regex_count);
|
||||
try testing.expectEqual(@as(i64, 15), updated.skipped_unsupported_count);
|
||||
try testing.expectEqualStrings("a" ** 64, updated.checksum.?);
|
||||
|
||||
// The two untouched rows kept their defaults.
|
||||
@@ -423,6 +440,7 @@ fn listSourceRowsUnderFailure(gpa: Allocator) !void {
|
||||
.wildcard_count = 3,
|
||||
.exception_count = 5,
|
||||
.skipped_regex_count = 4,
|
||||
.skipped_unsupported_count = 6,
|
||||
.checksum = "b" ** 64,
|
||||
});
|
||||
|
||||
@@ -489,6 +507,7 @@ test "updateSource leaves the runtime columns where the refresh path left them"
|
||||
.wildcard_count = 3,
|
||||
.exception_count = 2,
|
||||
.skipped_regex_count = 1,
|
||||
.skipped_unsupported_count = 4,
|
||||
.checksum = "c" ** 64,
|
||||
});
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ pub const StatusView = struct {
|
||||
wildcards: u32,
|
||||
exceptions: u32,
|
||||
skipped_regex: u32,
|
||||
skipped_unsupported: u32,
|
||||
|
||||
pub fn from(status: *const manager_mod.SourceStatus) StatusView {
|
||||
return .{
|
||||
@@ -75,6 +76,7 @@ pub const StatusView = struct {
|
||||
.wildcards = status.counts.wildcards,
|
||||
.exceptions = status.counts.exceptions,
|
||||
.skipped_regex = status.counts.skipped_regex,
|
||||
.skipped_unsupported = status.counts.skipped_unsupported,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -321,6 +323,7 @@ test "editing a blocklist keeps the counters the refresh wrote" {
|
||||
.wildcard_count = 3,
|
||||
.exception_count = 2,
|
||||
.skipped_regex_count = 1,
|
||||
.skipped_unsupported_count = 8,
|
||||
.checksum = "abc",
|
||||
});
|
||||
|
||||
@@ -335,6 +338,8 @@ test "editing a blocklist keeps the counters the refresh wrote" {
|
||||
try testing.expectEqualStrings("renamed", row.name);
|
||||
try testing.expect(!row.enabled);
|
||||
try testing.expectEqual(@as(i64, 42), row.domain_count);
|
||||
try testing.expectEqual(@as(i64, 1), row.skipped_regex_count);
|
||||
try testing.expectEqual(@as(i64, 8), row.skipped_unsupported_count);
|
||||
try testing.expectEqual(@as(usize, 2), bench.reloads);
|
||||
}
|
||||
|
||||
@@ -383,7 +388,13 @@ test "a status becomes the flat shape the API answers with" {
|
||||
const message = "connection refused";
|
||||
@memcpy(status.last_error[0..message.len], message);
|
||||
status.last_error_len = message.len;
|
||||
status.counts = .{ .domains = 10, .wildcards = 2, .exceptions = 4, .skipped_regex = 1 };
|
||||
status.counts = .{
|
||||
.domains = 10,
|
||||
.wildcards = 2,
|
||||
.exceptions = 4,
|
||||
.skipped_regex = 1,
|
||||
.skipped_unsupported = 6,
|
||||
};
|
||||
|
||||
const view: StatusView = .from(&status);
|
||||
try testing.expectEqual(@as(i64, 7), view.id);
|
||||
@@ -393,4 +404,6 @@ test "a status becomes the flat shape the API answers with" {
|
||||
try testing.expectEqualStrings(message, view.last_error);
|
||||
try testing.expectEqual(@as(u32, 10), view.domains);
|
||||
try testing.expectEqual(@as(u32, 4), view.exceptions);
|
||||
try testing.expectEqual(@as(u32, 1), view.skipped_regex);
|
||||
try testing.expectEqual(@as(u32, 6), view.skipped_unsupported);
|
||||
}
|
||||
|
||||
@@ -1876,7 +1876,7 @@ components:
|
||||
|
||||
Blocklist:
|
||||
type: object
|
||||
required: [id, url, name, enabled, is_suggested, last_updated, domain_count, wildcard_count, exception_count, skipped_regex_count, checksum]
|
||||
required: [id, url, name, enabled, is_suggested, last_updated, domain_count, wildcard_count, exception_count, skipped_regex_count, skipped_unsupported_count, checksum]
|
||||
properties:
|
||||
id: { type: integer }
|
||||
url: { type: string }
|
||||
@@ -1890,6 +1890,7 @@ components:
|
||||
wildcard_count: { type: integer }
|
||||
exception_count: { type: integer }
|
||||
skipped_regex_count: { type: integer }
|
||||
skipped_unsupported_count: { type: integer }
|
||||
checksum:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -1919,7 +1920,7 @@ components:
|
||||
|
||||
SourceStatus:
|
||||
type: object
|
||||
required: [id, state, loaded, last_attempt, last_success, url, last_error, domains, wildcards, exceptions, skipped_regex]
|
||||
required: [id, state, loaded, last_attempt, last_success, url, last_error, domains, wildcards, exceptions, skipped_regex, skipped_unsupported]
|
||||
properties:
|
||||
id: { type: integer }
|
||||
state:
|
||||
@@ -1936,6 +1937,7 @@ components:
|
||||
wildcards: { type: integer }
|
||||
exceptions: { type: integer }
|
||||
skipped_regex: { type: integer }
|
||||
skipped_unsupported: { type: integer }
|
||||
|
||||
Rule:
|
||||
type: object
|
||||
|
||||
Reference in New Issue
Block a user