# ============================================================================= # Tests for R/msg.R # ============================================================================= # --- Helpers ------------------------------------------------------------------ #' Capture message output as a single string capture_msg <- function(expr) { paste0( capture.output(tryCatch(expr, error = identity), type = "message"), collapse = "\n" ) } #' Strip all ANSI escape codes from a string strip_ansi <- function(x) { gsub("\033\\[[0-9;]*m", "", x) } #' Capture + strip ANSI capture_plain <- function(expr) { strip_ansi(capture_msg(expr)) } #' Temporarily set options and restore on exit with_options <- function(opts, expr) { old <- do.call(options, opts) on.exit(do.call(options, old), add = TRUE) force(expr) } #' Temporarily set env var and restore on exit with_envvar <- function(vars, expr) { old <- Sys.getenv(names(vars), unset = NA_character_) do.call(Sys.setenv, vars) on.exit({ for (nm in names(old)) { if (is.na(old[[nm]])) { Sys.unsetenv(nm) } else { do.call("Sys.setenv", stats::setNames(list(old[[nm]]), nm)) } } }, add = TRUE) force(expr) } # ============================================================================= # 1. ANSI / Terminal Detection # ============================================================================= test_that(".supports_ansi() returns logical scalar", { result <- .supports_ansi() expect_type(result, "logical") expect_length(result, 1L) }) test_that(".supports_ansi() respects pkg.no_color option", { with_options(list(pkg.no_color = TRUE), { expect_false(.supports_ansi()) }) with_options(list(pkg.no_color = FALSE), { # just check it returns logical, actual value depends on terminal expect_type(.supports_ansi(), "logical") }) }) test_that(".supports_ansi() respects crayon.enabled option", { with_options(list(crayon.enabled = TRUE, pkg.no_color = FALSE), { expect_true(.supports_ansi()) }) with_options(list(crayon.enabled = FALSE, pkg.no_color = FALSE), { expect_false(.supports_ansi()) }) }) test_that(".supports_ansi() respects NO_COLOR env var", { with_options(list(pkg.no_color = FALSE, crayon.enabled = NULL), { with_envvar(list(NO_COLOR = "1", TERM = "xterm"), { expect_false(.supports_ansi()) }) }) }) test_that(".supports_ansi() respects TERM=dumb env var", { with_options(list(pkg.no_color = FALSE, crayon.enabled = NULL), { with_envvar(list(TERM = "dumb", NO_COLOR = ""), { expect_false(.supports_ansi()) }) }) }) # ============================================================================= # 2. Style Helpers # ============================================================================= test_that(".style() returns plain string when ANSI disabled", { with_options(list(pkg.no_color = TRUE), { expect_equal(.style("hello", .ansi$bold), "hello") expect_equal(.style("world", .ansi$red), "world") }) }) test_that(".style() wraps string with ANSI codes when enabled", { with_options(list(crayon.enabled = TRUE), { result <- .style("hello", .ansi$bold) expect_true(grepl("\033\\[", result)) expect_true(grepl("hello", result)) expect_true(grepl("\033\\[0m", result)) # reset at end }) }) test_that(".bold() / .italic() / .underline() return character", { with_options(list(pkg.no_color = TRUE), { expect_equal(.bold("x"), "x") expect_equal(.italic("x"), "x") expect_equal(.underline("x"), "x") }) }) test_that(".col() returns plain text when ANSI disabled", { with_options(list(pkg.no_color = TRUE), { expect_equal(.col("text", "red"), "text") expect_equal(.col("text", "green"), "text") }) }) test_that(".col_bold() returns plain text when ANSI disabled", { with_options(list(pkg.no_color = TRUE), { expect_equal(.col_bold("text", "blue"), "text") }) }) # ============================================================================= # 3. Symbol Resolution # ============================================================================= test_that(".sym() returns a non-empty character string for all symbols", { sym_names <- c( "tick", "cross", "warning", "info", "arrow", "bullet", "pointer", "line", "ellipsis", "play", "star", "circle", "sq_small", "hourglass", "wrench", "package", "sparkle" ) for (nm in sym_names) { result <- .sym(nm) expect_type(result, "character") expect_true(nchar(result) >= 1L, label = paste("symbol:", nm)) } }) test_that(".sym() returns '?' for unknown symbol", { expect_equal(.sym("nonexistent_symbol_xyz"), "?") }) test_that(".sym() returns ASCII when pkg.unicode = FALSE", { with_options(list(pkg.unicode = FALSE), { expect_equal(.sym("tick"), "v") expect_equal(.sym("cross"), "x") expect_equal(.sym("warning"), "!") expect_equal(.sym("info"), "i") expect_equal(.sym("bullet"), "*") }) }) test_that(".sym() returns Unicode when pkg.unicode = TRUE", { with_options(list(pkg.unicode = TRUE), { expect_equal(.sym("tick"), "\u2714") expect_equal(.sym("cross"), "\u2716") expect_equal(.sym("warning"), "\u26A0") expect_equal(.sym("info"), "\u2139") expect_equal(.sym("bullet"), "\u2022") }) }) # ============================================================================= # 4. String Helpers # ============================================================================= test_that("%||% returns left value when not NULL", { expect_equal("a" %||% "b", "a") expect_equal(1L %||% 2L, 1L) expect_equal(FALSE %||% TRUE, FALSE) }) test_that("%||% returns right value when left is NULL", { expect_equal(NULL %||% "b", "b") expect_equal(NULL %||% 42L, 42L) expect_equal(NULL %||% NULL, NULL) }) test_that(".truncate() leaves short strings unchanged", { expect_equal(.truncate("hello", width = 20L), "hello") }) test_that(".truncate() truncates long strings", { long <- strrep("a", 100L) result <- .truncate(long, width = 20L, suffix = "...") expect_equal(nchar(result), 20L) expect_true(endsWith(result, "...")) }) test_that(".truncate() handles exact-width strings", { s <- strrep("x", 10L) expect_equal(.truncate(s, width = 10L, suffix = "..."), s) }) test_that(".truncate() is vectorized", { x <- c("short", strrep("a", 50L)) result <- .truncate(x, width = 20L) expect_length(result, 2L) expect_equal(result[1], "short") expect_equal(nchar(result[2]), 20L) }) test_that(".wrap() returns character vector", { result <- .wrap("This is a somewhat long sentence that might wrap.", width = 30L) expect_type(result, "character") expect_true(all(nchar(result) <= 30L)) }) test_that(".wrap() respects indent", { result <- .wrap("Hello world", width = 40L, indent = 4L) expect_true(all(startsWith(result, " "))) }) test_that(".plural() returns singular for n = 1", { expect_equal(.plural(1L, "file"), "file") expect_equal(.plural(1L, "box", "boxes"), "box") }) test_that(".plural() returns plural for n != 1", { expect_equal(.plural(0L, "file"), "files") expect_equal(.plural(2L, "file"), "files") expect_equal(.plural(10L, "box", "boxes"), "boxes") }) test_that(".n_items() formats correctly", { expect_equal(.n_items(1L, "file"), "1 file") expect_equal(.n_items(2L, "file"), "2 files") expect_equal(.n_items(0L, "item"), "0 items") }) test_that(".collapse() handles edge cases", { expect_equal(.collapse(character(0L)), "") expect_equal(.collapse("a"), "a") expect_equal(.collapse(c("a", "b")), "a, b") expect_equal(.collapse(c("a", "b", "c")), "a, b, c") }) test_that(".collapse() uses custom sep", { expect_equal(.collapse(c("a", "b", "c"), sep = " | "), "a | b | c") }) test_that(".collapse() uses last connector", { expect_equal(.collapse(c("a", "b", "c"), last = " and "), "a, b and c") expect_equal(.collapse(c("a", "b"), last = " or "), "a or b") }) test_that(".quote() wraps in single quotes", { expect_equal(.quote("hello"), "'hello'") expect_equal(.quote("x", q = '"'), '"x"') }) test_that(".bquote() wraps in backticks", { expect_equal(.bquote("my_fn"), "`my_fn`") }) test_that(".dquote() wraps in double quotes", { expect_equal(.dquote("hello"), '"hello"') }) # ============================================================================= # 5. Core Message Functions # ============================================================================= test_that("msg_success() emits a message", { expect_message(msg_success("All good")) }) test_that("msg_success() message contains the text", { out <- capture_plain(msg_success("All good")) expect_true(grepl("All good", out)) }) test_that("msg_info() emits a message containing the text", { out <- capture_plain(msg_info("Some info")) expect_true(grepl("Some info", out)) }) test_that("msg_warn() emits a message containing the text", { out <- capture_plain(msg_warn("Watch out")) expect_true(grepl("Watch out", out)) }) test_that("msg_danger() emits a message containing the text", { out <- capture_plain(msg_danger("Something broke")) expect_true(grepl("Something broke", out)) }) test_that("msg_process() emits a message containing the text", { out <- capture_plain(msg_process("Downloading")) expect_true(grepl("Downloading", out)) }) test_that("msg_bullet() emits a message containing the text", { out <- capture_plain(msg_bullet("Remember this")) expect_true(grepl("Remember this", out)) }) test_that("msg_todo() emits a message containing the text", { out <- capture_plain(msg_todo("Fix this later")) expect_true(grepl("Fix this later", out)) }) test_that("all msg_*() functions return NULL invisibly", { expect_null(suppressMessages(msg_success("x"))) expect_null(suppressMessages(msg_info("x"))) expect_null(suppressMessages(msg_warn("x"))) expect_null(suppressMessages(msg_danger("x"))) expect_null(suppressMessages(msg_process("x"))) expect_null(suppressMessages(msg_bullet("x"))) expect_null(suppressMessages(msg_todo("x"))) }) test_that("msg_*() functions accept multiple arguments (paste0)", { out <- capture_plain(msg_info("Value is: ", 42L)) expect_true(grepl("Value is: 42", out)) out <- capture_plain(msg_success("Done in ", 3L, " steps")) expect_true(grepl("Done in 3 steps", out)) }) # ============================================================================= # 6. Debug Messages # ============================================================================= test_that("msg_debug() is silent when pkg.debug = FALSE", { with_options(list(pkg.debug = FALSE), { expect_silent(msg_debug("secret")) out <- capture_msg(msg_debug("secret")) expect_equal(nchar(out), 0L) }) }) test_that("msg_debug() emits message when pkg.debug = TRUE", { with_options(list(pkg.debug = TRUE), { expect_message(msg_debug("debug info")) out <- capture_plain(msg_debug("debug info")) expect_true(grepl("debug info", out)) }) }) test_that("msg_debug() returns NULL invisibly regardless of option", { with_options(list(pkg.debug = FALSE), { expect_null(msg_debug("x")) }) with_options(list(pkg.debug = TRUE), { expect_null(suppressMessages(msg_debug("x"))) }) }) # ============================================================================= # 7. Headers & Dividers # ============================================================================= test_that("msg_header() emits a message", { expect_message(msg_header("My Section")) }) test_that("msg_header() message contains the title", { out <- capture_plain(msg_header("My Section")) expect_true(grepl("My Section", out)) }) test_that("msg_header() output width respects width argument", { out <- strip_ansi(capture_msg(msg_header("Test", width = 40L))) # strip newline line <- trimws(out, which = "right") expect_lte(nchar(line), 41L) # +1 for possible newline char }) test_that("msg_rule() emits a message", { expect_message(msg_rule()) }) test_that("msg_rule() output is non-empty", { out <- capture_plain(msg_rule()) expect_true(nchar(trimws(out)) > 0L) }) test_that("msg_blank() emits an empty message", { out <- capture_msg(msg_blank()) expect_true(grepl("^\\s*$", out)) }) test_that("msg_header() / msg_rule() / msg_blank() return NULL invisibly", { expect_null(suppressMessages(msg_header("x"))) expect_null(suppressMessages(msg_rule())) expect_null(suppressMessages(msg_blank())) }) # ============================================================================= # 8. Structured Lists # ============================================================================= test_that("msg_list() emits messages for each item", { items <- c(a = "1", b = "2", c = "3") msgs <- capture_plain(msg_list(items)) expect_true(grepl("a", msgs)) expect_true(grepl("b", msgs)) expect_true(grepl("1", msgs)) expect_true(grepl("2", msgs)) }) test_that("msg_list() shows header when provided", { out <- capture_plain(msg_list(c(x = "1"), header = "My Header")) expect_true(grepl("My Header", out)) }) test_that("msg_list() handles unnamed vector", { expect_message(msg_list(c("item1", "item2"))) out <- capture_plain(msg_list(c("item1", "item2"))) expect_true(grepl("item1", out)) expect_true(grepl("item2", out)) }) test_that("msg_list() returns NULL invisibly", { expect_null(suppressMessages(msg_list(c(a = "1")))) }) test_that("msg_kv() displays keys and values", { out <- capture_plain(msg_kv(list(Method = "lm", N = "100"))) expect_true(grepl("Method", out)) expect_true(grepl("lm", out)) expect_true(grepl("N", out)) expect_true(grepl("100", out)) }) test_that("msg_kv() aligns keys (same padding)", { out <- strip_ansi(capture_msg(msg_kv(list(A = "1", LongKey = "2")))) lines <- strsplit(out, "\n")[[1L]] lines <- lines[nzchar(trimws(lines))] # Both lines should have same start position for value expect_length(lines, 2L) }) test_that("msg_kv() returns NULL invisibly", { expect_null(suppressMessages(msg_kv(list(a = "1")))) }) # ============================================================================= # 9. Abort & Warning # ============================================================================= test_that("msg_abort() throws an error", { expect_error(msg_abort("Something went wrong")) }) test_that("msg_abort() error message contains the text", { err <- tryCatch(msg_abort("Bad input"), error = identity) expect_true(grepl("Bad input", conditionMessage(err))) }) test_that("msg_abort() error message contains cross symbol", { with_options(list(pkg.unicode = FALSE), { err <- tryCatch(msg_abort("oops"), error = identity) expect_true(grepl("x", conditionMessage(err))) }) }) test_that("msg_abort() produces error condition", { err <- tryCatch(msg_abort("fail"), error = identity) expect_s3_class(err, "error") expect_s3_class(err, "condition") }) test_that("msg_abort() attaches custom class", { err <- tryCatch( msg_abort("fail", class = "my_custom_error"), error = identity ) expect_s3_class(err, "my_custom_error") expect_s3_class(err, "error") }) test_that("msg_abort() call is NULL by default", { err <- tryCatch(msg_abort("fail"), error = identity) expect_null(conditionCall(err)) }) test_that("msg_abort() accepts multiple arguments", { err <- tryCatch(msg_abort("Value ", 42L, " is invalid"), error = identity) expect_true(grepl("Value 42 is invalid", conditionMessage(err))) }) test_that("msg_warning() issues a warning", { expect_warning(msg_warning("Careful!")) }) test_that("msg_warning() warning message contains the text", { w <- tryCatch(msg_warning("Careful!"), warning = identity) expect_true(grepl("Careful!", conditionMessage(w))) }) test_that("msg_warning() produces warning condition", { w <- tryCatch(msg_warning("oops"), warning = identity) expect_s3_class(w, "warning") expect_s3_class(w, "condition") }) test_that("msg_warning() attaches custom class", { w <- tryCatch( msg_warning("oops", class = "my_custom_warning"), warning = identity ) expect_s3_class(w, "my_custom_warning") expect_s3_class(w, "warning") }) test_that("msg_warning() returns NULL invisibly", { result <- withCallingHandlers( msg_warning("x"), warning = function(w) invokeRestart("muffleWarning") ) expect_null(result) }) # ============================================================================= # 10. Verbose # ============================================================================= test_that("msg_verbose() emits message when pkg.verbose = TRUE", { with_options(list(pkg.verbose = TRUE), { expect_message(msg_verbose("verbose text")) out <- capture_plain(msg_verbose("verbose text")) expect_true(grepl("verbose text", out)) }) }) test_that("msg_verbose() is silent when pkg.verbose = FALSE", { with_options(list(pkg.verbose = FALSE), { expect_silent(msg_verbose("verbose text")) }) }) test_that("msg_verbose() respects explicit verbose argument", { expect_message(msg_verbose("hi", verbose = TRUE)) expect_silent( msg_verbose("hi", verbose = FALSE)) }) test_that("msg_verbose() explicit arg overrides option", { with_options(list(pkg.verbose = FALSE), { expect_message(msg_verbose("hi", verbose = TRUE)) }) with_options(list(pkg.verbose = TRUE), { expect_silent(msg_verbose("hi", verbose = FALSE)) }) }) test_that("msg_verbose() returns NULL invisibly", { with_options(list(pkg.verbose = FALSE), { expect_null(msg_verbose("x")) }) with_options(list(pkg.verbose = TRUE), { expect_null(suppressMessages(msg_verbose("x"))) }) }) # ============================================================================= # 11. msg_try() # ============================================================================= test_that("msg_try() returns value on success", { result <- msg_try(1L + 1L) expect_equal(result, 2L) }) test_that("msg_try() with on_error='abort' re-throws as error", { expect_error(msg_try(stop("boom"), on_error = "abort")) }) test_that("msg_try() with on_error='message' emits message instead of error", { expect_message( expect_no_error(msg_try(stop("boom"), on_error = "message")) ) }) test_that("msg_try() with on_error='warn' emits message (styled warning)", { # msg_try catches error and calls msg_warn which emits a message expect_message( expect_no_error(msg_try(stop("boom"), on_error = "warn")) ) }) test_that("msg_try() with on_error='ignore' silently swallows error", { expect_silent( expect_no_error(msg_try(stop("boom"), on_error = "ignore")) ) }) test_that("msg_try() muffles warnings and emits styled message", { expect_message( expect_no_warning(msg_try(warning("careful"))) ) }) # ============================================================================= # 12. Timing Helper # ============================================================================= test_that(".fmt_time() formats sub-minute durations", { expect_equal(.fmt_time(0.5, digits = 2L), "0.50s") expect_equal(.fmt_time(1.0, digits = 1L), "1.0s") expect_equal(.fmt_time(59.9, digits = 0L), "60s") }) test_that(".fmt_time() formats minute durations", { result <- .fmt_time(90, digits = 1L) expect_true(grepl("1m", result)) }) test_that(".fmt_time() formats hour durations", { result <- .fmt_time(3661, digits = 0L) expect_true(grepl("1h", result)) expect_true(grepl("1m", result)) }) test_that("msg_timed() returns expression value invisibly", { result <- suppressMessages(msg_timed(42L, msg = "test")) expect_equal(result, 42L) }) test_that("msg_timed() emits a message with the label", { out <- capture_plain(msg_timed(Sys.sleep(0), msg = "My task")) expect_true(grepl("My task", out)) }) test_that("msg_timed() message contains a time value", { out <- capture_plain(msg_timed(Sys.sleep(0), msg = "Task")) # Should contain digits followed by 's' expect_true(grepl("[0-9]", out)) }) # ============================================================================= # 13. Progress Bar # ============================================================================= test_that("msg_progress() returns a list with tick/reset/done", { pb <- msg_progress(10L) expect_type(pb, "list") expect_true(all(c("tick", "reset", "done") %in% names(pb))) expect_true(is.function(pb$tick)) expect_true(is.function(pb$reset)) expect_true(is.function(pb$done)) }) test_that("msg_progress() tick does not error", { pb <- msg_progress(5L) expect_no_error({ pb$tick() pb$tick() }) }) test_that("msg_progress() done emits success message", { pb <- msg_progress(3L) out <- capture_plain({ pb$tick(); pb$tick(); pb$tick() pb$done("Finished!") }) expect_true(grepl("Finished!", out)) }) test_that("msg_progress() tick does not exceed total", { pb <- msg_progress(2L) # Tick more than total — should not error expect_no_error({ for (i in 1:10) pb$tick() }) }) test_that("msg_progress() reset works after ticking", { pb <- msg_progress(5L) pb$tick(); pb$tick(); pb$tick() expect_no_error(pb$reset()) }) # ============================================================================= # 14. Spinner # ============================================================================= test_that("msg_spinner() returns a list with spin/done/fail", { sp <- msg_spinner("Working") expect_type(sp, "list") expect_true(all(c("spin", "done", "fail") %in% names(sp))) expect_true(is.function(sp$spin)) expect_true(is.function(sp$done)) expect_true(is.function(sp$fail)) }) test_that("msg_spinner() spin does not error", { sp <- msg_spinner("Loading") expect_no_error({ sp$spin() sp$spin() sp$spin() }) }) test_that("msg_spinner() done emits success message", { sp <- msg_spinner("Loading") out <- capture_plain(sp$done("Ready!")) expect_true(grepl("Ready!", out)) }) test_that("msg_spinner() fail emits danger message", { sp <- msg_spinner("Loading") out <- capture_plain(sp$fail("Oops!")) expect_true(grepl("Oops!", out)) }) test_that("msg_spinner() cycles through all frames", { frames <- c("|", "/", "-", "\\") sp <- msg_spinner("test", frames = frames) # Spin length(frames) + 1 times to ensure cycling expect_no_error(for (i in seq_len(length(frames) + 1L)) sp$spin()) }) # ============================================================================= # 15. .build_msg() internals # ============================================================================= test_that(".build_msg() returns a character string", { with_options(list(pkg.no_color = TRUE, pkg.unicode = FALSE), { result <- .build_msg("tick", "br_green", "hello") expect_type(result, "character") expect_length(result, 1L) }) }) test_that(".build_msg() contains the text", { with_options(list(pkg.no_color = TRUE, pkg.unicode = FALSE), { result <- .build_msg("info", "br_blue", "my message") expect_true(grepl("my message", result)) }) }) test_that(".build_msg() contains the symbol", { with_options(list(pkg.no_color = TRUE, pkg.unicode = FALSE), { result <- .build_msg("tick", "br_green", "ok") expect_true(grepl(.sym("tick"), result, fixed = TRUE)) }) }) # ============================================================================= # 16. Edge Cases & Robustness # ============================================================================= test_that("msg functions handle empty string", { expect_message(msg_info("")) expect_message(msg_success("")) expect_message(msg_warn("")) }) test_that("msg functions handle special characters", { special <- "Hello & 'quotes' \"double\"" out <- capture_plain(msg_info(special)) expect_true(grepl("Hello", out)) }) test_that("msg functions handle numeric input via paste0", { out <- capture_plain(msg_info(3.14)) expect_true(grepl("3.14", out)) }) test_that("msg functions handle NA input", { out <- capture_plain(msg_info(NA)) expect_true(grepl("NA", out)) }) test_that("msg functions handle very long strings", { long <- strrep("x", 500L) expect_message(msg_info(long)) }) test_that("msg_abort() handles empty message", { err <- tryCatch(msg_abort(""), error = identity) expect_s3_class(err, "error") }) test_that("msg_list() handles empty list", { expect_no_error(suppressMessages(msg_list(list()))) }) test_that("msg_kv() handles single item", { expect_message(msg_kv(list(key = "value"))) }) test_that(".collapse() handles length-2 vector with last", { result <- .collapse(c("a", "b"), last = " and ") expect_equal(result, "a and b") }) test_that(".n_items() handles large numbers", { expect_equal(.n_items(1000000L, "row"), "1000000 rows") }) test_that(".truncate() handles empty string", { expect_equal(.truncate("", width = 10L), "") }) test_that(".wrap() handles empty string", { result <- .wrap("", width = 40L) expect_type(result, "character") })