tests/testthat/test-knowledge.R

# Knowledge tests

# ──────────────────────────────────────────────────────────────────────────────
# Knowledge generation using mini-dsl
# ──────────────────────────────────────────────────────────────────────────────

test_that("Knowledge object is created correctly using mini-DSL", {
  kn <- knowledge(
    tier(
      1 ~ V1 + V2,
      2 ~ V3,
      3 ~ c(V4, V5)
    ),
    V1 %!-->% V3,
    V1 %-->% V2,
    V2 %-->% V3
  )
  expect_equal(
    kn$vars,
    tibble::tibble(
      var = c("V1", "V2", "V3", "V4", "V5"),
      tier = c("1", "1", "2", "3", "3")
    )
  )
  expect_equal(kn$frozen, FALSE)
  expect_equal(
    kn$edges,
    tibble::tibble(
      status = c("forbidden", "required", "required"),
      from = c("V1", "V1", "V2"),
      to = c("V3", "V2", "V3"),
      tier_from = c("1", "1", "1"),
      tier_to = c("2", "1", "2")
    )
  )
  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("1", "2", "3")
    )
  )
})

# seeding with data frame
test_that("seeding Knowledge object with a my_df, matrix, or tibble works", {
  my_df <- data.frame(X1 = 1, X2 = 2, X3 = 3, X4 = 4, check.names = FALSE)
  tbl <- tibble::as_tibble(my_df)
  mat <- as.matrix(my_df)
  kn <-
    knowledge(
      my_df,
      tier(1 ~ X1, 2 ~ X2 + X3),
      X1 %-->% X2
    )
  kn_tbl <- knowledge(
    tbl,
    tier(1 ~ X1, 2 ~ X2 + X3),
    X1 %-->% X2
  )
  kn_mat <- knowledge(
    mat,
    tier(1 ~ X1, 2 ~ X2 + X3),
    X1 %-->% X2
  )
  expect_equal(kn, kn_tbl)
  expect_equal(kn, kn_mat)
  expect_equal(kn$frozen, TRUE)
  expect_equal(
    kn$vars,
    tibble::tibble(
      var = c("X1", "X2", "X3", "X4"),
      tier = c("1", "2", "2", NA_character_)
    )
  )
})

# ──────────────────────────────────────────────────────────────────────────────
# Tiers
# ──────────────────────────────────────────────────────────────────────────────

test_that("tier generation with named tiers using character names", {
  kn <- knowledge(
    tier(
      "One" ~ V1 + V2,
      "Two" ~ V3,
      "Three" ~ V4 + V5
    ),
    V1 %!-->% V3,
    V1 %-->% V2,
    V2 %-->% V3
  )
  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("One", "Two", "Three")
    )
  )
})

test_that("tier generation with named tiers using symbols/expression", {
  kn <- knowledge(
    tier(
      One ~ V1 + V2,
      Two ~ V3,
      Three ~ V4 + V5
    ),
    V1 %!-->% V3,
    V1 %-->% V2,
    V2 %-->% V3
  )
  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("One", "Two", "Three")
    )
  )
})

test_that("tier generation with named tiers using mix of integers, chars, and symbols", {
  kn <- knowledge(
    tier(
      1 ~ V1 + V2,
      Two ~ V3,
      3 ~ V4 + V5,
      "Four" ~ V6,
      Five ~ V7 + V8 + V9
    ),
    V1 %!-->% V3,
    V1 %-->% V2,
    V2 %-->% V3
  )
  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("1", "Two", "3", "Four", "Five")
    )
  )
})

test_that("tier generation with negative numeric tiers errors", {
  expect_error(
    knowledge(
      tier(
        -1 ~ V1 + V2,
      )
    ),
    "`tier` must be a single non-empty label or a non-negative numeric literal.",
    fixed = TRUE
  )
})

######### Tier sorting works correctly ###########

test_that("tier sorts if all numeric", {
  kn <- knowledge(
    tier(
      3 ~ V1 + V2,
      1 ~ V3,
      2 ~ V4 + V5
    )
  )
  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("1", "2", "3")
    )
  )
  expect_equal(kn$vars$tier, c("1", "2", "2", "3", "3"))

  kn <- knowledge(
    tier(
      10 ~ V1 + V2,
      1 ~ V3,
      2 ~ V4 + V5
    )
  )
  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("1", "2", "10")
    )
  )
  expect_equal(kn$vars$tier, c("1", "2", "2", "10", "10"))
})

# ──────────────────────────────────────────────────────────────────────────────
# Tiers using verbs only
# ──────────────────────────────────────────────────────────────────────────────

test_that("tier generation using verbs only", {
  kn <- knowledge() |>
    add_tier(1) |>
    add_tier(2, after = 1) |>
    add_tier(3, after = 2) |>
    add_to_tier(2 ~ V3)

  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("1", "2", "3")
    )
  )
})

test_that("tier generation using verbs only", {
  kn <- knowledge() |>
    add_tier(One) |>
    add_tier(Two, after = One) |>
    add_tier(Three, after = Two) |>
    add_to_tier(Two ~ V3)

  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("One", "Two", "Three")
    )
  )
})

test_that("tier generation using verbs only", {
  kn <- knowledge() |>
    add_tier(One) |>
    add_tier(Three, after = One) |>
    add_tier(Two, before = Three) |>
    add_to_tier(Two ~ V3)

  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("One", "Two", "Three")
    )
  )
})

test_that("tier generation using verbs only", {
  kn <- knowledge() |>
    add_tier(One) |>
    add_tier(2, after = One) |>
    add_tier(Three, after = 2) |>
    add_to_tier(Three ~ V3)

  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("One", "2", "Three")
    )
  )
})

test_that("tier generation with verbs works", {
  kn <- knowledge() |>
    add_tier(1) |>
    add_tier(3, after = 1) |>
    add_tier(Two, before = 3) |>
    add_tier(Two_and_a_Half, after = Two) |>
    add_tier(2.75, before = 3)
  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("1", "Two", "Two_and_a_Half", "2.75", "3")
    )
  )
})

# ──────────────────────────────────────────────────────────────────────────────
# Tiers using verbs and mini-DSL
# ──────────────────────────────────────────────────────────────────────────────

test_that("tier generation with mixing DSL and verbs", {
  kn <- knowledge(
    tier(
      1 ~ V1,
      2 ~ V2
    )
  ) |>
    add_tier(3, after = 2) |>
    add_to_tier(3 ~ V3)

  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("1", "2", "3")
    )
  )
})

test_that("tier generation with mixing DSL and verbs with symbols", {
  kn <- knowledge(
    tier(
      1 ~ V1,
      2 ~ V2
    )
  ) |>
    add_tier(Three, after = 2) |>
    add_to_tier(Three ~ V3)

  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("1", "2", "Three")
    )
  )
})

test_that("tier generation with mixing DSL and verbs with symbols and chars", {
  kn <- knowledge(
    tier(
      "One" ~ V1,
      Three ~ V2
    )
  ) |>
    add_tier(Two, before = "Three") |>
    add_to_tier("Two" ~ V3)

  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("One", "Two", "Three")
    )
  )
})

test_that("tier generation with mixing DSL and verbs with symbols and chars", {
  kn <- knowledge(
    tier(
      "One" ~ V1,
      Three ~ V2
    )
  ) |>
    add_tier(2, after = One) |>
    add_to_tier(2 ~ V3)

  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("One", "2", "Three")
    )
  )
})

test_that("tier generation with mixing DSL and verbs with symbols and chars", {
  kn <- knowledge(
    tier(
      "One" ~ V1,
      Three ~ V2
    )
  ) |>
    add_tier(2, after = One) |>
    add_to_tier(2 ~ V3)

  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("One", "2", "Three")
    )
  )
})

test_that("tier generation with mixing DSL and verbs with symbols and chars", {
  kn <- knowledge(
    tier(
      "One" ~ V1,
      Three ~ V2
    )
  ) |>
    add_tier(Two, before = Three) |>
    add_to_tier(Two ~ V3)

  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("One", "Two", "Three")
    )
  )
})

test_that("tier tidyselect helpers works with +", {
  kn1 <- knowledge(
    tpc_example,
    tier(
      young ~ starts_with(c("child", "youth")),
      old ~ starts_with("old")
    )
  )
  kn2 <- knowledge(
    tpc_example,
    tier(
      young ~ starts_with("child") + starts_with("youth"),
      old ~ starts_with("old")
    )
  )
  expect_equal(kn1, kn2)

  kn3 <- knowledge(
    tpc_example,
    tier(
      young ~ starts_with("child") + ends_with(c("3", "4")),
      old ~ starts_with("old")
    )
  )

  expect_equal(kn1, kn3)
})

# ──────────────────────────────────────────────────────────────────────────────
# Tiers using seq_tiers
# ──────────────────────────────────────────────────────────────────────────────

test_that("tier generation using seq_tiers", {
  my_df <- as.data.frame(
    matrix(
      runif(10), # 10 random numbers in (0,1)
      nrow = 1,
      ncol = 10,
      byrow = TRUE
    )
  )

  names(my_df) <- paste0("X_", 1:10) # label the columns X_1 … X_10

  kn <- knowledge(
    my_df,
    tier(
      seq_tiers(
        1:10,
        ends_with("_{i}")
      )
    ),
    X_1 %-->% X_2
  )
  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = 1:10 |> as.character()
    )
  )
  expect_equal(
    kn$vars,
    tibble::tibble(
      var = paste0("X_", 1:10),
      tier = 1:10 |> as.character()
    )
  )
})

test_that("tier generation using seq_tiers with labels", {
  my_df <- data.frame(
    X_1 = 1,
    X_2 = 2,
    tier3_A = 3,
    Y5_ok = 4,
    check.names = FALSE
  )

  kn <- knowledge(
    my_df,
    tier(
      seq_tiers(1:2, ends_with("_{i}")), # X_1, X_2
      seq_tiers(3, starts_with("tier{i}")), # tier3_…
      seq_tiers(5, matches("Y{i}_")) # exact match
    )
  )
  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = c("1", "2", "3", "5")
    )
  )
  expect_equal(
    kn$vars,
    tibble::tibble(
      var = c("X_1", "X_2", "tier3_A", "Y5_ok"),
      tier = c(1, 2, 3, 5) |> as.character()
    )
  )
})

# ──────────────────────────────────────────────────────────────────────────────
# Tiers using 1:n
# ──────────────────────────────────────────────────────────────────────────────

test_that("tier generation using 1:n", {
  my_df <- as.data.frame(
    matrix(
      runif(10), # 10 random numbers in (0,1)
      nrow = 1,
      ncol = 10,
      byrow = TRUE
    )
  )

  names(my_df) <- paste0("X_", 1:10) # label the columns X_1 … X_10

  kn <-
    knowledge(
      my_df,
      tier(
        1:10
      )
    )
  expect_equal(
    kn$tiers,
    tibble::tibble(
      label = 1:10 |> as.character()
    )
  )
  expect_equal(
    kn$vars,
    tibble::tibble(
      var = paste0("X_", 1:10),
      tier = 1:10 |> as.character()
    )
  )
})


# ──────────────────────────────────────────────────────────────────────────────
# Infix operators errors if my_df and variables don't match
# ──────────────────────────────────────────────────────────────────────────────
test_that("%-->% and %!-->% errors if my_df and variables don't match", {
  my_df <- data.frame(V1 = 1, V2 = 2, check.names = FALSE)
  expect_error(
    knowledge(my_df, 1 %-->% V2),
    "Required edge: no variables matched '1' from the left-hand side.",
    fixed = TRUE
  )
  expect_error(
    knowledge(my_df, V2 %-->% 1),
    "Required edge: no variables matched '1' from the right-hand side.",
    fixed = TRUE
  )

  expect_error(
    knowledge(my_df, 1 %!-->% V2),
    "Forbidden edge: no variables matched '1' from the left-hand side.",
    fixed = TRUE
  )
  expect_error(
    knowledge(my_df, V2 %!-->% 1),
    "Forbidden edge: no variables matched '1' from the right-hand side.",
    fixed = TRUE
  )
})

test_that("forbidden and required errors when no from vars matched", {
  my_df <- data.frame(V1 = 1, V2 = 2, check.names = FALSE)
  expect_error(
    knowledge(
      my_df,
      1 %!-->% V1
    ),
    "Forbidden edge: no variables matched '1' from the left-hand side.",
    fixed = TRUE
  )
  expect_error(
    knowledge(
      my_df,
      1 %-->% V1
    ),
    "Required edge: no variables matched '1' from the left-hand side.",
    fixed = TRUE
  )
})

test_that("forbidden and required errors when no to vars matched", {
  my_df <- data.frame(V1 = 1, V2 = 2, check.names = FALSE)
  expect_error(
    knowledge(
      my_df,
      V1 %!-->% 1
    ),
    "Forbidden edge: no variables matched '1' from the right-hand side.",
    fixed = TRUE
  )
  expect_error(
    knowledge(
      my_df,
      V1 %-->% 1
    ),
    "Required edge: no variables matched '1' from the right-hand side.",
    fixed = TRUE
  )
})

# ──────────────────────────────────────────────────────────────────────────────
# Tier errors
# ──────────────────────────────────────────────────────────────────────────────
test_that("tier() errors if no formulas are supplied", {
  my_df <- tibble::tibble(V1 = 1, V2 = 2)

  expect_error(
    knowledge(my_df, tier()),
    "tier() needs at least one two-sided formula.",
    fixed = TRUE
  )
})

test_that("add_tier() errors for an empty tier label", {
  kn <- knowledge()

  expect_error(
    add_tier(kn, ""), # invalid label
    "`tier` must be a non-empty label.",
    fixed = TRUE
  )
})

test_that("add_tier() errors when the label already exists", {
  kn <- knowledge() |> add_tier(1) # creates tier "1"

  expect_error(
    add_tier(kn, 1), # duplicate
    "Tier label `1` already exists.",
    fixed = TRUE
  )
})

test_that("add_tier() errors if the anchor tier is missing", {
  kn <- knowledge() |> add_tier("first") # only one tier so far

  expect_error(
    add_tier(kn, "second", after = "ghost"), # bad anchor
    "`ghost` does not refer to an existing tier.",
    fixed = TRUE
  )
})

test_that("add_to_tier() errors when adding existing variable to another tier", {
  expect_error(
    knowledge(
      tier(
        One ~ V1 + V2,
        2 ~ V3 + V4,
        "Three" ~ V5
      )
    ) |>
      add_to_tier(One ~ V3 + V4),
    "Cannot reassign variable(s) [V3, V4] to tier `One` using add_to_tier().",
    fixed = TRUE
  )
  expect_error(
    knowledge(
      tier(
        One ~ V1 + V2,
        2 ~ V3 + V4,
        "Three" ~ V5
      )
    ) |>
      add_to_tier(2 ~ V3 + V1),
    "Cannot reassign variable(s) [V1] to tier `2` using add_to_tier().",
    fixed = TRUE
  )
})

test_that("add_to_tier() errors if no formulas are supplied", {
  kn <- knowledge()

  expect_error(
    add_to_tier(kn), # no ...
    "add_to_tier() needs at least one two-sided formula.",
    fixed = TRUE
  )
})

test_that("add_to_tier() errors when a non-formula argument is given", {
  kn <- knowledge()

  expect_error(
    add_to_tier(kn, "oops"), # not a formula
    "Each argument must be a two-sided formula.",
    fixed = TRUE
  )
})

test_that("add_to_tier() errors when the target tier does not exist", {
  kn <- knowledge() # no tiers yet

  expect_error(
    add_to_tier(kn, ghost ~ V1), # lhs tier unknown
    "Tier `ghost` does not exist. Create it first with add_tier().",
    fixed = TRUE
  )
})

test_that("add_to_tier() errors when RHS matches no variables", {
  kn <- knowledge() |> add_tier("T1") # create tier

  # tidyselect call that matches nothing because kn$vars is still empty
  expect_error(
    add_to_tier(kn, T1 ~ starts_with("foo")),
    "matched no variables",
    fixed = FALSE
  )
})

test_that("tier throws error for one variable in two tiers", {
  expect_error(
    knowledge(
      tier(
        1 ~ V1 + V2,
        Two ~ V1,
      )
    ),
    "Tier specification Two ~ V1 tries to re-assign variable(s) [V1] to a new tier.",
    fixed = TRUE
  )
})

test_that("tier throws error for using numeric vector without my_df", {
  expect_error(
    knowledge(
      tier(
        1:10
      )
    ),
    "Using tier(<numeric vector>) needs the data-frame columns first.",
    fixed = TRUE
  )
})

test_that("tier() errors when numeric vector length != ncol(my_df)", {
  my_df <- data.frame(X1 = 1, X2 = 2, X3 = 3, X4 = 4, check.names = FALSE)
  expect_error(
    knowledge(
      my_df,
      tier(
        1:10
      )
    ),
    "Tier vector length (10) must equal number of variables (4).",
    fixed = TRUE
  )
})

test_that("numeric-vector tier() errors on duplicate indices", {
  my_df <- data.frame(A = 1, B = 2, C = 3, check.names = FALSE)

  expect_error(
    knowledge(
      my_df,
      tier(1:3), # first time: creates tiers 1,2,3
      tier(1:3) # second time: should detect 1,2,3 already exist
    ),
    "Tier index(es) 1, 2, 3 already exist.",
    fixed = TRUE
  )
})

test_that("tier() throws error when mispecifying tier", {
  my_df <- data.frame(A = 1, B = 2, C = 3, check.names = FALSE)

  expect_error(
    knowledge(
      my_df,
      tier(2 ~ 1)
    ),
    "Tier specification 2 ~ 1 matched no variables.",
    fixed = TRUE
  )
  expect_error(
    knowledge(
      my_df,
      tier(2 ~ X)
    ),
    "Unknown variable(s): [X]
They are not present in the data frame provided to this Knowledge object.",
    fixed = TRUE
  )

  expect_error(
    knowledge(
      data.frame(V1 = 1, V2 = 2, check.names = FALSE),
      tier(
        1 ~ V1,
        "V2"
      )
    ),
    "Each tier() argument must be a two-sided formula.",
    fixed = TRUE
  )
  # this throws some evaluation error
  expect_error(
    knowledge(
      tier(
        1 ~ V1,
        V4
      )
    )
  )
  my_df <- data.frame(A = 1, B = 2, check.names = FALSE)

  expect_error(
    knowledge(
      my_df,
      tier(
        1 ~ starts_with("Z")
      )
    ),
    "Tier specification 1 ~ starts_with(\"Z\") matched no variables.",
    fixed = TRUE
  )
})

test_that("numeric-vector tier() errors on duplicate indices", {
  my_df <- data.frame(A = 1, B = 2, C = 3, check.names = FALSE)

  expect_error(
    knowledge(
      my_df,
      tier(1:3), # first time: creates tiers 1,2,3
      tier(1:3) # second time: should detect 1,2,3 already exist
    ),
    "Tier index(es) 1, 2, 3 already exist.",
    fixed = TRUE
  )
})

test_that("seq_tiers() in tier() errors when no variables match the pattern", {
  my_df <- data.frame(A = 1, B = 2, C = 3, check.names = FALSE)

  expect_error(
    knowledge(
      my_df,
      tier(
        # build a bundle that will match no columns
        seq_tiers(1, starts_with("zzz"))
      )
    ),
    # literal match of the error message
    "Pattern starts_with(\"zzz\") matched no variables.",
    fixed = TRUE
  )
})

test_that("tier() errors when two seq_tiers patterns overlap", {
  my_df <- data.frame(
    A = 1,
    B = 2,
    C = 3,
    check.names = FALSE
  )

  expect_error(
    knowledge(
      my_df,
      tier(
        # seq_tiers(1:2, everything()) produces two formulas
        # 1 ~ everything(), 2 ~ everything()
        # so every column is matched twice -> should throw
        seq_tiers(1:2, everything())
      )
    ),
    "Some variables matched by two patterns: A, B, C",
    fixed = TRUE
  )
})

test_that("seq_tiers() placeholder validation and numeric default branch", {
  expect_error(
    seq_tiers(1:2, foo), # no placeholder
    "`vars` must contain the placeholder `i`",
    fixed = TRUE
  )

  fml <- seq_tiers(1:2, c(i, 42))

  expect_length(fml, 2)
  rhs_txt <- rlang::expr_text(rlang::f_rhs(fml[[1]]))

  expect_true(grepl("\\b1\\b", rhs_txt)) # i → "1"
  expect_true(grepl("\\b42\\b", rhs_txt)) # 42 unchanged
})


test_that("add_tier() errors when both `before` and `after` are supplied", {
  expect_error(
    knowledge() |>
      add_tier(2, before = 3, after = 1),
    "Cannot supply both `before` and `after`.",
    fixed = TRUE
  )
})

test_that("add_tier() errors when both `before` and `after` are supplied for a labelled tier", {
  expect_error(
    knowledge() |>
      add_tier(1) |>
      add_tier(dumb_tier, before = 1, after = 1),
    "Cannot supply both `before` and `after`.",
    fixed = TRUE
  )
})

test_that("add_tier() errors when either `before` or `after` is given but is not in kn$tiers", {
  expect_error(
    knowledge() |>
      add_tier(One, before = Two),
    "`before`/`after` cannot be used when there are no existing tiers.",
    fixed = TRUE
  )
})

test_that("add_to_tier() errors when tier input is bad", {
  expect_error(
    knowledge() |>
      add_tier(NA),
    "`tier` must be a single non-empty label or a non-negative numeric literal.",
    fixed = TRUE
  )
  expect_error(
    knowledge() |>
      add_tier(NULL),
    "`tier` must be a single non-empty label or a non-negative numeric literal.",
    fixed = TRUE
  )
})

test_that("add_tier() errors when no before or after is provided", {
  expect_error(
    knowledge() |>
      add_tier(1) |>
      add_tier(Two),
    "Once the Knowledge object already has tiers, supply exactly one of `before` or `after`.",
    fixed = TRUE
  )
  expect_error(
    knowledge() |>
      add_tier(1) |>
      add_tier(Two, after = 1) |>
      add_tier("Three"),
    "Once the Knowledge object already has tiers, supply exactly one of `before` or `after`.",
    fixed = TRUE
  )
})

test_that("tier() attaches to an existing tier label", {
  my_df <- tibble::tibble(V1 = 1, V2 = 2)

  kn <- knowledge(
    my_df,
    tier(1 ~ V1), # creates tier "1" and assigns V1
    tier(1 ~ V2) # same label, should hit the 'add_to_tier' path
  )

  # only one tier row exists and it is labelled "1"
  expect_equal(kn$tiers$label, "1")

  # both variables now belong to that tier
  expect_setequal(
    kn$vars |> dplyr::filter(tier == "1") |> dplyr::pull(var),
    c("V1", "V2")
  )
})

# ──────────────────────────────────────────────────────────────────────────────
# Tier violations
# ──────────────────────────────────────────────────────────────────────────────

test_that("reordering respects tier-violation rules", {
  # only forbidden edge → any reorder is fine
  kn <- knowledge(
    tier(One ~ V1, Two ~ V2, Three ~ V3),
    V2 %!-->% V3
  )
  expect_silent(reorder_tiers(kn, c("Three", "One", "Two")))
  expect_silent(reposition_tier(kn, Three, before = One))

  # required edge → illegal uphill move must error
  kn2 <- knowledge(
    tier(One ~ V1, Two ~ V2, Three ~ V3),
    V2 %-->% V3 # V2 must stay *before* V1
  )

  expect_error(
    reposition_tier(kn2, Three, after = One),
    "Edge(s) violate tier ordering: V2 --> V3",
    fixed = TRUE
  )

  expect_error(
    reorder_tiers(kn2, c("One", "Three", "Two")),
    "Edge(s) violate tier ordering: V2 --> V3",
    fixed = TRUE
  )
})

test_that("adding tier after required edge is provided will trigger tier violation error", {
  expect_error(
    knowledge(
      V2 %-->% V1
    ) |>
      add_tier(1) |>
      add_tier(2, after = 1) |>
      add_to_tier(2 ~ V2) |>
      add_to_tier(1 ~ V1),
    "Edge(s) violate tier ordering: V2 --> V1",
    fixed = TRUE
  )
})

# ──────────────────────────────────────────────────────────────────────────────
# Misc errors
# ──────────────────────────────────────────────────────────────────────────────
test_that("unfreeze() clears the frozen flag", {
  my_df <- tibble::tibble(A = 1, B = 2)

  kn_frozen <- knowledge(my_df) # passing a data frame sets frozen = TRUE
  expect_true(kn_frozen$frozen)

  kn_unfrozen <- unfreeze(kn_frozen)
  expect_false(kn_unfrozen$frozen)
})

test_that("unfreeze() allows adding new variables", {
  my_df <- tibble::tibble(A = 1, B = 2)
  kn <- knowledge(my_df) |> unfreeze() # thaw the object

  # add a new variable that wasn't in the original data frame
  kn2 <- add_vars(kn, "C")

  expect_setequal(kn2$vars$var, c("A", "B", "C"))
  expect_false(kn2$frozen) # flag stays FALSE
})
test_that("knowledge() throws error when using another a not defined function", {
  my_df <- data.frame(V1 = 1, V2 = 2, check.names = FALSE)
  expect_error(
    knowledge(
      my_df,
      musthave(V1 ~ 1)
    ),
    "Only tier(), exogenous(), and infix edge operators (%-->%, %!-->%) are allowed.",
    fixed = TRUE
  )
  expect_error(
    knowledge(
      makingmistakes(V1 ~ 1)
    ),
    "Only tier(), exogenous(), and infix edge operators (%-->%, %!-->%) are allowed.",
    fixed = TRUE
  )
})

test_that("print.Knowledge() snapshot", {
  local_edition(3) # enable v3 snapshotting

  withr::with_options(
    list(
      crayon.enabled = FALSE, # strip colour codes
      cli.num_colors = 1
    ),
    {
      kn <- knowledge(
        tibble::tibble(V1 = 1, V2 = 2),
        tier(1 ~ V1),
        tier(2 ~ V2),
        V1 %!-->% V2
      )

      expect_snapshot_output(print(kn), cran = FALSE)
    }
  )
})

test_that(".edge_verb() validates formula structure and matches", {
  kn <- knowledge() # empty, so no vars are known yet

  # not required or forbidden
  expect_error(
    .edge_verb(kn, "unknown", rlang::quo(V1 ~ V2)),
    "`status` (edge type) must be 'required' or 'forbidden' for ",
    fixed = TRUE
  )
  # not a two-sided formula
  expect_error(
    .edge_verb(kn, "forbidden", rlang::quo(V1)),
    "Edge specification must be a two-sided formula",
    fixed = TRUE
  )

  # both sides match zero vars: specific error branch
  expect_error(
    .edge_verb(
      kn,
      "forbidden",
      rlang::quo(starts_with("Z") ~ starts_with("W"))
    ),
    "Formula `starts_with(\"Z\") ~ starts_with(\"W\")` matched no variables.",
    fixed = TRUE
  )
})


test_that(".vars_from_spec() handles c(...) and symbol fallback paths", {
  kn <- knowledge(tibble::tibble(V1 = 1, V2 = 2))

  # unsupported argument inside c()
  expect_error(
    .vars_from_spec(kn, quote(c(V1, 42))),
    "Unsupported argument in c\\(\\):",
    perl = TRUE
  )

  # symbol resolves to a user-supplied character vector
  local_vec <- c("V1", "V2")
  expect_equal(
    .vars_from_spec(kn, quote(local_vec)),
    local_vec
  )
})

# ──────────────────────────────────────────────────────────────────────────────
# + operator
# ──────────────────────────────────────────────────────────────────────────────

test_that("merge of numeric-looking tiers preserves left order", {
  kn1 <- knowledge(tier(`1` ~ V1, `3` ~ V3))
  kn2 <- knowledge(tier(`1` ~ V4, `2` ~ V2, `3` ~ V5))

  kn12 <- kn1 + kn2

  expect_equal(
    kn12$tiers,
    tiers_tbl("1", "3", "2") # order: left first, new labels appended
  )

  ## duplicated variable: e1 wins because it’s listed first
  kn1a <- knowledge(tier(`1` ~ V1))
  kn2a <- knowledge(tier(`1` ~ V1_new))
  expect_equal(
    (kn1a + kn2a)$vars$var[1],
    "V1" # takes first definition from kn1a
  )
})

test_that("merge of arbitrary labels concatenates e1 order then new from e2", {
  kn1 <- knowledge(tier(A ~ V1, AA ~ V3))
  kn2 <- knowledge(tier(A ~ V4, B ~ V2))

  kn12 <- kn1 + kn2

  expect_equal(
    kn12$tiers,
    tiers_tbl("A", "AA", "B") # "B" appended after all of kn1’s labels
  )
})

test_that("merge errors if resulting tiers violate required-edge order", {
  kn_left <- knowledge(tier(One ~ V1))
  kn_right <- knowledge(
    tier(Two ~ V2),
    V2 %-->% V1
  )

  expect_error(
    kn_left + kn_right,
    "Edge(s) violate tier ordering",
    fixed = TRUE
  )
})

test_that("merge errors if tiers overlap", {
  kn1 <- knowledge(tier(One ~ V1, Two ~ V2))
  kn2 <- knowledge(tier(Three ~ V2, Three ~ V3))
  expect_error(
    kn1 + kn2,
    "Tier conflict detected for 1 variable",
    fixed = TRUE
  )
})

test_that("merge errors if required and forbidden edges overlap", {
  kn1 <- knowledge(
    V1 %!-->% V2,
    V2 %!-->% V3
  )
  kn2 <- knowledge(
    V1 %-->% V2,
    V2 %-->% V3
  )

  expect_error(
    kn1 + kn2,
    "Edge(s) appear as both forbidden and required: V1 --> V2, V2 --> V3",
    fixed = TRUE
  )
})

# ──────────────────────────────────────────────────────────────────────────────
# deparse_knowledge()
# ──────────────────────────────────────────────────────────────────────────────

test_that("deparse_knowledge() emits minimal code for empty knowledge", {
  kn <- knowledge()
  code <- deparse_knowledge(kn)
  expected <- "knowledge(\n)"
  expect_equal(code, expected)
})

test_that("deparse_knowledge() includes data-frame name when provided", {
  my_df <- data.frame(A = 1, B = 2)
  kn <- knowledge(my_df, tier(1 ~ A, 2 ~ B))
  code <- deparse_knowledge(kn, "my_df")
  expected <- paste0(
    "knowledge(my_df,",
    "\n  tier(",
    "\n    1 ~ A,",
    "\n    2 ~ B",
    "\n  )",
    "\n)"
  )
  expect_equal(code, expected)
})

test_that("deparse_knowledge() groups multiple tiers into one tier() call", {
  my_df <- data.frame(X = 1, Y = 2, Z = 3)
  kn <- knowledge(
    my_df,
    tier(first ~ X + Y, second ~ Z)
  )
  code <- deparse_knowledge(kn, "my_df")
  expect_true(grepl("tier\\(\\s*first ~ X \\+ Y,\\s*second ~ Z\\s*\\)", code))
})

test_that("deparse_knowledge() collapses forbidden edges by source", {
  my_df <- data.frame(A = 1, B = 2, C = 3, D = 4)
  kn <- knowledge(
    my_df,
    A %!-->% C,
    A %!-->% D,
    B %!-->% C
  )
  code <- deparse_knowledge(kn, "my_df")
  # Should convert to
  #   A %!-->% c(C, D),
  #   B %!-->% C
  expected_pattern <- "knowledge(my_df,\n  A %!-->% c(C, D),\n  B %!-->% C\n)"

  expect_equal(code, expected_pattern)
})

test_that("deparse_knowledge() collapses required edges by source", {
  my_df <- data.frame(P = 1, Q = 2, R = 3)
  kn <- knowledge(
    my_df,
    P %-->% Q,
    P %-->% R
  )
  code <- deparse_knowledge(kn, "my_df")

  expected_pattern <- "knowledge(my_df,\n  P %-->% c(Q, R)\n)"
  expect_equal(code, expected_pattern)
})

test_that("deparse_knowledge() round-trips: eval(parse(code)) equals original", {
  my_df <- data.frame(A = 1, B = 2, C = 3)
  kn <- knowledge(
    my_df,
    tier(1 ~ A + B, 2 ~ C),
    A %!-->% C,
    B %-->% A
  )
  code <- deparse_knowledge(kn, "my_df")

  kn2 <- eval(parse(text = code))
  expect_equal(kn2, kn)
})

test_that("print and summary method for knowledge", {
  my_df <- data.frame(A = 1, B = 2, C = 3, D = 4, E = 5, F = 6)
  kn <- knowledge(
    my_df,
    tier(1 ~ A + B, 2 ~ C),
    A %!-->% C,
    B %-->% A
  )
  print(kn)
  print(kn, wide = TRUE)
  print(kn, compact = TRUE)
  print(kn, wide = TRUE, compact = TRUE)
  summary(kn)
  expect_true(TRUE)
})

test_that("print and summary method for empty knowledge works", {
  kn <- knowledge()
  print(kn)
  print(kn, wide = TRUE)
  print(kn, compact = TRUE)
  print(kn, wide = TRUE, compact = TRUE)
  summary(kn)
  expect_true(TRUE)
})

test_that("print and summary method for no tier knowledge works", {
  kn <- knowledge(A %-->% B)
  print(kn)
  print(kn, wide = TRUE)
  print(kn, compact = TRUE)
  print(kn, wide = TRUE, compact = TRUE)
  summary(kn)
  expect_true(TRUE)
})

Try the causalDisco package in your browser

Any scripts or data that you put into this service are public.

causalDisco documentation built on April 13, 2026, 5:06 p.m.