Nothing
# Tests for the ffm_batch() tibble-driven runner. Compilation/validation tests
# are CI-safe (dry-run, no binaries); execution is binary-gated at the bottom.
test_that("ffm_batch() dry-run adds a command column per job, no success", {
jobs <- tibble::tibble(input = c("a.mp4", "b.mp4"), output = c("a.mp3", "b.mp3"))
res <- ffm_batch(
jobs,
function(input, output, ...) {
p <- ffm_dry(input, output)
ffm_codec(p, audio = "copy")
},
run = FALSE
)
expect_s3_class(res, "tbl_df")
expect_equal(nrow(res), 2)
expect_true("command" %in% names(res))
expect_false("success" %in% names(res))
expect_match(res$command[[1]], '-i "a.mp4" -codec:a copy "a.mp3"', fixed = TRUE)
})
test_that("ffm_batch() passes columns to .f by name and forwards ...", {
jobs <- tibble::tibble(input = "a.mp4", output = "a.mp4", start = 1, end = 2)
res <- ffm_batch(
jobs,
function(input, output, start, end, reencode, ...) {
ffm_seek(ffm_dry(input, output), start = start, end = end,
reencode = reencode)
},
reencode = TRUE,
run = FALSE
)
expect_match(res$command[[1]], "-ss 1 -to 2", fixed = TRUE)
})
test_that("ffm_batch() adds no verified column on a dry run", {
jobs <- tibble::tibble(input = "a.mp4", output = "a.mp4")
res <- ffm_batch(
jobs,
function(input, output, ...) ffm_codec(ffm_dry(input, output), audio = "copy"),
run = FALSE, verify = list(width = 320)
)
expect_false("verified" %in% names(res))
})
test_that("ffm_batch() rejects an invalid verify spec type", {
jobs <- tibble::tibble(input = "a.mp4", output = "a.mp4")
expect_error(
ffm_batch(jobs, function(input, output, ...) ffm_dry(input, output),
run = FALSE, verify = "width"),
"named list or a function"
)
})
test_that("resolve_batch_verify() replicates a list and maps a function", {
jobs <- tibble::tibble(input = c("a", "b"), output = c("x", "y"),
w = c(10, 20))
s1 <- resolve_batch_verify(list(width = 320), jobs)
expect_length(s1, 2)
expect_equal(s1[[1]], list(width = 320))
s2 <- resolve_batch_verify(function(w, ...) list(width = w), jobs)
expect_equal(s2[[1]], list(width = 10))
expect_equal(s2[[2]], list(width = 20))
})
test_that("resolve_batch_verify() rejects an empty or unnamed spec", {
jobs <- tibble::tibble(input = "a", output = "x")
expect_error(
resolve_batch_verify(function(...) list(), jobs), "non-empty named list"
)
expect_error(
resolve_batch_verify(function(...) list(1, 2), jobs), "non-empty named list"
)
})
test_that("ffm_batch() rejects an empty verify spec before running", {
# resolved before any encode, so this aborts without needing ffmpeg
jobs <- tibble::tibble(input = "a.mp4", output = "a.mp4")
expect_error(
ffm_batch(jobs, function(input, output, ...) ffm_dry(input, output),
verify = function(...) list()),
"non-empty named list"
)
})
test_that("ffm_batch() rejects a non-logical progress flag", {
jobs <- tibble::tibble(input = "a.mp4", output = "a.mp4")
expect_error(
ffm_batch(jobs, function(input, output, ...) ffm_dry(input, output),
run = FALSE, progress = "yes"),
"progress"
)
})
test_that("ffm_batch() rejects a non-data-frame or empty jobs table", {
expect_error(ffm_batch(list(a = 1), function(...) NULL), "data frame")
expect_error(
ffm_batch(tibble::tibble(input = character()), function(...) NULL),
"at least one row"
)
})
test_that("ffm_batch() errors when .f does not return an ffm pipeline", {
jobs <- tibble::tibble(input = "a.mp4", output = "a.mp4")
expect_error(
ffm_batch(jobs, function(input, output, ...) "not a pipeline", run = FALSE),
"ffm pipeline"
)
})
test_that("ffm_batch(parallel = TRUE) warns when the future plan is sequential", {
skip_if_not_installed("furrr")
old <- future::plan(future::sequential)
withr::defer(future::plan(old))
jobs <- tibble::tibble(input = "a.mp4", output = "a.mp3")
# run = FALSE (no execution) still warns: the guard fires before the run
# branch, so the warning is independent of `run`. Muffle the benign furrr
# global-export warning that only appears under devtools::load_all().
withCallingHandlers(
expect_warning(
ffm_batch(jobs, function(input, output, ...) ffm_dry(input, output),
run = FALSE, parallel = TRUE),
class = "tidymedia_sequential_plan"
),
warning = function(w) {
if (grepl("may not be available", conditionMessage(w))) {
invokeRestart("muffleWarning")
}
}
)
})
test_that("warn_if_sequential_plan() is silent under a non-sequential plan", {
# Detection keys off the plan's class, not merely `parallel`. Stub
# future::plan() to report a parallel strategy without spinning workers
# (a real parallel plan would then need furrr to actually run jobs).
local_mocked_bindings(
plan = function(...) structure(function() NULL, class = "multisession"),
.package = "future"
)
expect_no_warning(warn_if_sequential_plan())
})
test_that("ffm_batch(parallel = FALSE) never warns about the plan", {
jobs <- tibble::tibble(input = "a.mp4", output = "a.mp3")
expect_no_warning(
ffm_batch(jobs, function(input, output, ...) ffm_dry(input, output),
run = FALSE, parallel = FALSE)
)
})
test_that("ffm_batch() runs each job and reports success (binary-gated)", {
skip_if_no_ffmpeg()
a <- make_test_video()
b <- make_test_video()
out_a <- withr::local_tempfile(fileext = ".mp3")
out_b <- withr::local_tempfile(fileext = ".mp3")
jobs <- tibble::tibble(input = c(a, b), output = c(out_a, out_b))
res <- ffm_batch(jobs, function(input, output, ...) {
ffm_files(input, output) |> ffm_drop("video")
})
expect_true(all(res$success))
expect_true(all(file.exists(res$output)))
})
# M08: verification wired into the batch (binary-gated) --------------------------
test_that("ffm_batch(verify =) adds a verified column without aborting", {
skip_if_no_encoder("libx264")
a <- make_test_video()
b <- make_test_video()
out_a <- withr::local_tempfile(fileext = ".mp4")
out_b <- withr::local_tempfile(fileext = ".mp4")
jobs <- tibble::tibble(input = c(a, b), output = c(out_a, out_b))
res <- ffm_batch(jobs, function(input, output, ...) {
ffm_files(input, output) |>
ffm_scale(width = 32, height = 32) |>
ffm_codec(video = "libx264")
}, verify = list(width = 32, height = 32))
expect_true("verified" %in% names(res))
expect_true(all(res$verified))
expect_true(all(res$success))
})
test_that("ffm_batch(verify =) records FALSE for a mismatch, no abort", {
skip_if_no_encoder("libx264")
a <- make_test_video()
out_a <- withr::local_tempfile(fileext = ".mp4")
jobs <- tibble::tibble(input = a, output = out_a)
res <- ffm_batch(jobs, function(input, output, ...) {
ffm_files(input, output) |>
ffm_scale(width = 32, height = 32) |>
ffm_codec(video = "libx264")
}, verify = list(width = 999))
expect_true(res$success[[1]])
expect_false(res$verified[[1]])
})
test_that("ffm_batch(verify =) accepts a function of the job columns", {
skip_if_no_encoder("libx264")
a <- make_test_video()
out_a <- withr::local_tempfile(fileext = ".mp4")
jobs <- tibble::tibble(input = a, output = out_a, w = 32)
res <- ffm_batch(jobs, function(input, output, w, ...) {
ffm_files(input, output) |>
ffm_scale(width = w, height = w) |>
ffm_codec(video = "libx264")
}, verify = function(w, ...) list(width = w, height = w))
expect_true(res$verified[[1]])
})
test_that("ffm_batch(progress = TRUE) completes without erroring", {
skip_if_no_encoder("libx264")
a <- make_test_video()
b <- make_test_video()
out_a <- withr::local_tempfile(fileext = ".mp4")
out_b <- withr::local_tempfile(fileext = ".mp4")
jobs <- tibble::tibble(input = c(a, b), output = c(out_a, out_b))
expect_no_error(
res <- ffm_batch(jobs, function(input, output, ...) {
ffm_files(input, output) |> ffm_scale(32, 32) |> ffm_codec(video = "libx264")
}, progress = TRUE)
)
expect_true(all(res$success))
})
# M06: safe execution with hostile paths (binary-gated) --------------------------
test_that("ffm_batch() runs jobs whose paths contain hostile characters", {
skip_if_no_encoder("libmp3lame")
src <- make_test_video()
dir <- withr::local_tempdir()
hostile_in <- file.path(dir, "job one's $x `a`.mp4")
expect_true(file.copy(src, hostile_in))
jobs <- tibble::tibble(
input = hostile_in,
output = file.path(dir, "out one's $y `b`.mp3")
)
res <- ffm_batch(jobs, .f = function(input, output, ...) {
ffm_files(input, output) |>
ffm_drop("video") |>
ffm_codec(audio = "libmp3lame")
})
expect_true(all(res$success))
expect_true(file.exists(jobs$output))
})
# M092 AC3: run_with_progress()'s return contract, without a binary ----------
test_that("run_with_progress() returns one success/timed_out record per job", {
# M70's O6: this contract was covered only behind skip_if_no_ffmpeg(), and
# CI's macOS and Windows runners install no media binaries -- so a mismatch
# surfaced as a hard vapply() type error on a user's machine rather than red
# on CI. The stub keeps the check binary-free, and the two vapply() calls
# below are the exact expressions ffm_batch() applies to this return
# (R/ffm_batch.R:157-158): they are what a violating shape breaks.
run_one <- function(pipeline) {
list(success = !grepl("bad", pipeline), timed_out = FALSE)
}
pipelines <- c("good-1", "bad-2", "good-3")
results <- run_with_progress(pipelines, run_one)
expect_length(results, length(pipelines))
for (r in results) {
expect_named(r, c("success", "timed_out"))
expect_true(rlang::is_bool(r$success))
expect_true(rlang::is_bool(r$timed_out))
}
# The consumer expressions themselves, in order.
expect_identical(vapply(results, `[[`, logical(1), "success"),
c(TRUE, FALSE, TRUE))
expect_identical(sum(vapply(results, `[[`, logical(1), "timed_out")), 0L)
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.