Macros & Testing

Macros turn an interactive session into a replayable YAML recipe. This vignette covers the full macro lifecycle: save, load, replay, parameterise, batch-process, diff, and snapshot-test.


Save and replay

Every step you run on a session is silently recorded. Call save_macro() when you are done:

library(macrox)

sess <- mx_session("report_2024.pdf")

sess |> select_table("monthly", page = 8, method = "bbox")
sess |> rename_columns("monthly", c(Month = "month", Total = "total"))
sess |> cast_types("monthly", c(total = "integer"))
sess |> filter_rows("monthly", exclude_where = "month == 'Total'")

sess |> save_macro("my_report")
# Writes my_report.yml to the working directory

Next year — one call replays every step:

tables <- mx_replay("report_2025.pdf", macro = "my_report")

tables$monthly   # clean data frame, ready to use

Inspect and manage steps

sess |> show_steps()          # numbered list of recorded steps
sess |> remove_step(3)        # drop a step by index
sess |> preview("monthly")    # head(6) with column names
sess |> preview_all()         # all tables: column stats, row totals

The YAML format

my_report.yml looks like this:

macro:
  name: my_report
  created: '2024-11-01 14:22'
  source: report_2024.pdf
  n_steps: 4
steps:
  - step: select_table
    label: monthly
    page: 8
    method: bbox
  - step: rename_columns
    label: monthly
    mapping:
      Month: month
      Total: total
  - step: cast_types
    label: monthly
    cols:
      total: integer
  - step: filter_rows
    label: monthly
    exclude_where: month == 'Total'

The YAML is plain text — commit it to version control, review diffs, and edit it by hand when needed.


Parameterised macros

When the same macro needs to adapt to a value that changes each year — a page number, a year filter, a sheet name — use $parameter placeholders instead of hard-coding the value.

Saving with parameters

sess |> select_table("annual",
  page         = 8,
  method       = "bbox",
  filter_value = "$year"   # placeholder
)
sess |> filter_rows("annual", exclude_where = "year_col != '$year'")

sess |> save_macro("annual_report",
  params = c(year = "integer")   # declare with optional type annotation
  # or just: params = "year"     # bare name, no type
)

The macro YAML gains a params: header:

macro:
  name: annual_report
  params:
    year:
      type: integer
steps:
  - step: filter_rows
    label: annual
    exclude_where: year_col != '$year'

Replaying with parameter values

Supply values at replay time — macrox substitutes every $year occurrence before dispatching any steps:

tables <- mx_replay("report_2025.pdf",
  macro  = "annual_report",
  params = list(year = 2025L)
)

Multiple parameters work the same way:

tables <- mx_replay("report_2025.pdf",
  macro  = "quarterly_report",
  params = list(year = 2025L, quarter = "Q1")
)

Rules


Batch replay

Process a folder of reports in one call:

files <- list.files("reports/", pattern = "\\.pdf$", full.names = TRUE)

results <- mx_replay_batch(files, macro = "my_report")
# results is a named list: one table list per file

Show a progress bar and run in parallel (requires mirai):

results <- mx_replay_batch(files,
  macro     = "my_report",
  .progress = TRUE,
  .parallel = TRUE   # uses mirai for parallel execution
)

Parameterised macros work in batch too — pass a named list:

results <- mx_replay_batch(files,
  macro  = "annual_report",
  params = list(year = 2025L)
)

Comparing outputs with diff_replay()

diff_replay() replays the same macro against two files and returns a structured diff:

d <- diff_replay(
  file1 = "report_2024.pdf",
  file2 = "report_2025.pdf",
  macro = "my_report"
)

print(d)
# ~ monthly [changed] (rows: 12→13, 4 cell changes)
#     total   Δ mean=123.4    max=210.0    sum=1234.5   (4 rows)

Drill into a specific table’s cell-level changes:

detail(d, "monthly")
#   row   col   ref    new    delta
#   3     total 1200   1410   210
#   7     total 980    1001   21

Year-over-year comparison with parameters

d <- diff_replay(
  file1   = "report_2024.pdf",
  file2   = "report_2025.pdf",
  macro   = "annual_report",
  params  = list(year = 2024L),   # applied to file1
  params2 = list(year = 2025L)    # applied to file2
)

Snapshot testing with test_macro()

test_macro() is designed for use inside testthat blocks. On the first call it writes a YAML snapshot; subsequent calls compare against it and fail descriptively if output drifts.

Workflow

  1. Run test_macro() locally with your reference PDF. The snapshot is written to tests/testthat/macro-snapshots/.
  2. Commit the snapshot alongside your package.
  3. In CI, either replay with the same PDF or supply pre-replayed tables. test_macro() compares and fails on any drift.

Basic usage

# tests/testthat/test-my-report.R
test_that("my_report extracts correctly", {
  test_macro("report_2024.pdf", macro = "my_report")
})

The snapshot file tests/testthat/macro-snapshots/my_report.snap.yml records row counts, column names, and the first 5 rows of each table.

Supplying pre-replayed tables

Useful in CI when you don’t want to keep a PDF in the repository:

test_that("monthly table matches snapshot", {
  tables <- mx_replay("report_2024.pdf", macro = "my_report")

  test_macro(
    tables = tables,
    name   = "my_report"   # explicit name required when tables is supplied
  )
})

With parameterised macros

test_that("annual_report with year=2024 matches snapshot", {
  test_macro("report_2024.pdf",
    macro  = "annual_report",
    params = list(year = 2024L),
    name   = "annual_report_2024"
  )
})

Use a unique name per parameter set so snapshots don’t overwrite each other.

Accepting updated output

When the report genuinely changes and you want to update the snapshot:

# From R console or in a one-off script
test_macro("report_2025.pdf",
  macro  = "my_report",
  update = TRUE
)

# Or set the environment variable (testthat::snapshot_accept() sets this)
Sys.setenv(TESTTHAT_SNAPSHOT_UPDATE = "true")

What is captured

The snapshot YAML records per table:

Field Description
nrow Total row count
ncol Column count
cols Column names
head First n_head rows (default 5), stored as character strings
tables:
  monthly:
    nrow: 12
    ncol: 3
    cols:
      - Month
      - Male
      - Total
    head:
      - Month: January
        Male: '1234'
        Total: '2500'
      - Month: February
        Male: '1190'
        Total: '2410'

CI configuration

In .github/workflows/R-CMD-check.yaml, no extra steps are needed — test_macro() reads snapshots committed to the repo. If you use a local PDF that shouldn’t be checked in, supply pre-replayed tables instead.

To regenerate snapshots from CI, add a step that sets TESTTHAT_SNAPSHOT_UPDATE=true and commits the result, or use testthat::snapshot_accept() locally after reviewing the diff.


validate_macro()

validate_macro() checks that a macro YAML is well-formed and that all referenced step types are known before attempting a replay:

validate_macro("my_report")
# v my_report.yml: 4 steps, valid
# x quarterly_report.yml: unknown step 'slect_table' at position 1

Use this in pre-commit hooks or CI to catch typos in hand-edited macros before they reach production.