tests/testthat/test-install.R

test_that("install_pkg_cli_apps installs launchers for matching scripts", {
  Sys.setenv("RAPP_NO_MODIFY_PATH" = "1")
  on.exit(Sys.unsetenv("RAPP_NO_MODIFY_PATH"), add = TRUE)
  pkg <- "rappTestBasic"
  fake <- setup_fake_rapp_package(tempdir(), "-install-basic", package = pkg)
  on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE)

  app_path <- file.path(fake[["exec"]], "hello.R")
  writeLines(
    c(
      "#!/usr/bin/env Rapp",
      "print('hello, world!')"
    ),
    app_path
  )

  # Should be ignored because first line lacks the keyword.
  writeLines(
    c(
      "not a launcher candidate",
      "print('skip me')"
    ),
    file.path(fake[["exec"]], "skip.R")
  )

  destdir <- tempfile("rapp-bin-basic")
  on.exit(unlink(destdir, recursive = TRUE), add = TRUE)

  expect_false(dir.exists(destdir))

  messages <- character()
  created <- withCallingHandlers(
    install_pkg_cli_apps(pkg, destdir = destdir, lib.loc = fake[["lib"]]),
    message = function(m) {
      messages <<- c(messages, conditionMessage(m))
      invokeRestart("muffleMessage")
    }
  )
  expect_true(any(grepl("^created:", messages)))

  expect_true(dir.exists(destdir))

  expected_launcher <- if (is_windows()) {
    path(destdir, "hello.bat")
  } else {
    path(destdir, "hello")
  }

  expect_same_path(created, expected_launcher)
  expect_same_path(
    path(list.files(destdir, full.names = TRUE)),
    expected_launcher
  )

  lines <- readLines(expected_launcher)
  if (is_windows()) {
    expect_identical(lines[[1]], "@echo off")
    expect_match(
      lines[[2]],
      sprintf(
        "^:: Generated by `Rapp::install_pkg_cli_apps\\(package = \"%s\"\\)`\\. Do not edit by hand\\.$",
        pkg
      )
    )
  } else {
    expect_identical(lines[[1]], "#!/bin/sh")
    expect_match(
      lines[[2]],
      sprintf(
        "^# Generated by `Rapp::install_pkg_cli_apps\\(package = \"%s\"\\)`\\. Do not edit by hand\\.$",
        pkg
      )
    )
  }

  expect_true(any(grepl("Rapp::run()", lines, fixed = TRUE)))
  expect_true(any(grepl("hello.R", lines, fixed = TRUE)))
})


test_that("install_pkg_cli_apps installs launchers for conventional Rscript apps", {
  Sys.setenv("RAPP_NO_MODIFY_PATH" = "1")
  on.exit(Sys.unsetenv("RAPP_NO_MODIFY_PATH"), add = TRUE)

  pkg <- "rappTestRscript"
  fake <- setup_fake_rapp_package(tempdir(), "-install-rscript", package = pkg)
  on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE)

  app_path <- file.path(fake[["exec"]], "plain.R")
  writeLines(
    c(
      "#!/usr/bin/env Rscript",
      "cat('plain cli\\n')"
    ),
    app_path
  )

  destdir <- tempfile("rapp-bin-rscript")
  on.exit(unlink(destdir, recursive = TRUE), add = TRUE)

  messages <- character()
  created <- withCallingHandlers(
    install_pkg_cli_apps(pkg, destdir = destdir, lib.loc = fake[["lib"]]),
    message = function(m) {
      messages <<- c(messages, conditionMessage(m))
      invokeRestart("muffleMessage")
    }
  )
  expect_true(any(grepl("^created:", messages)))

  expected_launcher <- if (is_windows()) {
    path(destdir, "plain.bat")
  } else {
    path(destdir, "plain")
  }

  expect_same_path(created, expected_launcher)

  lines <- readLines(expected_launcher)
  if (is_windows()) {
    expect_identical(lines[[1]], "@echo off")
    expect_match(
      lines[[2]],
      sprintf(
        "^:: Generated by `Rapp::install_pkg_cli_apps\\(package = \"%s\"\\)`\\. Do not edit by hand\\.$",
        pkg
      )
    )
    expect_true(grepl("%\\*$", lines[[length(lines)]]))
  } else {
    expect_identical(lines[[1]], "#!/bin/sh")
    expect_match(
      lines[[2]],
      sprintf(
        "^# Generated by `Rapp::install_pkg_cli_apps\\(package = \"%s\"\\)`\\. Do not edit by hand\\.$",
        pkg
      )
    )
    expect_true(grepl("\"\\$@\"$", lines[[length(lines)]]))
  }

  expect_true(any(grepl("Rscript", lines, fixed = TRUE)))
  expect_true(any(grepl("plain.R", lines, fixed = TRUE)))
  expect_false(any(grepl("Rapp::run()", lines, fixed = TRUE)))
})


test_that("non-Rapp executables respect overwrite flag", {
  Sys.setenv("RAPP_NO_MODIFY_PATH" = "1")
  on.exit(Sys.unsetenv("RAPP_NO_MODIFY_PATH"), add = TRUE)

  pkg <- "rappTestOverwrite"
  fake <- setup_fake_rapp_package(
    tempdir(),
    "-install-overwrite",
    package = pkg
  )
  on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE)

  app_path <- file.path(fake[["exec"]], "clash.R")
  writeLines(c("#!/usr/bin/env Rapp", "print('conflict')"), app_path)

  destdir <- tempfile("rapp-bin-overwrite")
  dir.create(destdir, recursive = TRUE)
  on.exit(unlink(destdir, recursive = TRUE), add = TRUE)

  target <- if (is_windows()) {
    path(destdir, "clash.bat")
  } else {
    path(destdir, "clash")
  }

  custom_launcher <- if (is_windows()) {
    c("@echo off", "custom launcher")
  } else {
    c("#!/bin/sh", "custom launcher")
  }
  writeLines(custom_launcher, target)
  Sys.chmod(target, mode = "0755")

  expect_warning(
    created <- install_pkg_cli_apps(
      pkg,
      destdir = destdir,
      lib.loc = fake[["lib"]],
      overwrite = FALSE
    ),
    "Skipping existing executable"
  )
  expect_length(created, 0)
  expect_identical(readLines(target), custom_launcher)

  messages <- character()
  created <- withCallingHandlers(
    install_pkg_cli_apps(
      pkg,
      destdir = destdir,
      lib.loc = fake[["lib"]],
      overwrite = TRUE
    ),
    message = function(m) {
      messages <<- c(messages, conditionMessage(m))
      invokeRestart("muffleMessage")
    }
  )
  expect_true(any(grepl("^created:", messages)))
  expect_same_path(created, target)
  expect_true(any(grepl(
    "Generated by `Rapp::install_pkg_cli_apps",
    readLines(target),
    fixed = TRUE
  )))
})


test_that("install_pkg_cli_apps persists Windows PATH when destdir is only transient", {
  skip_if_not(is_windows())

  destdir <- tempfile("rapp-bin-win-path")
  dir.create(destdir, recursive = TRUE)
  on.exit(unlink(destdir, recursive = TRUE), add = TRUE)

  current_path <- Sys.getenv("PATH")
  path_with_destdir <- paste(destdir, current_path, sep = .Platform$path.sep)
  withr::local_envvar(
    RAPP_NO_MODIFY_PATH = NA,
    PATH = path_with_destdir
  )

  powershell_calls <- list()
  testthat::local_mocked_bindings(
    get_env_win_registry = function(name) {
      expect_identical(name, "Path")
      "C:\\Windows\\system32"
    },
    run_powershell = function(args) {
      powershell_calls <<- append(powershell_calls, list(list(
        args = args,
        path_entry = Sys.getenv("RAPP_NEW_PATH_ENTRY")
      )))
      0L
    },
    .package = "Rapp"
  )

  expect_true(Rapp:::ensure_path_windows(destdir))
  expect_identical(Sys.getenv("PATH"), path_with_destdir)
  expect_length(powershell_calls, 1L)
  expect_same_path(powershell_calls[[1L]][["path_entry"]], destdir)
})


test_that("path_entries can keep the existing process PATH raw", {
  env_path <- paste("first", "second", sep = .Platform$path.sep)

  expect_identical(
    Rapp:::path_entries(env_path, normalize = FALSE),
    c("first", "second")
  )
  expect_identical(Rapp:::path_entries("", normalize = FALSE), character())
})


test_that("install_pkg_cli_apps adds default macOS install dir to PATH setup", {
  skip_if_not(identical(Sys.info()[["sysname"]], "Darwin"))
  skip_on_cran()

  home <- tempfile("rapp-home-")
  dir.create(home, recursive = TRUE)
  withr::local_envvar(
    HOME = home,
    RAPP_NO_MODIFY_PATH = NA,
    RAPP_BIN_DIR = NA,
    XDG_BIN_HOME = NA,
    XDG_DATA_HOME = NA,
    PATH = "/usr/bin"
  )
  on.exit(unlink(home, recursive = TRUE), add = TRUE)

  pkg <- "rappTestMacPath"
  fake <- setup_fake_rapp_package(tempdir(), "-install-mac-path", package = pkg)
  on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE)

  app_path <- file.path(fake[["exec"]], "hello.R")
  writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path)

  messages <- character()
  created <- withCallingHandlers(
    install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]]),
    message = function(m) {
      messages <<- c(messages, conditionMessage(m))
      invokeRestart("muffleMessage")
    }
  )

  destdir <- path(home, ".local", "bin")
  expect_same_path(created, path(destdir, "hello"))
  expect_true(any(grepl("Added .*\\.local/bin to PATH", messages)))
  expect_true(any(grepl("source ~/.zprofile", messages, fixed = TRUE)))

  zprofile <- file.path(home, ".zprofile")
  expect_true(file.exists(zprofile))
  expect_identical(
    readLines(zprofile),
    c(
      "",
      'case ":$PATH:" in',
      '  *:"$HOME/.local/bin":*) ;;',
      '  *) export PATH="$HOME/.local/bin:$PATH" ;;',
      "esac"
    )
  )
  expect_true(grepl(destdir, Sys.getenv("PATH"), fixed = TRUE))
})


test_that("install_pkg_cli_apps adds explicit default macOS destdir to PATH setup", {
  skip_if_not(identical(Sys.info()[["sysname"]], "Darwin"))
  skip_on_cran()

  home <- tempfile("rapp-home-")
  dir.create(home, recursive = TRUE)
  withr::local_envvar(
    HOME = home,
    RAPP_NO_MODIFY_PATH = NA,
    RAPP_BIN_DIR = NA,
    XDG_BIN_HOME = NA,
    XDG_DATA_HOME = NA,
    PATH = "/usr/bin"
  )
  on.exit(unlink(home, recursive = TRUE), add = TRUE)
  destdir <- path(home, ".local", "bin")

  pkg <- "rappTestMacExplicitPath"
  fake <- setup_fake_rapp_package(
    tempdir(),
    "-install-mac-explicit-path",
    package = pkg
  )
  on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE)

  app_path <- file.path(fake[["exec"]], "hello.R")
  writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path)

  suppressMessages(
    created <- install_pkg_cli_apps(
      pkg,
      destdir = "~/.local/bin",
      lib.loc = fake[["lib"]]
    )
  )

  expect_same_path(created, path(destdir, "hello"))
  expect_true(file.exists(file.path(home, ".zprofile")))
})


test_that("install_pkg_cli_apps respects ZDOTDIR for macOS PATH setup", {
  skip_if_not(identical(Sys.info()[["sysname"]], "Darwin"))
  skip_on_cran()

  home <- tempfile("rapp-home-")
  zdotdir <- tempfile("rapp-zdotdir-")
  dir.create(home, recursive = TRUE)
  dir.create(zdotdir, recursive = TRUE)
  withr::local_envvar(
    HOME = home,
    ZDOTDIR = zdotdir,
    RAPP_NO_MODIFY_PATH = NA,
    RAPP_BIN_DIR = NA,
    XDG_BIN_HOME = NA,
    XDG_DATA_HOME = NA,
    PATH = "/usr/bin"
  )
  on.exit(unlink(c(home, zdotdir), recursive = TRUE), add = TRUE)

  pkg <- "rappTestMacZdotdir"
  fake <- setup_fake_rapp_package(tempdir(), "-install-mac-zdotdir", package = pkg)
  on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE)

  app_path <- file.path(fake[["exec"]], "hello.R")
  writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path)

  suppressMessages(install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]]))

  expect_false(file.exists(file.path(home, ".zprofile")))
  expect_true(file.exists(file.path(zdotdir, ".zprofile")))
})


test_that("install_pkg_cli_apps warns and continues when macOS PATH setup cannot be written", {
  skip_if_not(identical(Sys.info()[["sysname"]], "Darwin"))
  skip_on_cran()

  home <- tempfile("rapp-home-")
  dir.create(home, recursive = TRUE)
  dir.create(file.path(home, ".zprofile"))
  withr::local_envvar(
    HOME = home,
    RAPP_NO_MODIFY_PATH = NA,
    RAPP_BIN_DIR = NA,
    XDG_BIN_HOME = NA,
    XDG_DATA_HOME = NA,
    PATH = "/usr/bin"
  )
  on.exit(unlink(home, recursive = TRUE), add = TRUE)

  pkg <- "rappTestMacPathReadonly"
  fake <- setup_fake_rapp_package(
    tempdir(),
    "-install-mac-path-readonly",
    package = pkg
  )
  on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE)

  app_path <- file.path(fake[["exec"]], "hello.R")
  writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path)

  expect_warning(
    suppressMessages(
      created <- install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]])
    ),
    "Could not add ~/.local/bin to PATH"
  )
  expect_same_path(created, path(home, ".local", "bin", "hello"))
})


test_that("install_pkg_cli_apps does not duplicate macOS PATH setup", {
  skip_if_not(identical(Sys.info()[["sysname"]], "Darwin"))
  skip_on_cran()

  home <- tempfile("rapp-home-")
  dir.create(home, recursive = TRUE)
  withr::local_envvar(
    HOME = home,
    RAPP_NO_MODIFY_PATH = NA,
    RAPP_BIN_DIR = NA,
    XDG_BIN_HOME = NA,
    XDG_DATA_HOME = NA,
    PATH = "/usr/bin"
  )
  on.exit(unlink(home, recursive = TRUE), add = TRUE)

  pkg <- "rappTestMacPathTwice"
  fake <- setup_fake_rapp_package(tempdir(), "-install-mac-path-twice", package = pkg)
  on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE)

  app_path <- file.path(fake[["exec"]], "hello.R")
  writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path)

  suppressMessages(install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]]))
  suppressMessages(install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]]))

  zprofile <- file.path(home, ".zprofile")
  lines <- readLines(zprofile)
  expect_equal(sum(grepl('case ":\\$PATH:" in', lines)), 1L)
})


test_that("install_pkg_cli_apps ignores unrelated macOS profile mentions", {
  skip_if_not(identical(Sys.info()[["sysname"]], "Darwin"))
  skip_on_cran()

  home <- tempfile("rapp-home-")
  dir.create(home, recursive = TRUE)
  withr::local_envvar(
    HOME = home,
    RAPP_NO_MODIFY_PATH = NA,
    RAPP_BIN_DIR = NA,
    XDG_BIN_HOME = NA,
    XDG_DATA_HOME = NA,
    PATH = "/usr/bin"
  )
  on.exit(unlink(home, recursive = TRUE), add = TRUE)

  zprofile <- file.path(home, ".zprofile")
  writeLines("# ~/.local/bin is where Rapp writes launchers", zprofile)

  pkg <- "rappTestMacPathComment"
  fake <- setup_fake_rapp_package(
    tempdir(),
    "-install-mac-path-comment",
    package = pkg
  )
  on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE)

  app_path <- file.path(fake[["exec"]], "hello.R")
  writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path)

  suppressMessages(install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]]))

  expect_identical(
    readLines(zprofile),
    c(
      "# ~/.local/bin is where Rapp writes launchers",
      "",
      'case ":$PATH:" in',
      '  *:"$HOME/.local/bin":*) ;;',
      '  *) export PATH="$HOME/.local/bin:$PATH" ;;',
      "esac"
    )
  )
})


test_that("install_pkg_cli_apps warns when macOS PATH setup already exists but PATH is stale", {
  skip_if_not(identical(Sys.info()[["sysname"]], "Darwin"))
  skip_on_cran()

  home <- tempfile("rapp-home-")
  dir.create(home, recursive = TRUE)
  withr::local_envvar(
    HOME = home,
    RAPP_NO_MODIFY_PATH = NA,
    RAPP_BIN_DIR = NA,
    XDG_BIN_HOME = NA,
    XDG_DATA_HOME = NA,
    PATH = "/usr/bin"
  )
  on.exit(unlink(home, recursive = TRUE), add = TRUE)

  zprofile <- file.path(home, ".zprofile")
  writeLines(
    c(
      'case ":$PATH:" in',
      '  *:"$HOME/.local/bin":*) ;;',
      '  *) export PATH="$HOME/.local/bin:$PATH" ;;',
      "esac"
    ),
    zprofile
  )

  pkg <- "rappTestMacPathStale"
  fake <- setup_fake_rapp_package(
    tempdir(),
    "-install-mac-path-stale",
    package = pkg
  )
  on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE)

  app_path <- file.path(fake[["exec"]], "hello.R")
  writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path)

  expect_warning(
    suppressMessages(install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]])),
    "~/.local/bin is still not on PATH"
  )
  expect_equal(sum(grepl('case ":\\$PATH:" in', readLines(zprofile))), 1L)
})


test_that("install_pkg_cli_apps leaves macOS profile alone when PATH already includes destdir", {
  skip_if_not(identical(Sys.info()[["sysname"]], "Darwin"))
  skip_on_cran()

  home <- tempfile("rapp-home-")
  dir.create(home, recursive = TRUE)
  destdir <- path(home, ".local", "bin")
  withr::local_envvar(
    HOME = home,
    RAPP_NO_MODIFY_PATH = NA,
    RAPP_BIN_DIR = NA,
    XDG_BIN_HOME = NA,
    XDG_DATA_HOME = NA,
    PATH = paste(destdir, "/usr/bin", sep = .Platform$path.sep)
  )
  on.exit(unlink(home, recursive = TRUE), add = TRUE)

  pkg <- "rappTestMacPathPresent"
  fake <- setup_fake_rapp_package(
    tempdir(),
    "-install-mac-path-present",
    package = pkg
  )
  on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE)

  app_path <- file.path(fake[["exec"]], "hello.R")
  writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path)

  suppressMessages(install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]]))

  expect_false(file.exists(file.path(home, ".zprofile")))
})


test_that("install_pkg_cli_apps leaves macOS PATH setup alone when disabled", {
  skip_if_not(identical(Sys.info()[["sysname"]], "Darwin"))
  skip_on_cran()

  home <- tempfile("rapp-home-")
  dir.create(home, recursive = TRUE)
  withr::local_envvar(
    HOME = home,
    RAPP_NO_MODIFY_PATH = "1",
    RAPP_BIN_DIR = NA,
    XDG_BIN_HOME = NA,
    XDG_DATA_HOME = NA,
    PATH = "/usr/bin"
  )
  on.exit(unlink(home, recursive = TRUE), add = TRUE)

  pkg <- "rappTestMacPathDisabled"
  fake <- setup_fake_rapp_package(
    tempdir(),
    "-install-mac-path-disabled",
    package = pkg
  )
  on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE)

  app_path <- file.path(fake[["exec"]], "hello.R")
  writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path)

  suppressMessages(install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]]))

  expect_false(file.exists(file.path(home, ".zprofile")))
})


test_that("front matter customises launcher options", {
  withr::local_envvar(RAPP_NO_MODIFY_PATH = "1")

  pkg <- "rappTestFront"
  fake <- setup_fake_rapp_package(
    tempdir(),
    "-install-frontmatter",
    package = pkg
  )
  on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE)

  app_path <- file.path(fake[["exec"]], "opts.R")
  writeLines(
    c(
      "#!/usr/bin/env -S Rscript -e 'Rapp::run()'",
      "#| launcher:",
      "#|   vanilla: true",
      "#|   'no-environ': true",
      "#|   'no-site-file': true",
      "#|   'no-init-file': true",
      "#|   restore: true",
      "#|   save: true",
      "#|   verbose: true",
      "#|   default_packages: [stats]",
      "print('options test')"
    ),
    app_path
  )

  destdir <- tempfile("rapp-bin-frontmatter")
  on.exit(unlink(destdir, recursive = TRUE), add = TRUE)

  messages <- character()
  created <- withCallingHandlers(
    install_pkg_cli_apps(pkg, destdir = destdir, lib.loc = fake[["lib"]]),
    message = function(m) {
      messages <<- c(messages, conditionMessage(m))
      invokeRestart("muffleMessage")
    }
  )
  expect_true(any(grepl("^created:", messages)))

  expected_launcher <- if (is_windows()) {
    path(destdir, "opts.bat")
  } else {
    path(destdir, "opts")
  }
  expect_same_path(created, expected_launcher)

  lines <- readLines(expected_launcher)
  exec_line <- lines[length(lines)]
  expect_true(grepl("--vanilla", exec_line, fixed = TRUE))
  expect_true(grepl("--no-environ", exec_line, fixed = TRUE))
  expect_true(grepl("--no-site-file", exec_line, fixed = TRUE))
  expect_true(grepl("--no-init-file", exec_line, fixed = TRUE))
  expect_true(grepl("--restore", exec_line, fixed = TRUE))
  expect_true(grepl("--save", exec_line, fixed = TRUE))
  expect_true(grepl("--verbose", exec_line, fixed = TRUE))
  expect_true(grepl("--default-packages=stats", exec_line, fixed = TRUE))
  expect_false(grepl(paste0("base,", pkg), exec_line, fixed = TRUE))
  expect_true(grepl("Rapp::run()", exec_line, fixed = TRUE))
})


test_that("launcher front matter accepts kebab-case option names", {
  withr::local_envvar(RAPP_NO_MODIFY_PATH = "1")

  pkg <- "rappTestLauncherKebab"
  fake <- setup_fake_rapp_package(
    tempdir(),
    "-install-launcher-kebab",
    package = pkg
  )
  on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE)

  app_path <- file.path(fake[["exec"]], "kebab.R")
  writeLines(
    c(
      "#!/usr/bin/env Rapp",
      "#| launcher:",
      "#|   default-packages: [stats]",
      "print('launcher kebab test')"
    ),
    app_path
  )

  destdir <- tempfile("rapp-bin-launcher-kebab")
  on.exit(unlink(destdir, recursive = TRUE), add = TRUE)

  created <- suppressMessages(
    install_pkg_cli_apps(pkg, destdir = destdir, lib.loc = fake[["lib"]])
  )

  expected_launcher <- if (is_windows()) {
    path(destdir, "kebab.bat")
  } else {
    path(destdir, "kebab")
  }
  expect_same_path(created, expected_launcher)

  exec_line <- tail(readLines(expected_launcher), 1L)
  expect_true(grepl("--default-packages=stats", exec_line, fixed = TRUE))
  expect_false(grepl(paste0("base,", pkg), exec_line, fixed = TRUE))
})


test_that("launcher-like front matter keys do not partially match", {
  withr::local_envvar(RAPP_NO_MODIFY_PATH = "1")

  pkg <- "rappTestLauncherExtra"
  fake <- setup_fake_rapp_package(
    tempdir(),
    "-install-launcher-extra",
    package = pkg
  )
  on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE)

  app_path <- file.path(fake[["exec"]], "extra.R")
  writeLines(
    c(
      "#!/usr/bin/env -S Rscript -e 'Rapp::run()'",
      "#| launcher-extra:",
      "#|   name: wrong",
      "#|   vanilla: true",
      "#|   default_packages: [stats]",
      "print('launcher extra test')"
    ),
    app_path
  )

  destdir <- tempfile("rapp-bin-launcher-extra")
  on.exit(unlink(destdir, recursive = TRUE), add = TRUE)

  messages <- character()
  created <- withCallingHandlers(
    install_pkg_cli_apps(pkg, destdir = destdir, lib.loc = fake[["lib"]]),
    message = function(m) {
      messages <<- c(messages, conditionMessage(m))
      invokeRestart("muffleMessage")
    }
  )
  expect_true(any(grepl("^created:", messages)))

  expected_launcher <- if (is_windows()) {
    path(destdir, "extra.bat")
  } else {
    path(destdir, "extra")
  }
  expect_same_path(created, expected_launcher)

  lines <- readLines(expected_launcher)
  exec_line <- lines[length(lines)]
  expect_false(grepl("--vanilla", exec_line, fixed = TRUE))
  expect_false(grepl("--default-packages=stats", exec_line, fixed = TRUE))
  expect_true(grepl(paste0("base,", pkg), exec_line, fixed = TRUE))
})


test_that("install_pkg_cli_apps prunes orphaned launchers", {
  Sys.setenv("RAPP_NO_MODIFY_PATH" = "1")
  on.exit(Sys.unsetenv("RAPP_NO_MODIFY_PATH"), add = TRUE)
  pkg <- "rappTestPrune"
  fake <- setup_fake_rapp_package(tempdir(), "-install-prune", package = pkg)
  on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE)

  keep_path <- path(fake[["exec"]], "keep.R")
  drop_path <- path(fake[["exec"]], "drop.R")
  writeLines(c("#!/usr/bin/env Rapp", "print('keep')"), keep_path)
  writeLines(c("#!/usr/bin/env Rapp", "print('drop')"), drop_path)

  destdir <- tempfile("rapp-bin-prune")
  on.exit(unlink(destdir, recursive = TRUE), add = TRUE)

  messages <- character()
  first_created <- withCallingHandlers(
    install_pkg_cli_apps(pkg, destdir = destdir, lib.loc = fake[["lib"]]),
    message = function(m) {
      messages <<- c(messages, conditionMessage(m))
      invokeRestart("muffleMessage")
    }
  )
  expect_true(sum(grepl("^created:", messages)) == 2L)

  keep_launcher <- if (is_windows()) {
    path(destdir, "keep.bat")
  } else {
    path(destdir, "keep")
  }
  drop_launcher <- if (is_windows()) {
    path(destdir, "drop.bat")
  } else {
    path(destdir, "drop")
  }

  expect_same_paths_set(first_created, c(keep_launcher, drop_launcher))
  expect_true(all(file.exists(c(keep_launcher, drop_launcher))))

  unlink(drop_path)

  messages <- character()
  second_created <- withCallingHandlers(
    install_pkg_cli_apps(pkg, destdir = destdir, lib.loc = fake[["lib"]]),
    message = function(m) {
      messages <<- c(messages, conditionMessage(m))
      invokeRestart("muffleMessage")
    }
  )
  messages <- trimws(messages)

  expect_true(any(grepl("^created: ", messages)))
  expect_true(any(paste0("deleted: ", drop_launcher) %in% messages))
  expect_same_path(second_created, keep_launcher)
  expect_true(file.exists(keep_launcher))
  expect_false(file.exists(drop_launcher))
})

Try the Rapp package in your browser

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

Rapp documentation built on June 11, 2026, 5:07 p.m.