Files
uscogdata/tests/testthat/test-explain.R
T
jared 4de915b557 feat: cog_recipes + recipe= + signposting suggestions
Adds cog_recipes() to list the curated harmonization_recipes catalog (24
recipes / schema_version >= 5), and a recipe= argument on cog_spending()/
cog_revenue() that runs a recipe's generic multi-code join instead of the
category view: SUM(amt * weight) across whichever component codes are
present for a (year, canonical_govid), scoped by gov_type_scope. The join
deliberately does not filter is_aggregate -- the wide era (<= 2011) exposes
these split families (corrections 04+05, IG *89/*47, U4- rents, etc.) ONLY
as aggregate rows, with leaf codes first appearing in 2012, so excluding
aggregates would zero out the wide-era half of every recipe. This is safe
by corpus construction: wide-era rows are aggregate-only, modern rows are
leaf-only, and every component is year-scoped, so there is no
double-counting. recipe= is mutually exclusive with category=; the result's
subtype column reads "recipe" and category reads the recipe's label.

Adds recipe-component-driven signposting: when a basis="harmonized" +
category query comes back with zero rows in a requested year, and a
harmonization recipe covering that category would actually produce rows
for this government in that year (via the same join .run_recipe() uses),
the recipe is surfaced in provenance$suggestions plus one
cli::cli_inform() message. This is deliberately keyed off recipe
components rather than harmonization_map's suggested_recipe_id column
(which is empty on every live row -- the wide era's split families are
NA-by-construction via aggregate exclusion, not an NA ruling to hang a
suggestion off of).

Also populates the previously-always-empty provenance$series_break_refs
(schema v5 only: series_breaks_pq rows whose fin_code is among the
observed codes and whose break_year falls in the requested span), and
extends cog_explain() with Basis/Harmonization/Recipe/Suggestions/Series
breaks sections.
2026-07-18 23:33:02 -04:00

90 lines
3.1 KiB
R

test_that("cog_explain prints verb header and target", {
skip_if_no_corpus()
r <- cog_spending("121011212191", 2020L, "Corrections")
# cli writes to stderr; capture both stdout and message streams.
txt <- paste(c(
capture.output(cog_explain(r)),
capture.output(cog_explain(r), type = "message")
), collapse = "\n")
expect_true(grepl("cog_spending", txt))
expect_true(grepl("Corrections", txt))
expect_true(grepl("121011212191", txt))
})
test_that("cog_explain format='list' returns structured provenance", {
skip_if_no_corpus()
r <- cog_spending("121011212191", 2020L, "Corrections")
prov <- cog_explain(r, format = "list")
expect_identical(prov, attr(r, "provenance"))
})
test_that("cog_explain returns result invisibly for chaining", {
skip_if_no_corpus()
r <- cog_spending("121011212191", 2020L, "Corrections")
res <- withVisible(cog_explain(r))
expect_false(res$visible)
expect_identical(res$value, r)
})
test_that("cog_explain errors on non-verb input", {
df <- tibble::tibble(a = 1)
expect_error(cog_explain(df), "provenance")
})
test_that("cog_explain prints basis + harmonization block", {
skip_if_no_corpus()
r <- cog_spending("121011212191", 2020L, "Corrections")
txt <- paste(c(
capture.output(cog_explain(r)),
capture.output(cog_explain(r), type = "message")
), collapse = "\n")
expect_true(grepl("Basis: harmonized", txt))
expect_true(grepl("Harmonization", txt))
expect_true(grepl("Excluded 0 row", txt))
})
test_that("cog_explain prints a Recipe section for recipe = results", {
skip_if_no_corpus()
r <- cog_spending("121011212191", c(2011L, 2012L), recipe = "corrections_combined")
txt <- paste(c(
capture.output(cog_explain(r)),
capture.output(cog_explain(r), type = "message")
), collapse = "\n")
expect_true(grepl("Recipe", txt))
expect_true(grepl("corrections_combined", txt))
expect_true(grepl("E04", txt))
expect_true(grepl("E05", txt))
})
test_that("cog_explain prints a Suggestions section when the provenance has one", {
skip_if_no_corpus()
r <- suppressMessages(
cog_spending("121011212191", c(2011L, 2012L), category = "Corrections")
)
txt <- paste(c(
capture.output(cog_explain(r)),
capture.output(cog_explain(r), type = "message")
), collapse = "\n")
expect_true(grepl("Suggestions", txt))
expect_true(grepl("corrections_combined", txt))
expect_true(grepl("re-run with recipe", txt))
})
test_that("cog_explain prints denominator + popyear_range + counts", {
skip_if_no_corpus()
with_fixture_corpus({
r <- cog_spending("121011212191", years = 2019:2020,
category = "Police", per_capita = TRUE)
out <- paste(c(
capture.output(cog_explain(r)),
capture.output(cog_explain(r), type = "message")
), collapse = "\n")
expect_true(grepl("Census F-33", out))
expect_true(grepl("popyear", out, ignore.case = TRUE))
expect_true(grepl("census_f33", out))
# popyear_range should render as 4-digit calendar years, not raw 2-digit
expect_true(grepl("2019-2020", out))
expect_false(grepl("popyear range: 19-20", out, fixed = TRUE))
})
})