The balance work added category_type = "balance" rows to the corpus and cog_balances() to read them, but left cog_categories() -- the discovery surface -- unable to describe them: - subtype COALESCEd only spend_subtype and revenue_subtype, so every balance row came back with subtype = NA - type rejected "balance", so there was no way to ask for the holdings taxonomy at all Both matter downstream: cog-api derives its subtype vocabulary from cog_categories(), so an NA subtype becomes an unusable API parameter. Found while implementing cog-api#26. Note cog_balances() itself still takes no subtype argument -- for holdings category is a strict coarsening of balance_subtype -- but the value belongs in the discovery surface regardless. Tests read the expected subtype set independently from the crosswalk parquet rather than from the function under test.
133 lines
5.4 KiB
R
133 lines
5.4 KiB
R
test_that("cog_categories returns all categories grouped by subtype", {
|
|
skip_if_no_corpus()
|
|
r <- cog_categories()
|
|
expect_s3_class(r, "tbl_df")
|
|
expected <- c("category", "category_type", "subtype",
|
|
"n_codes", "item_codes")
|
|
expect_true(all(expected %in% names(r)))
|
|
expect_gt(nrow(r), 10L)
|
|
# corpus preserves Census-native "expenditure" vocabulary; the API takes
|
|
# "spending" as a friendlier alias.
|
|
#
|
|
# `balance` joined as a third category_type with the cash-and-security
|
|
# holding codes (pipeline#76). `cog_categories()` is a CATALOGUE verb, not a
|
|
# money verb, so it surfaces every category_type the corpus carries -- the
|
|
# stock/flow guard belongs on cog_spending()/cog_revenue(), which must never
|
|
# return a balance row.
|
|
expect_setequal(unique(r$category_type),
|
|
c("expenditure", "revenue", "balance"))
|
|
})
|
|
|
|
test_that("cog_categories(type = 'spending') returns only expenditure rows", {
|
|
skip_if_no_corpus()
|
|
r <- cog_categories(type = "spending")
|
|
expect_true(all(r$category_type == "expenditure"))
|
|
# "assistance" (the J-prefix aid/benefit codes) joined the vocabulary with
|
|
# the crosswalk completion in cog_pipeline#60/#65 -- every flow code
|
|
# carrying dollars now maps to a category.
|
|
# `interest` (I89, I91-I94) and `insurance_benefits` (Y05/Y06/Y14/Y53)
|
|
# joined with the I/Q/Y flow batch -- the last two characters of Census's
|
|
# expenditure taxonomy. `interest` is what makes the three-concept model
|
|
# computable: primary = direct minus debt service.
|
|
expect_true(all(r$subtype %in%
|
|
c("operations", "capital", "intergovernmental", "assistance",
|
|
"interest", "insurance_benefits")))
|
|
})
|
|
|
|
test_that("cog_categories surfaces the intergovernmental spending subtype", {
|
|
skip_if_no_corpus()
|
|
r <- cog_categories(type = "spending")
|
|
expect_true("intergovernmental" %in% r$subtype)
|
|
# IG rows reuse the existing functional categories -- they add a subtype,
|
|
# not new category values.
|
|
ig_cats <- sort(unique(r$category[r$subtype == "intergovernmental"]))
|
|
direct_cats <- sort(unique(r$category[r$subtype != "intergovernmental"]))
|
|
expect_true(all(ig_cats %in% c(direct_cats, "Other Education")))
|
|
})
|
|
|
|
test_that("cog_categories(type = 'revenue') returns only revenue rows", {
|
|
skip_if_no_corpus()
|
|
r <- cog_categories(type = "revenue")
|
|
expect_true(all(r$category_type == "revenue"))
|
|
# The four non-general subtypes are deliberately NOT own_source: Census's
|
|
# General Revenue excludes insurance trust (Y01 alone is $1.31T corpus-wide,
|
|
# plus the employee-retirement X codes), utility (A91-A94) and liquor store
|
|
# (A90) revenue by definition, which is what makes both of its published
|
|
# revenue concepts computable -- see `revenue_concept` in `?cog_revenue`.
|
|
expect_true(all(r$subtype %in%
|
|
c("own_source", "federal", "state", "local_aid",
|
|
"insurance_trust", "utility", "liquor_store")))
|
|
})
|
|
|
|
test_that("cog_categories(pattern = ...) filters case-insensitively", {
|
|
skip_if_no_corpus()
|
|
r <- cog_categories(pattern = "police")
|
|
expect_gt(nrow(r), 0L)
|
|
expect_true(all(grepl("Police", r$category, ignore.case = TRUE)))
|
|
})
|
|
|
|
test_that("cog_categories has one row per (category, subtype)", {
|
|
skip_if_no_corpus()
|
|
r <- cog_categories()
|
|
key <- paste(r$category, r$subtype, sep = "|")
|
|
expect_equal(length(key), length(unique(key)))
|
|
})
|
|
|
|
test_that("cog_categories item_codes is non-empty comma-separated string", {
|
|
skip_if_no_corpus()
|
|
r <- cog_categories()
|
|
expect_true(all(nzchar(r$item_codes)))
|
|
expect_true(all(r$n_codes >= 1L))
|
|
# n_codes should equal count of commas + 1
|
|
expect_equal(r$n_codes,
|
|
vapply(strsplit(r$item_codes, ","), length, integer(1)))
|
|
})
|
|
|
|
test_that("cog_categories sorted by category_type, category, subtype", {
|
|
skip_if_no_corpus()
|
|
r <- cog_categories()
|
|
sorted <- r[order(r$category_type, r$category, r$subtype), ]
|
|
expect_identical(r, sorted)
|
|
})
|
|
|
|
test_that("cog_categories rejects invalid type", {
|
|
expect_error(cog_categories(type = "both"), "type")
|
|
})
|
|
|
|
test_that("cog_categories() surfaces balance subtypes", {
|
|
skip_if_no_corpus()
|
|
with_fixture_corpus({
|
|
cc <- cog_categories()
|
|
b <- cc[cc$category_type == "balance", ]
|
|
expect_true(nrow(b) > 0L)
|
|
|
|
# Every balance row must carry its subtype. Before the COALESCE included
|
|
# balance_subtype these were all NA, which silently made the balance
|
|
# taxonomy undiscoverable -- cog-api derives its subtype vocabulary from
|
|
# this function, so an NA here becomes an unusable API parameter.
|
|
expect_false(any(is.na(b$subtype)))
|
|
|
|
# The exact set, read independently from the crosswalk rather than from
|
|
# the function under test.
|
|
con2 <- DBI::dbConnect(duckdb::duckdb())
|
|
on.exit(DBI::dbDisconnect(con2, shutdown = TRUE), add = TRUE)
|
|
p <- file.path(fixture_corpus_path(), "data", "summary_categories.parquet")
|
|
want <- DBI::dbGetQuery(con2, sprintf(
|
|
"SELECT DISTINCT balance_subtype FROM read_parquet(%s)
|
|
WHERE category_type = 'balance' AND balance_subtype IS NOT NULL
|
|
ORDER BY 1", uscogdata:::.sql_lit_chr(p)))$balance_subtype
|
|
expect_true(length(want) > 1L)
|
|
expect_identical(sort(unique(b$subtype)), sort(want))
|
|
})
|
|
})
|
|
|
|
test_that('cog_categories(type = "balance") filters to holdings', {
|
|
skip_if_no_corpus()
|
|
with_fixture_corpus({
|
|
b <- cog_categories(type = "balance")
|
|
expect_true(nrow(b) > 0L)
|
|
expect_identical(unique(b$category_type), "balance")
|
|
expect_false(any(is.na(b$subtype)))
|
|
})
|
|
})
|