Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/actions/prepare-test-runtime/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Prepare test runtime
description: >-
Install R test dependencies, build the extension and bundled sess, install
sess, compile the tests and put arf on PATH (except on Windows). Run after
setup-test-tools.
runs:
using: composite
steps:
- name: Install R test dependencies
uses: r-lib/actions/setup-r-dependencies@f9a764fea8d5c63df6ef9a5c7795bf7deb5d7e05 # v2
with:
working-directory: sess
# Keep Suggests for live plot tests. Install lintr/styler explicitly
# for Interactive tests; they are optional languageserver dependencies.
# renv is exercised by the Interactive library-isolation tests.
extra-packages: |
any::remotes
any::pkgbuild
any::languageserver
any::lintr
any::styler
any::renv
- run: pnpm run compile
shell: bash
- name: Install bundled sess with prepared dependencies
run: remotes::install_local('dist/resources/sess', dependencies = FALSE, upgrade = 'never', force = TRUE)
shell: Rscript {0}
- name: Verify live plot lifecycle dependencies
run: |
packages <- c("processx", "later", "jsonlite", "jgd", "httpgd", "svglite")
for (package in packages) {
message("Loading plot test dependency: ", package)
loadNamespace(package)
}
stopifnot(packageVersion("httpgd") >= "2.0.1")
shell: Rscript {0}
- name: Compile test files
run: pnpm run clean && pnpm exec tsc
shell: bash
- name: Install arf binary for Interactive tests
if: runner.os != 'Windows'
shell: bash
env:
RUNNER_OS_NAME: ${{ runner.os }}
run: |
ARF_ARCH="$(uname -m)"
if [ "${ARF_ARCH}" = "arm64" ]; then ARF_ARCH="aarch64"; fi
if [ "${RUNNER_OS_NAME}" = "macOS" ]; then
ARF_TARGET="${ARF_ARCH}-apple-darwin"
else
ARF_TARGET="${ARF_ARCH}-unknown-linux-gnu"
fi
ARF_ARCHIVE="arf-console-${ARF_TARGET}.tar.xz"
ARF_RELEASE="https://github.com/eitsupi/arf/releases/download/v0.5.3"
curl -fsSL "${ARF_RELEASE}/${ARF_ARCHIVE}" -o "${RUNNER_TEMP}/${ARF_ARCHIVE}"
curl -fsSL "${ARF_RELEASE}/sha256.sum" -o "${RUNNER_TEMP}/arf.sha256"
(cd "${RUNNER_TEMP}" && awk -v name="${ARF_ARCHIVE}" '$2 == name || $2 == "*" name' arf.sha256 > arf-selected.sha256 && test -s arf-selected.sha256 && shasum -a 256 -c arf-selected.sha256)
mkdir -p "${RUNNER_TEMP}/arf"
tar -xJf "${RUNNER_TEMP}/${ARF_ARCHIVE}" -C "${RUNNER_TEMP}/arf"
ARF_EXECUTABLE=$(find "${RUNNER_TEMP}/arf" -type f -name arf | head -n 1)
test -n "${ARF_EXECUTABLE}"
echo "$(dirname "${ARF_EXECUTABLE}")" >> "$GITHUB_PATH"
echo "ARF_PATH=${ARF_EXECUTABLE}" >> "$GITHUB_ENV"
17 changes: 17 additions & 0 deletions .github/actions/setup-test-tools/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Set up test tools
description: >-
Install R, pnpm, Node.js and the locked npm dependencies. R packages are not
installed yet, so sess bootstrap checks can run in this base environment.
runs:
using: composite
steps:
- uses: r-lib/actions/setup-r@f9a764fea8d5c63df6ef9a5c7795bf7deb5d7e05 # v2
with:
use-public-rspm: true
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
shell: bash
6 changes: 5 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
# `/` covers only workflows and a root action.yml; list local composite
# actions explicitly so their pinned actions are updated too.
directories:
- /
- /.github/actions/*
commit-message:
prefix: "ci: "
schedule:
Expand Down
90 changes: 28 additions & 62 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ on:
- '!.roborev/**'
- '!scripts/benchmark-*'
- '!.github/**'
- '.github/actions/**'
- '.github/workflows/main.yml'
- '.github/scripts/extension-version.js'
- '.github/scripts/extension-version.test.js'
Expand All @@ -47,58 +48,31 @@ concurrency:
cancel-in-progress: true

jobs:
test:
# Node and extension-host tests run as parallel jobs per OS. Both need the
# same R, sess and arf runtime, which the local composite actions set up.
test-node:
name: Node (${{ matrix.os }})
env:
VSCR_REQUIRE_PLOT_TESTS: '1'
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
os:
- macos-latest
- ubuntu-latest
- windows-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: r-lib/actions/setup-r@f9a764fea8d5c63df6ef9a5c7795bf7deb5d7e05 # v2
with:
use-public-rspm: true
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- uses: ./.github/actions/setup-test-tools
# Bootstrap checks run before R test dependencies are installed.
- name: Verify sess source identity and bootstrap
run: pnpm run test:sess-source
- name: Verify managed tool library
run: pnpm run test:tool-library
- name: Verify managed tool consent
run: pnpm run test:tool-consent
- name: Install R test dependencies
uses: r-lib/actions/setup-r-dependencies@f9a764fea8d5c63df6ef9a5c7795bf7deb5d7e05 # v2
with:
working-directory: sess
# Keep Suggests for live plot tests. Install lintr/styler explicitly
# for Interactive tests; they are optional languageserver dependencies.
# renv is exercised by the Interactive library-isolation tests.
extra-packages: |
any::remotes
any::pkgbuild
any::languageserver
any::lintr
any::styler
any::renv
- run: pnpm run compile
- name: Install bundled sess with prepared dependencies
run: remotes::install_local('dist/resources/sess', dependencies = FALSE, upgrade = 'never', force = TRUE)
shell: Rscript {0}
- name: Verify live plot lifecycle dependencies
run: |
packages <- c("processx", "later", "jsonlite", "jgd", "httpgd", "svglite")
for (package in packages) {
message("Loading plot test dependency: ", package)
loadNamespace(package)
}
stopifnot(packageVersion("httpgd") >= "2.0.1")
shell: Rscript {0}
- uses: ./.github/actions/prepare-test-runtime
- name: Verify managed jgd preparation
run: pnpm run test:prepare-jgd
- name: Verify managed rstudioapi preparation
Expand All @@ -111,30 +85,6 @@ jobs:
- name: Run sess tests
run: tinytest::test_package("sess")
shell: Rscript {0}
- name: Compile test files
run: pnpm run clean && pnpm exec tsc
- name: Install arf binary for Interactive tests
if: runner.os != 'Windows'
shell: bash
run: |
ARF_ARCH="$(uname -m)"
if [ "${ARF_ARCH}" = "arm64" ]; then ARF_ARCH="aarch64"; fi
if [ "${{ runner.os }}" = "macOS" ]; then
ARF_TARGET="${ARF_ARCH}-apple-darwin"
else
ARF_TARGET="${ARF_ARCH}-unknown-linux-gnu"
fi
ARF_ARCHIVE="arf-console-${ARF_TARGET}.tar.xz"
ARF_RELEASE="https://github.com/eitsupi/arf/releases/download/v0.5.3"
curl -fsSL "${ARF_RELEASE}/${ARF_ARCHIVE}" -o "${RUNNER_TEMP}/${ARF_ARCHIVE}"
curl -fsSL "${ARF_RELEASE}/sha256.sum" -o "${RUNNER_TEMP}/arf.sha256"
(cd "${RUNNER_TEMP}" && awk -v name="${ARF_ARCHIVE}" '$2 == name || $2 == "*" name' arf.sha256 > arf-selected.sha256 && test -s arf-selected.sha256 && shasum -a 256 -c arf-selected.sha256)
mkdir -p "${RUNNER_TEMP}/arf"
tar -xJf "${RUNNER_TEMP}/${ARF_ARCHIVE}" -C "${RUNNER_TEMP}/arf"
ARF_EXECUTABLE=$(find "${RUNNER_TEMP}/arf" -type f -name arf | head -n 1)
test -n "${ARF_EXECUTABLE}"
echo "$(dirname "${ARF_EXECUTABLE}")" >> "$GITHUB_PATH"
echo "ARF_PATH=${ARF_EXECUTABLE}" >> "$GITHUB_ENV"
- name: Test persistent tmux sessions (Linux)
if: runner.os == 'Linux'
run: |
Expand All @@ -143,6 +93,22 @@ jobs:
VSCR_TEST_TMUX=1 pnpm exec mocha out/test/node/interactiveRuntime.test.js --ui tdd --timeout 60000 --grep "^Interactive real R runtime standalone agent survives its launcher process exiting$"
- name: Run Node tests including live plot lifecycle tests
run: pnpm run test:node
test-extension:
name: Extension (${{ matrix.os }})
env:
VSCR_REQUIRE_PLOT_TESTS: '1'
strategy:
fail-fast: false
matrix:
os:
- macos-latest
- ubuntu-latest
- windows-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ./.github/actions/setup-test-tools
- uses: ./.github/actions/prepare-test-runtime
- name: Run plot lifecycle extension tests (Linux)
if: runner.os == 'Linux'
run: xvfb-run -a pnpm run test:extension --label plot-lifecycle
Expand Down
16 changes: 10 additions & 6 deletions R/tests/attach_sess.R
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,11 @@ writeLines(c(
"} else if (mode %in% c('parallel_approve', 'parallel_decline', 'parallel_third',",
" 'parallel_failure', 'parallel_retry')) {",
" timeout <- 10",
# The follower budget covers the owner's whole source install, which can
# take well over 20 seconds on Windows runners.
" parallel_setup_timeout <- 60",
" if (mode == 'parallel_approve') {",
" result <- attach_sess(timeout=timeout, setup_timeout=20)",
" result <- attach_sess(timeout=timeout, setup_timeout=parallel_setup_timeout)",
" stopifnot(identical(result, TRUE), 'sess' %in% loadedNamespaces())",
" stopifnot(identical(sess_loaded_source_revision(), revision))",
" actual <- getNamespaceInfo(asNamespace('sess'), 'path')",
Expand All @@ -370,7 +373,7 @@ writeLines(c(
" stopifnot(!file.exists(ready_path), !dir.exists(lock_path))",
" stopifnot(same_paths(.libPaths(), ordinary))",
" } else {",
" result <- attach_sess(timeout=timeout, setup_timeout=20)",
" result <- attach_sess(timeout=timeout, setup_timeout=parallel_setup_timeout)",
" stopifnot(identical(result, FALSE), !('sess' %in% loadedNamespaces()))",
" if (mode == 'parallel_retry') stopifnot(file.exists(file.path(managed_library, 'sess', 'DESCRIPTION')))",
" else stopifnot(!file.exists(file.path(managed_library, 'sess', 'DESCRIPTION'))) ",
Expand Down Expand Up @@ -772,7 +775,8 @@ start_parallel_child <- function(case_root, mode, role, result_file, install_hel
}

wait_for_child <- function(paths, description) {
deadline <- Sys.time() + 30
# Must exceed the parallel follower's setup budget so its own diagnostic wins.
deadline <- Sys.time() + 90
while (!file.exists(paths$result) && Sys.time() < deadline) Sys.sleep(0.01)
child_output <- read_child_logs(paths)
if (!file.exists(paths$result)) {
Expand Down Expand Up @@ -818,7 +822,7 @@ run_parallel_setup <- function(approve) {
" utils::install.packages(pkg_path, repos=NULL, type='source', lib=library, quiet=TRUE)",
paste0(" if (!file.exists(file.path(library, 'sess', 'DESCRIPTION'))) stop('install failed')"),
paste0(" file.create(", r_literal(description_visible), ")"),
paste0(" deadline <- Sys.time() + 20; release <- ", r_literal(install_release)),
paste0(" deadline <- Sys.time() + 60; release <- ", r_literal(install_release)),
" while (!file.exists(release) && Sys.time() < deadline) Sys.sleep(0.01)",
" if (!file.exists(release)) stop('installer fixture release timed out')",
paste0(" file.create(", r_literal(install_complete), ")"),
Expand All @@ -839,11 +843,11 @@ run_parallel_setup <- function(approve) {
wait_for_file(follower_observed, 10, "follower observation of the held setup lock")

if (approve) {
wait_for_file(description_visible, 10, "installed package metadata while owner holds lock")
wait_for_file(description_visible, 60, "installed package metadata while owner holds lock")
stopifnot(dir.exists(lock_path), file.exists(file.path(managed_library, "sess", "DESCRIPTION")))
stopifnot(!file.exists(load_log), !file.exists(ready_path))
file.create(install_release)
wait_for_file(install_complete, 10, "installer verification completion")
wait_for_file(install_complete, 60, "installer verification completion")
}
wait_for_child(owner, paste(mode, "role=owner"))
wait_for_child(follower, paste(mode, "role=follower"))
Expand Down
48 changes: 34 additions & 14 deletions R/tests/prepare-jgd.R
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,11 @@ local({
), stub_attach)
startup_file <- file.path(profile_root, "startup.status")
startup_profile <- file.path(profile_root, "run-profile.R")
# Publish completion only after profile.R returns, so the status file is final.
# Polling the status while the child replaces it fails on Windows: an open
# reader blocks the rename and a reader racing the rename gets Permission denied.
profile_done <- file.path(profile_root, "profile-done")
profile_done_temporary <- paste0(profile_done, ".tmp")
writeLines(c(
paste0("Sys.setenv(VSCODE_R_SESS_PKG_PATH=", r_literal(package), ","),
paste0(" VSCODE_R_SESS_SOURCE_HELPER=", r_literal(source_helper), ","),
Expand All @@ -503,7 +508,10 @@ local({
paste0(" R_PROFILE_USER_OLD=", r_literal(empty_user_profile), ","),
" SESS_PLOT_BACKEND='jgd', SESS_RSTUDIOAPI='FALSE', JGD_SOCKET='fixture-socket')",
paste0("options(repos=c(CRAN=", r_literal(repo), "), pkgType='source')"),
"source(file.path(getwd(), 'R', 'profile.R'))"
paste0("tryCatch(source(file.path(getwd(), 'R', 'profile.R')), finally = {",
"writeLines('done', ", r_literal(profile_done_temporary), "); ",
"stopifnot(file.rename(", r_literal(profile_done_temporary), ", ",
r_literal(profile_done), ")) })")
), startup_profile)
profile_result <- file.path(profile_root, "result")
profile_process <- system2_with_child_environment(
Expand All @@ -514,22 +522,34 @@ local({
)
request <- wait_requests(profile_consent, 1L)
answer_requests(request, "decline")
deadline <- Sys.time() + 30
while (!file.exists(profile_log) && Sys.time() < deadline) Sys.sleep(0.01)
if (!file.exists(profile_log)) stop("Declined startup profile did not connect sess.")
profile_logs <- function() {
# Redirected output can stay open briefly after the done marker on Windows.
unlist(lapply(c("stdout", "stderr"), function(name) {
path <- file.path(profile_root, name)
deadline <- Sys.time() + 10
repeat {
lines <- suppressWarnings(tryCatch(
if (file.exists(path)) readLines(path, warn = FALSE) else character(),
error = function(error) NULL
))
if (!is.null(lines)) return(lines)
if (Sys.time() >= deadline) return(paste("could not read", path))
Sys.sleep(0.1)
}
}))
}
deadline <- Sys.time() + 60
while (!file.exists(profile_done) && Sys.time() < deadline) Sys.sleep(0.05)
if (!file.exists(profile_done)) {
stop("Declined startup profile did not finish: ", paste(profile_logs(), collapse = "\n"))
}
if (!file.exists(profile_log)) {
stop("Declined startup profile did not connect sess: ", paste(profile_logs(), collapse = "\n"))
}
stopifnot(identical(readLines(profile_log), c("fixture-socket", "FALSE", "FALSE")))
startup_status <- readLines(startup_file, warn = FALSE)
deadline <- Sys.time() + 10
while (length(startup_status) == 6L && startup_status[[6L]] == "pending" &&
Sys.time() < deadline) {
Sys.sleep(0.01)
startup_status <- readLines(startup_file, warn = FALSE)
}
if (length(startup_status) != 6L || startup_status[[6L]] != "ready") {
logs <- unlist(lapply(c("stdout", "stderr"), function(name) {
path <- file.path(profile_root, name)
if (file.exists(path)) readLines(path, warn = FALSE) else character()
}))
logs <- profile_logs()
stop("Unexpected profile startup status: ", paste(startup_status, collapse = " | "),
" logs: ", paste(logs, collapse = "\n"))
}
Expand Down
Loading
Loading