From 4dba22a2f6ed8aee61fe63d84f82c6c9de8d55d2 Mon Sep 17 00:00:00 2001 From: wlandau-lilly Date: Thu, 4 Jan 2024 16:24:07 -0500 Subject: [PATCH] Add logging --- DESCRIPTION | 2 +- NEWS.md | 4 +- R/crew_controller_local.R | 8 +- R/crew_eval.R | 2 + R/crew_launcher.R | 3 +- R/crew_launcher_local.R | 146 +++++++++++++++++++++- man/crew_class_launcher_local.Rd | 137 +++++++++++++++++++- man/crew_controller_local.Rd | 17 ++- man/crew_launcher_local.Rd | 17 ++- tests/testthat/test-crew_launcher_local.R | 116 +++++++++++++++++ 10 files changed, 438 insertions(+), 14 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 66f58af3..4fc07d5f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -16,7 +16,7 @@ Description: In computationally demanding analysis projects, 'clustermq' by Schubert (2019) ), and 'batchtools' by Lang, Bischel, and Surmann (2017) . -Version: 0.7.0.9004 +Version: 0.7.0.9005 License: MIT + file LICENSE URL: https://wlandau.github.io/crew/, https://github.com/wlandau/crew BugReports: https://github.com/wlandau/crew/issues diff --git a/NEWS.md b/NEWS.md index 17974282..5b54b384 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,7 +1,9 @@ -# crew 0.7.0.9004 (development) +# crew 0.7.0.9005 (development) * Configure workers to send themselves a termination signal if the connection to the dispatcher is broken (#141, @psychelzh). Huge thanks to @shikokuchuo for the support through https://github.com/shikokuchuo/mirai/issues/87, https://github.com/shikokuchuo/mirai/pull/88, and https://github.com/shikokuchuo/nanonext/pull/25! * Throw a warning from `controller$map()` if at least one task threw one. `warnings = FALSE` suppresses this behavior. +* Set `output = TRUE` in `daemon()` so `stdout` and `stderr` streams print. +* Add new arguments `local_log_directory` and `local_log_join` to write to local log files. # crew 0.7.0 diff --git a/R/crew_controller_local.R b/R/crew_controller_local.R index 02d7e03a..a1c5b198 100644 --- a/R/crew_controller_local.R +++ b/R/crew_controller_local.R @@ -35,7 +35,9 @@ crew_controller_local <- function( reset_packages = FALSE, reset_options = FALSE, garbage_collection = FALSE, - launch_max = 5L + launch_max = 5L, + local_log_directory = NULL, + local_log_join = TRUE ) { crew_deprecate( name = "seconds_exit", @@ -70,7 +72,9 @@ crew_controller_local <- function( reset_options = reset_options, garbage_collection = garbage_collection, launch_max = launch_max, - tls = tls + tls = tls, + local_log_directory = local_log_directory, + local_log_join = local_log_join ) controller <- crew_controller(client = client, launcher = launcher) controller$validate() diff --git a/R/crew_eval.R b/R/crew_eval.R index 7bb5fa27..ea0b2c91 100644 --- a/R/crew_eval.R +++ b/R/crew_eval.R @@ -62,12 +62,14 @@ crew_eval <- function( list2env(x = globals, envir = globalenv()) envir <- list2env(x = data, parent = globalenv()) capture_error <- function(condition) { + message(paste("Error:", conditionMessage(condition))) # for log files state$error <- crew_eval_message(condition) state$error_class <- class(condition) state$trace <- paste(as.character(sys.calls()), collapse = "\n") NULL } capture_warning <- function(condition) { + message(paste("Warning:", conditionMessage(condition))) # for log files state$count_warnings <- (state$count_warnings %||% 0L) + 1L should_store_warning <- (state$count_warnings < crew_eval_max_warnings) && (nchar(state$warnings %||% "") < crew_eval_max_nchar) diff --git a/R/crew_launcher.R b/R/crew_launcher.R index 3cd1ddba..dea65f1c 100644 --- a/R/crew_launcher.R +++ b/R/crew_launcher.R @@ -445,11 +445,12 @@ crew_class_launcher <- R6::R6Class( list( url = socket, autoexit = signal_disconnect, + cleanup = cleanup, + output = TRUE, maxtasks = private$.tasks_max, idletime = private$.seconds_idle * 1000, walltime = private$.seconds_wall * 1000, timerstart = private$.tasks_timers, - cleanup = cleanup, tls = private$.tls$worker(name = private$.name), rs = mirai::nextstream(private$.name) ) diff --git a/R/crew_launcher_local.R b/R/crew_launcher_local.R index 6cd0cdb2..25c294d8 100644 --- a/R/crew_launcher_local.R +++ b/R/crew_launcher_local.R @@ -4,6 +4,17 @@ #' @description Create an `R6` object to launch and maintain #' local process workers. #' @inheritParams crew_launcher +#' @param local_log_directory Either `NULL` or a character of length 1 +#' with the file path to a directory to write worker-specific log files +#' with standard output and standard error messages. +#' Each log file represents a single *instance* of a running worker, +#' so there will be more log files +#' if a given worker starts and terminates a lot. Set to `NULL` to suppress +#' log files (default). +#' @param local_log_join Logical of length 1. If `TRUE`, `crew` will write +#' standard output and standard error to the same log file for +#' each worker instance. If `FALSE`, then they these two streams +#' will go to different log files with informative suffixes. #' @examples #' if (identical(Sys.getenv("CREW_EXAMPLES"), "true")) { #' client <- crew_client() @@ -31,7 +42,9 @@ crew_launcher_local <- function( reset_options = FALSE, garbage_collection = FALSE, launch_max = 5L, - tls = crew::crew_tls() + tls = crew::crew_tls(), + local_log_directory = NULL, + local_log_join = TRUE ) { crew_deprecate( name = "seconds_exit", @@ -56,7 +69,9 @@ crew_launcher_local <- function( reset_options = reset_options, garbage_collection = garbage_collection, launch_max = launch_max, - tls = tls + tls = tls, + local_log_directory = local_log_directory, + local_log_join = local_log_join ) launcher$validate() launcher @@ -83,7 +98,125 @@ crew_class_launcher_local <- R6::R6Class( classname = "crew_class_launcher_local", inherit = crew_class_launcher, cloneable = FALSE, + private = list( + .local_log_directory = NULL, + .local_log_join = NULL, + .log_prepare = function() { + if (!is.null(private$.local_log_directory)) { + dir_create(private$.local_log_directory) + } + }, + .log_path = function(name, type) { + directory <- private$.local_log_directory + if (is.null(directory)) { + return(NULL) + } + suffix <- if_any(private$.local_log_join, "", paste0("-", type)) + file.path(directory, sprintf("%s%s.log", name, suffix)) + } + ), + active = list( + #' @field local_log_directory See [crew_launcher_local()]. + local_log_directory = function() { + .subset2(private, ".local_log_directory") + }, + #' @field local_log_join See [crew_launcher_local()]. + local_log_join = function() { + .subset2(private, ".local_log_join") + } + ), public = list( + #' @description Local launcher constructor. + #' @return An `R6` object with the local launcher. + #' @param name See [crew_launcher()]. + #' @param seconds_interval See [crew_launcher()]. + #' @param seconds_timeout See [crew_launcher()]. + #' @param seconds_launch See [crew_launcher()]. + #' @param seconds_idle See [crew_launcher()]. + #' @param seconds_wall See [crew_launcher()]. + #' @param seconds_exit See [crew_launcher()]. + #' @param tasks_max See [crew_launcher()]. + #' @param tasks_timers See [crew_launcher()]. + #' @param reset_globals See [crew_launcher()]. + #' @param reset_packages See [crew_launcher()]. + #' @param reset_options See [crew_launcher()]. + #' @param garbage_collection See [crew_launcher()]. + #' @param launch_max See [crew_launcher()]. + #' @param tls See [crew_launcher()]. + #' @param processes See [crew_launcher()]. + #' @param local_log_directory See [crew_launcher_local()]. + #' @param local_log_join See [crew_launcher_local()]. + #' @examples + #' if (identical(Sys.getenv("CREW_EXAMPLES"), "true")) { + #' client <- crew_client() + #' client$start() + #' launcher <- crew_launcher_local(name = client$name) + #' launcher$start(workers = client$workers) + #' launcher$launch(index = 1L) + #' m <- mirai::mirai("result", .compute = client$name) + #' Sys.sleep(0.25) + #' m$data + #' client$terminate() + #' } + initialize = function( + name = NULL, + seconds_interval = NULL, + seconds_timeout = NULL, + seconds_launch = NULL, + seconds_idle = NULL, + seconds_wall = NULL, + seconds_exit = NULL, + tasks_max = NULL, + tasks_timers = NULL, + reset_globals = NULL, + reset_packages = NULL, + reset_options = NULL, + garbage_collection = NULL, + launch_max = NULL, + tls = NULL, + processes = NULL, + local_log_directory = NULL, + local_log_join = NULL + ) { + super$initialize( + name = name, + seconds_interval = seconds_interval, + seconds_timeout = seconds_timeout, + seconds_launch = seconds_launch, + seconds_idle = seconds_idle, + seconds_wall = seconds_wall, + seconds_exit = seconds_exit, + tasks_max = tasks_max, + tasks_timers = tasks_timers, + reset_globals = reset_globals, + reset_packages = reset_packages, + reset_options = reset_options, + garbage_collection = garbage_collection, + launch_max = launch_max, + tls = tls, + processes = processes + ) + private$.local_log_directory <- local_log_directory + private$.local_log_join <- local_log_join + }, + #' @description Validate the local launcher. + #' @return `NULL` (invisibly). + validate = function() { + super$validate() + crew_assert( + private$.local_log_directory %|||% "x", + is.character(.), + length(.) == 1L, + !anyNA(.), + nzchar(.), + message = "local_log_directory must be NULL or a valid directory path." + ) + crew_assert( + private$.local_log_join, + isTRUE(.) || isFALSE(.), + message = "local_log_join must be TRUE or FALSE." + ) + }, #' @description Launch a local process worker which will #' dial into a socket. #' @details The `call` argument is R code that will run to @@ -94,7 +227,9 @@ crew_class_launcher_local <- R6::R6Class( #' later on. #' @param call Character of length 1 with a namespaced call to #' [crew_worker()] which will run in the worker and accept tasks. - #' @param name Character of length 1 with an informative worker name. + #' @param name Character of length 1 with a long informative worker name + #' which contains the `launcher`, `worker`, and `instance` arguments + #' described below. #' @param launcher Character of length 1, name of the launcher. #' @param worker Positive integer of length 1, index of the worker. #' This worker index remains the same even when the current instance @@ -105,10 +240,13 @@ crew_class_launcher_local <- R6::R6Class( launch_worker = function(call, name, launcher, worker, instance) { bin <- if_any(tolower(Sys.info()[["sysname"]]) == "windows", "R.exe", "R") path <- file.path(R.home("bin"), bin) + private$.log_prepare() processx::process$new( command = path, args = c("-e", call), - cleanup = TRUE + cleanup = TRUE, + stdout = private$.log_path(name = name, type = "stdout"), + stderr = private$.log_path(name = name, type = "stderr") ) }, #' @description Terminate a local process worker. diff --git a/man/crew_class_launcher_local.Rd b/man/crew_class_launcher_local.Rd index e9f60e37..d8e15ecc 100644 --- a/man/crew_class_launcher_local.Rd +++ b/man/crew_class_launcher_local.Rd @@ -10,6 +10,22 @@ See \code{\link[=crew_launcher_local]{crew_launcher_local()}}. } \examples{ +if (identical(Sys.getenv("CREW_EXAMPLES"), "true")) { +client <- crew_client() +client$start() +launcher <- crew_launcher_local(name = client$name) +launcher$start(workers = client$workers) +launcher$launch(index = 1L) +m <- mirai::mirai("result", .compute = client$name) +Sys.sleep(0.25) +m$data +client$terminate() +} + +## ------------------------------------------------ +## Method `crew_class_launcher_local$new` +## ------------------------------------------------ + if (identical(Sys.getenv("CREW_EXAMPLES"), "true")) { client <- crew_client() client$start() @@ -31,9 +47,20 @@ Other plugin_local: \section{Super class}{ \code{\link[crew:crew_class_launcher]{crew::crew_class_launcher}} -> \code{crew_class_launcher_local} } +\section{Active bindings}{ +\if{html}{\out{
}} +\describe{ +\item{\code{local_log_directory}}{See \code{\link[=crew_launcher_local]{crew_launcher_local()}}.} + +\item{\code{local_log_join}}{See \code{\link[=crew_launcher_local]{crew_launcher_local()}}.} +} +\if{html}{\out{
}} +} \section{Methods}{ \subsection{Public methods}{ \itemize{ +\item \href{#method-crew_class_launcher_local-new}{\code{crew_class_launcher_local$new()}} +\item \href{#method-crew_class_launcher_local-validate}{\code{crew_class_launcher_local$validate()}} \item \href{#method-crew_class_launcher_local-launch_worker}{\code{crew_class_launcher_local$launch_worker()}} \item \href{#method-crew_class_launcher_local-terminate_worker}{\code{crew_class_launcher_local$terminate_worker()}} } @@ -47,7 +74,6 @@ Other plugin_local:
  • crew::crew_class_launcher$done()
  • crew::crew_class_launcher$errors()
  • crew::crew_class_launcher$forward()
  • -
  • crew::crew_class_launcher$initialize()
  • crew::crew_class_launcher$launch()
  • crew::crew_class_launcher$rotate()
  • crew::crew_class_launcher$scale()
  • @@ -59,12 +85,115 @@ Other plugin_local:
  • crew::crew_class_launcher$terminate()
  • crew::crew_class_launcher$terminate_workers()
  • crew::crew_class_launcher$unlaunched()
  • -
  • crew::crew_class_launcher$validate()
  • crew::crew_class_launcher$wait()
  • }} \if{html}{\out{
    }} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-crew_class_launcher_local-new}{}}} +\subsection{Method \code{new()}}{ +Local launcher constructor. +\subsection{Usage}{ +\if{html}{\out{
    }}\preformatted{crew_class_launcher_local$new( + name = NULL, + seconds_interval = NULL, + seconds_timeout = NULL, + seconds_launch = NULL, + seconds_idle = NULL, + seconds_wall = NULL, + seconds_exit = NULL, + tasks_max = NULL, + tasks_timers = NULL, + reset_globals = NULL, + reset_packages = NULL, + reset_options = NULL, + garbage_collection = NULL, + launch_max = NULL, + tls = NULL, + processes = NULL, + local_log_directory = NULL, + local_log_join = NULL +)}\if{html}{\out{
    }} +} + +\subsection{Arguments}{ +\if{html}{\out{
    }} +\describe{ +\item{\code{name}}{See \code{\link[=crew_launcher]{crew_launcher()}}.} + +\item{\code{seconds_interval}}{See \code{\link[=crew_launcher]{crew_launcher()}}.} + +\item{\code{seconds_timeout}}{See \code{\link[=crew_launcher]{crew_launcher()}}.} + +\item{\code{seconds_launch}}{See \code{\link[=crew_launcher]{crew_launcher()}}.} + +\item{\code{seconds_idle}}{See \code{\link[=crew_launcher]{crew_launcher()}}.} + +\item{\code{seconds_wall}}{See \code{\link[=crew_launcher]{crew_launcher()}}.} + +\item{\code{seconds_exit}}{See \code{\link[=crew_launcher]{crew_launcher()}}.} + +\item{\code{tasks_max}}{See \code{\link[=crew_launcher]{crew_launcher()}}.} + +\item{\code{tasks_timers}}{See \code{\link[=crew_launcher]{crew_launcher()}}.} + +\item{\code{reset_globals}}{See \code{\link[=crew_launcher]{crew_launcher()}}.} + +\item{\code{reset_packages}}{See \code{\link[=crew_launcher]{crew_launcher()}}.} + +\item{\code{reset_options}}{See \code{\link[=crew_launcher]{crew_launcher()}}.} + +\item{\code{garbage_collection}}{See \code{\link[=crew_launcher]{crew_launcher()}}.} + +\item{\code{launch_max}}{See \code{\link[=crew_launcher]{crew_launcher()}}.} + +\item{\code{tls}}{See \code{\link[=crew_launcher]{crew_launcher()}}.} + +\item{\code{processes}}{See \code{\link[=crew_launcher]{crew_launcher()}}.} + +\item{\code{local_log_directory}}{See \code{\link[=crew_launcher_local]{crew_launcher_local()}}.} + +\item{\code{local_log_join}}{See \code{\link[=crew_launcher_local]{crew_launcher_local()}}.} +} +\if{html}{\out{
    }} +} +\subsection{Returns}{ +An \code{R6} object with the local launcher. +} +\subsection{Examples}{ +\if{html}{\out{
    }} +\preformatted{if (identical(Sys.getenv("CREW_EXAMPLES"), "true")) { +client <- crew_client() +client$start() +launcher <- crew_launcher_local(name = client$name) +launcher$start(workers = client$workers) +launcher$launch(index = 1L) +m <- mirai::mirai("result", .compute = client$name) +Sys.sleep(0.25) +m$data +client$terminate() +} +} +\if{html}{\out{
    }} + +} + +} +\if{html}{\out{
    }} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-crew_class_launcher_local-validate}{}}} +\subsection{Method \code{validate()}}{ +Validate the local launcher. +\subsection{Usage}{ +\if{html}{\out{
    }}\preformatted{crew_class_launcher_local$validate()}\if{html}{\out{
    }} +} + +\subsection{Returns}{ +\code{NULL} (invisibly). +} +} +\if{html}{\out{
    }} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-crew_class_launcher_local-launch_worker}{}}} \subsection{Method \code{launch_worker()}}{ @@ -80,7 +209,9 @@ dial into a socket. \item{\code{call}}{Character of length 1 with a namespaced call to \code{\link[=crew_worker]{crew_worker()}} which will run in the worker and accept tasks.} -\item{\code{name}}{Character of length 1 with an informative worker name.} +\item{\code{name}}{Character of length 1 with a long informative worker name +which contains the \code{launcher}, \code{worker}, and \code{instance} arguments +described below.} \item{\code{launcher}}{Character of length 1, name of the launcher.} diff --git a/man/crew_controller_local.Rd b/man/crew_controller_local.Rd index 8a0146cd..b6b56b64 100644 --- a/man/crew_controller_local.Rd +++ b/man/crew_controller_local.Rd @@ -24,7 +24,9 @@ crew_controller_local( reset_packages = FALSE, reset_options = FALSE, garbage_collection = FALSE, - launch_max = 5L + launch_max = 5L, + local_log_directory = NULL, + local_log_join = TRUE ) } \arguments{ @@ -113,6 +115,19 @@ It is recommended to set \code{launch_max} above 0 because sometimes workers are unproductive under perfectly ordinary circumstances. But \code{launch_max} should still be small enough to detect errors in the underlying platform.} + +\item{local_log_directory}{Either \code{NULL} or a character of length 1 +with the file path to a directory to write worker-specific log files +with standard output and standard error messages. +Each log file represents a single \emph{instance} of a running worker, +so there will be more log files +if a given worker starts and terminates a lot. Set to \code{NULL} to suppress +log files (default).} + +\item{local_log_join}{Logical of length 1. If \code{TRUE}, \code{crew} will write +standard output and standard error to the same log file for +each worker instance. If \code{FALSE}, then they these two streams +will go to different log files with informative suffixes.} } \description{ Create an \code{R6} object to submit tasks and diff --git a/man/crew_launcher_local.Rd b/man/crew_launcher_local.Rd index ded59b4f..74fb0747 100644 --- a/man/crew_launcher_local.Rd +++ b/man/crew_launcher_local.Rd @@ -19,7 +19,9 @@ crew_launcher_local( reset_options = FALSE, garbage_collection = FALSE, launch_max = 5L, - tls = crew::crew_tls() + tls = crew::crew_tls(), + local_log_directory = NULL, + local_log_join = TRUE ) } \arguments{ @@ -93,6 +95,19 @@ circumstances. But \code{launch_max} should still be small enough to detect errors in the underlying platform.} \item{tls}{A TLS configuration object from \code{\link[=crew_tls]{crew_tls()}}.} + +\item{local_log_directory}{Either \code{NULL} or a character of length 1 +with the file path to a directory to write worker-specific log files +with standard output and standard error messages. +Each log file represents a single \emph{instance} of a running worker, +so there will be more log files +if a given worker starts and terminates a lot. Set to \code{NULL} to suppress +log files (default).} + +\item{local_log_join}{Logical of length 1. If \code{TRUE}, \code{crew} will write +standard output and standard error to the same log file for +each worker instance. If \code{FALSE}, then they these two streams +will go to different log files with informative suffixes.} } \description{ Create an \code{R6} object to launch and maintain diff --git a/tests/testthat/test-crew_launcher_local.R b/tests/testthat/test-crew_launcher_local.R index 251c582a..da189fbb 100644 --- a/tests/testthat/test-crew_launcher_local.R +++ b/tests/testthat/test-crew_launcher_local.R @@ -1,3 +1,51 @@ +crew_test("crew_launcher_local() active binding members", { + launcher <- crew_launcher_local( + name = "x", + local_log_directory = "y", + local_log_join = FALSE + ) + expect_equal(launcher$local_log_directory, "y") + expect_false(launcher$local_log_join) +}) + +crew_test("crew_launcher_local() log_prepare()", { + skip_on_cran() + launcher <- crew_launcher_local( + name = "x", + local_log_directory = tempfile(), + local_log_join = FALSE + ) + on.exit(unlink(launcher$local_log_directory)) + private <- crew_private(launcher) + expect_false(dir.exists(launcher$local_log_directory)) + private$.log_prepare() + expect_true(dir.exists(launcher$local_log_directory)) +}) + +crew_test("crew_launcher_local() log_path(), joined logs", { + skip_on_cran() + launcher <- crew_launcher_local( + name = "x", + local_log_directory = "dir", + local_log_join = TRUE + ) + private <- crew_private(launcher) + out <- private$.log_path(name = "x", type = "stdout") + expect_equal(out, file.path("dir", "x.log")) +}) + +crew_test("crew_launcher_local() log_path(), separate logs", { + skip_on_cran() + launcher <- crew_launcher_local( + name = "x", + local_log_directory = "dir", + local_log_join = FALSE + ) + private <- crew_private(launcher) + out <- private$.log_path(name = "x", type = "stdout") + expect_equal(out, file.path("dir", "x-stdout.log")) +}) + crew_test("crew_launcher_local() can run a task on a worker", { skip_on_cran() skip_on_os("windows") @@ -192,6 +240,74 @@ crew_test("crew_launcher_local() can run a task and end a worker", { expect_true(launcher$workers$terminated[1L]) }) +crew_test("joined logs", { + skip_on_cran() + skip_on_covr() + skip_on_os("windows") + x <- crew_controller_local( + workers = 1L, + seconds_idle = 60, + local_log_directory = tempfile(), + local_log_join = TRUE + ) + on.exit({ + x$terminate() + rm(x) + gc() + crew_test_sleep() + }) + x$start() + x$push(print("this-print")) + x$push(message("this-message")) + x$push(warning("this-warning")) + x$push(stop("this-stop")) + x$wait(mode = "all") + Sys.sleep(0.25) + dir <- x$launcher$local_log_directory + logs <- list.files(dir, full.names = TRUE) + expect_equal(length(logs), 1L) + lines <- readLines(logs) + expect_true(any(grepl("this-print", lines, fixed = TRUE))) + expect_true(any(grepl("this-message", lines, fixed = TRUE))) + expect_true(any(grepl("Warning: this-warning", lines, fixed = TRUE))) + expect_true(any(grepl("Error: this-stop", lines, fixed = TRUE))) +}) + +crew_test("separate logs", { + skip_on_cran() + skip_on_covr() + skip_on_os("windows") + x <- crew_controller_local( + workers = 1L, + seconds_idle = 60, + local_log_directory = tempfile(), + local_log_join = FALSE + ) + on.exit({ + x$terminate() + rm(x) + gc() + crew_test_sleep() + }) + x$start() + on.exit(x$terminate()) + x$push(print("this-print")) + x$push(message("this-message")) + x$push(warning("this-warning")) + x$push(stop("this-stop")) + x$wait(mode = "all") + Sys.sleep(0.25) + dir <- x$launcher$local_log_directory + logs <- list.files(dir, full.names = TRUE) + expect_equal(length(logs), 2L) + stderr <- readLines(logs[1L]) + stdout <- readLines(logs[2L]) + expect_true(any(grepl("this-print", stdout, fixed = TRUE))) + expect_true(any(grepl("this-message", stderr, fixed = TRUE))) + expect_true(any(grepl("Warning: this-warning", stderr, fixed = TRUE))) + expect_true(any(grepl("Error: this-stop", stderr, fixed = TRUE))) +}) + crew_test("deprecate seconds_exit", { suppressWarnings(crew_launcher_local(seconds_exit = 1)) expect_true(TRUE)