Skip to content

streamline report generation #107

streamline report generation

streamline report generation #107

################################################################################
# trigger CI
# DataSHIELD GHA test suite - dsBaseClient
# Replaces azure-pipelines.yml / opal_azure-pipelines.yml / armadillo_azure-pipelines.yml.
#
# Structure (all jobs below run in parallel except where "needs" says otherwise):
# r-checks - devtools::document()/check() sync checks. No backend needed.
# opal-dsbase (matrix: smk/arg/perf/misc) - dsBase suite against Opal,
# one category per job.
# opal-dsdanger - dsDanger suite against Opal (small, not sharded).
# opal-report - needs the two Opal jobs above; merges + publishes report.
# armadillo-dsbase (matrix: smk/arg/perf/misc) - dsBase suite against Armadillo.
# armadillo-dsdanger - dsDanger suite against Armadillo.
# armadillo-report - needs the two Armadillo jobs above; merges + publishes report.
#
# The dsBase suite (matching TEST_FILTER_DSBASE - same 292 files the Azure
# pipelines run) is split by matrix into 4 fixed categories - smk (116 files),
# arg (96), perf (51), and misc (the smaller categories: datachk, disc, expt,
# smk_expt, math, and the "_-" catch-all - 29 combined) - each with its own
# testthat filter substring, no file-list partitioning logic. Categories
# aren't evenly sized, so job durations differ, but a category can be added,
# removed, or temporarily skipped just by editing the matrix list.
#
# Each dsbase/dsdanger job spins up its OWN backend instance (isolated - no
# shared server state / concurrency risk between categories running at once).
#
# Opal runs via docker-compose (docker-compose_opal.yml).
# Armadillo runs as a plain `java -jar` process (not docker-compose): Armadillo
# self-manages its Rock container over the host Docker socket
# (docker-management-enabled: true / docker-run-in-container: false), which skips
# building/pulling the old custom armadillo_citest image. See
# molgenis-service-armadillo's application.template.yml for that flag pairing.
#
# Every job starts its backend as the very first step so it boots in the
# background while R dependencies install, instead of paying for both serially.
#
# As of Sept. 2025 the single-backend, dsBase-only, unsharded version of this
# took ~ 95 mins; the full (Opal+Armadillo, dsBase+dsDanger) unsharded run is
# well over an hour.
################################################################################
name: dsBaseClient tests' suite
on:
push:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0' # Weekly
- cron: '0 1 * * *' # Nightly
permissions:
contents: read
env:
_r_check_system_clock_: 0
WORKFLOW_ID: ${{ github.run_id }}-${{ github.run_attempt }}
PROJECT_NAME: dsBaseClient
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
REPO_OWNER: ${{ github.repository_owner }}
R_KEEP_PKG_SOURCE: yes
GITHUB_TOKEN: ${{ github.token || 'placeholder-token' }}
# These should all be constant, except the two TEST_FILTER_* values. These can
# be used to test subsets of test files in the testthat directory. Options are
# like: '*' <- run all tests, '*_smk_*' <- run all the smoke tests.
TEST_FILTER_DSBASE: '_-|datachk-|smk-|arg-|disc-|perf-|smk_expt-|expt-|math-'
TEST_FILTER_DSDANGER: '__dgr-|datachk_dgr-|smk_dgr-|arg_dgr-|disc_dgr-|smk_expt_dgr-|expt_dgr-|math_dgr-'
jobs:
################################################################################
# Doc-sync / R CMD check - no backend needed, so this runs once, not per
# backend or per shard.
################################################################################
r-checks:
name: Package checks (doc sync, R CMD check)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: r-lib/actions/setup-pandoc@v2
- uses: r-lib/actions/setup-r@v2
with:
r-version: release
http-user-agent: release
use-public-rspm: true
- name: Install system libraries
run: |
sudo apt-get update -qq
sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y
sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y
- uses: r-lib/actions/setup-r-dependencies@v2
with:
dependencies: 'c("Depends", "Imports", "LinkingTo")'
extra-packages: |
any::rcmdcheck
cran::devtools
cran::usethis
github::datashield/dsDangerClient
needs: check
- name: Check manual updated
run: |
orig_sum=$(find man -type f | sort -u | xargs cat | md5sum)
R -q -e "devtools::document()"
new_sum=$(find man -type f | sort -u | xargs cat | md5sum)
if [ "$orig_sum" != "$new_sum" ]; then
echo "Your committed man/*.Rd files are out of sync with the R headers."
exit 1
fi
continue-on-error: true
- name: Devtools checks
run: |
R -q -e "devtools::check(args = c('--no-examples', '--no-tests'))" | tee azure-pipelines_check.Rout
grep --quiet "^0 errors" azure-pipelines_check.Rout && grep --quiet " 0 warnings" azure-pipelines_check.Rout && grep --quiet " 0 notes" azure-pipelines_check.Rout
continue-on-error: true
################################################################################
# Opal - dsBase suite, sharded 4 ways. Each shard is a fully isolated job
# with its own Opal instance.
################################################################################
opal-dsbase:
name: Opal dsBase tests (${{ matrix.category }})
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
# One matrix entry per test-file category, each with its own testthat
# filter substring. smk and perf are split further by the first letter
# of the function name (after any "ds." prefix) rather than an
# enumerated file list, so newly added test files fall into a bucket
# automatically without editing this matrix. perf (50 fixed 30s-loop
# benchmarks) and smk (116 files) dominated wall-clock before this
# split - see armadillo-dsbase timings in job history.
matrix:
include:
- category: smk-1
filter: 'smk-ds.[a-lA-L]|smk-(checkClass|isDefined)'
- category: smk-2
filter: 'smk-ds.[m-vM-V]'
- category: arg
filter: 'arg-'
- category: perf-1
filter: 'perf-ds.[a-cA-C]'
- category: perf-2
filter: 'perf-ds.[d-mD-M]'
- category: perf-3
filter: 'perf-ds.[n-vN-V]|perf-(conndisconn|void)'
- category: misc
filter: 'datachk-|disc-|expt-|smk_expt-|math-|_-'
env:
DS_DRIVER: OpalDriver
DSBASE_REF: v7.0-dev
steps:
- name: Checkout dsBaseClient
uses: actions/checkout@v4
with:
path: dsBaseClient
- name: Start Opal docker-compose
run: docker compose -f docker-compose_opal.yml up -d --build
working-directory: dsBaseClient
- name: Uninstall default MySQL
run: |
sudo service mysql stop || true
sudo apt-get update
sudo apt-get remove --purge mysql-client mysql-server mysql-common -y
sudo apt-get autoremove -y
sudo apt-get autoclean -y
sudo rm -rf /var/lib/mysql/
- uses: r-lib/actions/setup-pandoc@v2
- uses: r-lib/actions/setup-r@v2
with:
r-version: release
http-user-agent: release
use-public-rspm: true
- name: Install system libraries
run: |
sudo apt-get update -qq
sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y
sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y
- uses: r-lib/actions/setup-r-dependencies@v2
with:
dependencies: 'c("Depends", "Imports", "LinkingTo")'
extra-packages: |
cran::devtools
cran::usethis
cran::covr
cran::fields
cran::meta
cran::metafor
cran::ggplot2
cran::gridExtra
cran::data.table
cran::DSI
cran::opalr
cran::DSOpal
cran::DSLite
cran::MolgenisAuth
cran::MolgenisArmadillo
cran::DSMolgenisArmadillo
cran::DescTools
cran::e1071
github::datashield/dsDangerClient
needs: check
- name: Install test datasets to Opal
run: |
sleep 60
R -q -f "obiba_opal-upload_testing_datasets.R"
working-directory: dsBaseClient/tests/testthat/data_files
- name: Install dsBase to Opal, set disclosure test options
run: |
R -q -e "library(opalr); opal <- opal.login(username = 'administrator', password = 'datashield_test&', url = 'http://localhost:8080/'); opal.put(opal, 'system', 'conf', 'general', '_rPackage'); opal.logout(opal)"
R -q -e "library(opalr); opal <- opal.login('administrator','datashield_test&', url='http://localhost:8080/'); dsadmin.install_github_package(opal, 'dsBase', username = 'datashield', ref = '${{ env.DSBASE_REF }}'); opal.logout(opal)"
sleep 60
expected_version=$(curl -sf "https://raw.githubusercontent.com/datashield/dsBase/${{ env.DSBASE_REF }}/DESCRIPTION" | sed -n 's/^Version: //p' | tr -d '\r')
if [ -z "$expected_version" ]; then
echo "Could not determine expected dsBase version from GitHub"
exit 1
fi
echo "Expected dsBase version: $expected_version"
R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); desc <- dsadmin.package_description(opal, 'dsBase'); opal.logout(opal); installed_version <- desc[['Version']]; cat('Installed dsBase version:', installed_version, '\n'); if (is.null(installed_version) || installed_version != '$expected_version') stop('dsBase version mismatch: expected $expected_version, found ', installed_version)"
R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.profile_init(opal, name = 'default', packages = c('dsBase', 'dsTidyverse', 'resourcer')); opal.logout(opal)"
R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.set_option(opal, 'default.datashield.privacyControlLevel', 'permissive'); opal.logout(opal)"
working-directory: dsBaseClient/tests/testthat/data_files
- name: Run dsBase tests with coverage & JUnit report
run: |
R -q -e "devtools::load_all();"
R -q -e '
cov <- covr::package_coverage(
type = c("none"),
code = c('"'"'
output_file <- file("test_console_output_dsbase.txt");
sink(output_file, split = TRUE);
junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml"));
progress_rep <- testthat::ProgressReporter$new(max_failures = 999999);
multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep));
options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}");
testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"'
)
)
saveRDS(cov, "coverage.rds")
write.csv(covr::coverage_to_list(cov), "coveragelist.csv")' || R_EXIT=$?
cat test_console_output_dsbase.txt
exit "${R_EXIT:-0}"
working-directory: dsBaseClient
- name: Upload shard results
uses: actions/upload-artifact@v4
with:
name: opal-dsbase-${{ matrix.category }}
path: |
dsBaseClient/test_results_dsbase.xml
dsBaseClient/test_console_output_dsbase.txt
dsBaseClient/coveragelist.csv
dsBaseClient/coverage.rds
################################################################################
# Opal - dsDanger suite. Small (~15 files), so it stays a single job rather
# than sharding further.
################################################################################
opal-dsdanger:
name: Opal dsDanger tests
runs-on: ubuntu-latest
timeout-minutes: 60
env:
DS_DRIVER: OpalDriver
DSBASE_REF: v7.0-dev
DSDANGER_REF: '6.3.4'
steps:
- name: Checkout dsBaseClient
uses: actions/checkout@v4
with:
path: dsBaseClient
- name: Start Opal docker-compose
run: docker compose -f docker-compose_opal.yml up -d --build
working-directory: dsBaseClient
- name: Uninstall default MySQL
run: |
sudo service mysql stop || true
sudo apt-get update
sudo apt-get remove --purge mysql-client mysql-server mysql-common -y
sudo apt-get autoremove -y
sudo apt-get autoclean -y
sudo rm -rf /var/lib/mysql/
- uses: r-lib/actions/setup-pandoc@v2
- uses: r-lib/actions/setup-r@v2
with:
r-version: release
http-user-agent: release
use-public-rspm: true
- name: Install system libraries
run: |
sudo apt-get update -qq
sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y
sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y
- uses: r-lib/actions/setup-r-dependencies@v2
with:
dependencies: 'c("Depends", "Imports", "LinkingTo")'
extra-packages: |
cran::devtools
cran::usethis
cran::git2r
cran::covr
cran::fields
cran::meta
cran::metafor
cran::ggplot2
cran::gridExtra
cran::data.table
cran::DSI
cran::opalr
cran::DSOpal
cran::DSLite
cran::MolgenisAuth
cran::MolgenisArmadillo
cran::DSMolgenisArmadillo
cran::DescTools
cran::e1071
github::datashield/dsDangerClient
needs: check
- name: Install dsDangerClient
run: |
R -q -e "
ref <- Sys.getenv('BRANCH_NAME')
ok <- tryCatch({ pak::pkg_install(sprintf('github::datashield/dsDangerClient@%s', ref)); TRUE }, error = function(e) FALSE)
if (!ok) pak::pkg_install('github::datashield/dsDangerClient')"
- name: Install test datasets to Opal
run: |
sleep 60
R -q -f "obiba_opal-upload_testing_datasets.R"
working-directory: dsBaseClient/tests/testthat/data_files
- name: Install dsBase to Opal, set disclosure test options
run: |
R -q -e "library(opalr); opal <- opal.login(username = 'administrator', password = 'datashield_test&', url = 'http://localhost:8080/'); opal.put(opal, 'system', 'conf', 'general', '_rPackage'); opal.logout(opal)"
R -q -e "library(opalr); opal <- opal.login('administrator','datashield_test&', url='http://localhost:8080/'); dsadmin.install_github_package(opal, 'dsBase', username = 'datashield', ref = '${{ env.DSBASE_REF }}'); opal.logout(opal)"
sleep 60
expected_version=$(curl -sf "https://raw.githubusercontent.com/datashield/dsBase/${{ env.DSBASE_REF }}/DESCRIPTION" | sed -n 's/^Version: //p' | tr -d '\r')
if [ -z "$expected_version" ]; then
echo "Could not determine expected dsBase version from GitHub"
exit 1
fi
echo "Expected dsBase version: $expected_version"
R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); desc <- dsadmin.package_description(opal, 'dsBase'); opal.logout(opal); installed_version <- desc[['Version']]; cat('Installed dsBase version:', installed_version, '\n'); if (is.null(installed_version) || installed_version != '$expected_version') stop('dsBase version mismatch: expected $expected_version, found ', installed_version)"
R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.profile_init(opal, name = 'default', packages = c('dsBase', 'dsTidyverse', 'resourcer')); opal.logout(opal)"
R -q -e "library(opalr); opal <- opal.login('administrator', 'datashield_test&', url='http://localhost:8080/'); dsadmin.set_option(opal, 'default.datashield.privacyControlLevel', 'permissive'); opal.logout(opal)"
working-directory: dsBaseClient/tests/testthat/data_files
- name: Install dsDanger package on Opal server
run: |
R -q -e "library(opalr); opal <- opal.login(username = 'administrator', password = 'datashield_test&', url = 'http://localhost:8080'); opal.put(opal, 'system', 'conf', 'general', '_rPackage'); opal.logout(opal)"
R -q -e "library(opalr); opal <- opal.login('administrator','datashield_test&', url='http://localhost:8080/'); dsadmin.install_github_package(opal, 'dsDanger', username = 'datashield', ref = '${{ env.DSDANGER_REF }}'); opal.logout(opal)"
working-directory: dsBaseClient
- name: Run dsDanger tests with JUnit report
run: |
R -q -e '
library(testthat);
output_file <- file("test_console_output_dsdanger.txt");
sink(output_file, split = TRUE);
junit_rep <- JunitReporter$new(file = "test_results_dsdanger.xml");
progress_rep <- ProgressReporter$new(max_failures = 999999);
multi_rep <- MultiReporter$new(reporters = list(progress_rep, junit_rep));
options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}");
testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ env.TEST_FILTER_DSDANGER }}", reporter = multi_rep, stop_on_failure = FALSE)' || R_EXIT=$?
cat test_console_output_dsdanger.txt
exit "${R_EXIT:-0}"
working-directory: dsBaseClient
- name: Upload dsDanger results
uses: actions/upload-artifact@v4
with:
name: opal-dsdanger
path: |
dsBaseClient/test_results_dsdanger.xml
dsBaseClient/test_console_output_dsdanger.txt
################################################################################
# Opal - merge all shard + dsDanger results and publish the report.
################################################################################
opal-report:
name: Opal report
needs: [opal-dsbase, opal-dsdanger]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout dsBaseClient
uses: actions/checkout@v4
with:
path: dsBaseClient
- uses: r-lib/actions/setup-r@v2
with:
r-version: release
use-public-rspm: true
- uses: r-lib/actions/setup-r-dependencies@v2
with:
dependencies: 'c("Depends", "Imports", "LinkingTo")'
extra-packages: |
cran::xml2
cran::covr
github::datashield/dsDangerClient
- name: Download shard/dsdanger results
uses: actions/download-artifact@v4
with:
pattern: 'opal-*'
path: dsBaseClient/artifacts
- name: Merge results and write job summary
run: |
mkdir -p logs
cat artifacts/*/test_console_output_*.txt > logs/test_console_output.txt
Rscript -e '
xml_files <- list.files("artifacts", pattern = "^test_results_.*\\.xml$", recursive = TRUE, full.names = TRUE)
docs <- lapply(xml_files, xml2::read_xml)
root <- xml2::xml_new_root("testsuites")
for (doc in docs) {
for (s in xml2::xml_find_all(doc, ".//testsuite")) xml2::xml_add_child(root, s)
}
xml2::write_xml(root, "logs/test_results.xml")
rds_files <- list.files("artifacts", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE)
if (length(rds_files) > 0) {
tallies <- lapply(rds_files, function(f) covr::tally_coverage(readRDS(f)))
agg <- aggregate(value ~ filename + line, data = do.call(rbind, tallies), FUN = sum)
filecoverage <- tapply(agg$value, agg$filename, function(x) round(sum(x > 0) / length(x) * 100, 2))
totalcoverage <- round(sum(agg$value > 0) / nrow(agg) * 100, 2)
cl <- list(filecoverage = filecoverage, totalcoverage = totalcoverage)
write.csv(cl, "logs/coveragelist.csv")
low <- head(sort(cl$filecoverage), 5)
cov_lines <- c(
sprintf("**Combined coverage: %.1f%%**", cl$totalcoverage),
"",
"Lowest-covered files:",
paste0("- `", names(low), "`: ", low, "%")
)
} else {
cov_lines <- "No coverage data found."
}
doc <- xml2::read_xml("logs/test_results.xml")
suites <- xml2::xml_find_all(doc, ".//testsuite")
n_tests <- sum(as.integer(xml2::xml_attr(suites, "tests")), na.rm = TRUE)
n_failures <- sum(as.integer(xml2::xml_attr(suites, "failures")), na.rm = TRUE)
n_errors <- sum(as.integer(xml2::xml_attr(suites, "errors")), na.rm = TRUE)
n_skipped <- sum(as.integer(xml2::xml_attr(suites, "skipped")), na.rm = TRUE)
failed <- xml2::xml_find_all(doc, ".//testcase[failure or error]")
fail_lines <- if (length(failed) > 0) {
msgs <- vapply(failed, function(tc) {
node <- xml2::xml_find_first(tc, "failure|error")
m <- xml2::xml_attr(node, "message")
if (is.na(m) || !nzchar(m)) m <- trimws(xml2::xml_text(node))
m
}, character(1))
c("", "### Failed tests",
paste0("- `", xml2::xml_attr(failed, "classname"), "::", xml2::xml_attr(failed, "name"), "` - ", msgs))
} else {
character(0)
}
summary <- c(
"## Opal dsBase/dsDanger test results",
"",
sprintf("**%d tests** - %d failures, %d errors, %d skipped", n_tests, n_failures, n_errors, n_skipped),
"",
cov_lines,
fail_lines
)
writeLines(summary, Sys.getenv("GITHUB_STEP_SUMMARY"))
'
working-directory: dsBaseClient
- name: Upload merged results
uses: actions/upload-artifact@v4
with:
name: opal-report-results
path: |
dsBaseClient/logs/test_results.xml
dsBaseClient/logs/test_console_output.txt
dsBaseClient/logs/coveragelist.csv
################################################################################
# Armadillo - dsBase suite, sharded 4 ways. Each shard downloads and runs its
# own Armadillo jar instance.
#
# Runs as a plain `java -jar` process instead of docker-compose: Armadillo
# self-manages its own Rock container over the host Docker socket
# (docker-management-enabled: true / docker-run-in-container: false), which
# avoids building/pulling the old custom armadillo_citest image and its
# dockerised Armadillo layer. The latest GitHub release jar is downloaded at
# run time. No process is restarted after installing dsBase - install then
# whitelist directly.
################################################################################
armadillo-dsbase:
name: Armadillo dsBase tests (${{ matrix.category }})
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
# One matrix entry per test-file category, each with its own testthat
# filter substring. smk and perf are split further by the first letter
# of the function name (after any "ds." prefix) rather than an
# enumerated file list, so newly added test files fall into a bucket
# automatically without editing this matrix. perf (50 fixed 30s-loop
# benchmarks) and smk (116 files) dominated wall-clock before this
# split - see armadillo-dsbase timings in job history.
matrix:
include:
- category: smk-1
filter: 'smk-ds.[a-lA-L]|smk-(checkClass|isDefined)'
- category: smk-2
filter: 'smk-ds.[m-vM-V]'
- category: arg
filter: 'arg-'
- category: perf-1
filter: 'perf-ds.[a-cA-C]'
- category: perf-2
filter: 'perf-ds.[d-mD-M]'
- category: perf-3
filter: 'perf-ds.[n-vN-V]|perf-(conndisconn|void)'
- category: misc
filter: 'datachk-|disc-|expt-|smk_expt-|math-|_-'
env:
DS_DRIVER: ArmadilloDriver
DSBASE_TARBALL: dsBase_7.0.0-permissive.tar.gz
steps:
- name: Checkout dsBaseClient
uses: actions/checkout@v4
with:
path: dsBaseClient
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Download and start Armadillo (jar)
run: |
mkdir -p armadillo_home/config armadillo_home/storage armadillo_home/logs
cat > armadillo_home/config/application.yml <<EOF
armadillo:
docker-management-enabled: true
docker-run-in-container: false
profiles:
- name: default
image: datashield/rock_citest-permissive:latest
port: 8085
package-whitelist:
- dsBase
- dsTidyverse
- resourcer
function-blacklist: [ ]
options:
datashield:
seed: 342325352
spring:
security:
user:
name: admin
password: admin
servlet:
multipart:
max-file-size: 1000MB
max-request-size: 1000MB
storage:
root-dir: $(pwd)/armadillo_home/storage
audit.log.path: '$(pwd)/armadillo_home/logs/audit.log'
stdout.log.path: '$(pwd)/armadillo_home/logs/armadillo.log'
logging:
level:
root: "warn"
org.molgenis: "warn"
EOF
release_json=$(curl -sf -H "Authorization: Bearer ${{ env.GITHUB_TOKEN }}" https://api.github.com/repos/molgenis/molgenis-service-armadillo/releases/latest) || { curl -s -H "Authorization: Bearer ${{ env.GITHUB_TOKEN }}" https://api.github.com/repos/molgenis/molgenis-service-armadillo/releases/latest; exit 1; }
jar_url=$(echo "$release_json" | jq -r '.assets[] | select(.name | endswith(".jar")) | .browser_download_url')
echo "Installing Armadillo $(echo "$release_json" | jq -r '.tag_name')"
curl -fSL -o armadillo_home/armadillo.jar "$jar_url"
JAVA_TOOL_OPTIONS="-Xms1G -Xmx2G -XX:+UseG1GC" \
nohup java -jar armadillo_home/armadillo.jar \
--server.port=8080 \
--spring.config.additional-location=file:$(pwd)/armadillo_home/config/application.yml \
> armadillo_home/logs/stdout.log 2>&1 &
echo $! > armadillo_home/armadillo.pid
docker pull datashield/rock_citest-permissive:latest
working-directory: dsBaseClient
- name: Uninstall default MySQL
run: |
sudo service mysql stop || true
sudo apt-get update
sudo apt-get remove --purge mysql-client mysql-server mysql-common -y
sudo apt-get autoremove -y
sudo apt-get autoclean -y
sudo rm -rf /var/lib/mysql/
- uses: r-lib/actions/setup-pandoc@v2
- uses: r-lib/actions/setup-r@v2
with:
r-version: release
http-user-agent: release
use-public-rspm: true
- name: Install system libraries
run: |
sudo apt-get update -qq
sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y
sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y
- uses: r-lib/actions/setup-r-dependencies@v2
with:
dependencies: 'c("Depends", "Imports", "LinkingTo")'
extra-packages: |
cran::devtools
cran::usethis
cran::covr
cran::fields
cran::meta
cran::metafor
cran::ggplot2
cran::gridExtra
cran::data.table
cran::DSI
cran::opalr
cran::DSOpal
cran::DSLite
cran::MolgenisAuth
cran::MolgenisArmadillo
cran::DSMolgenisArmadillo
cran::DescTools
cran::e1071
github::datashield/dsDangerClient
needs: check
- name: Wait for Armadillo to be ready
run: |
for i in $(seq 1 60); do
if curl -fsS http://localhost:8080/actuator/health 2>/dev/null | grep -q '"status":"UP"'; then
echo "Armadillo is ready"
exit 0
fi
sleep 5
done
echo "Armadillo did not become ready in time"
cat armadillo_home/logs/stdout.log || true
exit 1
working-directory: dsBaseClient
- name: Install test datasets to Armadillo
run: R -q -f "molgenis_armadillo-upload_testing_datasets.R"
working-directory: dsBaseClient/tests/testthat/data_files
- name: Install dsBase to Armadillo
run: |
curl -sf -u admin:admin -X POST http://localhost:8080/ds-profiles/default/start
profile_status=""
for i in $(seq 1 30); do
profile_status=$(curl -sf -u admin:admin http://localhost:8080/ds-profiles/status | jq -r '.[] | select(.name=="default") | .status')
[ "$profile_status" == "RUNNING" ] && break
sleep 10
done
if [ "$profile_status" != "RUNNING" ]; then
echo "default profile container did not become RUNNING in time (status: $profile_status)"
exit 1
fi
install_status=$(curl -u admin:admin -H 'Content-Type: multipart/form-data' -F "file=@${{ env.DSBASE_TARBALL }}" -o /dev/null -w '%{http_code}' -X POST http://localhost:8080/install-package)
if [ "$install_status" != "200" ]; then
echo "dsBase install request failed with HTTP status $install_status"
exit 1
fi
expected_version=$(tar -xOzf ${{ env.DSBASE_TARBALL }} dsBase/DESCRIPTION | sed -n 's/^Version: //p' | tr -d '\r')
if [ -z "$expected_version" ]; then
echo "Could not determine expected dsBase version from ${{ env.DSBASE_TARBALL }}"
exit 1
fi
echo "Expected dsBase version: $expected_version"
actual_version=""
for i in $(seq 1 30); do
packages_json=$(curl -sf -u admin:admin -X GET http://localhost:8080/packages || true)
actual_version=$(echo "$packages_json" | jq -r '.[] | select(.name == "dsBase") | .version' 2>/dev/null || true)
if [ "$actual_version" == "$expected_version" ]; then
break
fi
sleep 10
done
echo "Installed dsBase version: $actual_version"
if [ "$actual_version" != "$expected_version" ]; then
echo "dsBase version mismatch: expected $expected_version, found '$actual_version'"
exit 1
fi
curl -u admin:admin -X POST http://localhost:8080/whitelist/dsBase
working-directory: dsBaseClient
- name: Dump Armadillo server log
if: failure()
run: grep "Caused by:" armadillo_home/logs/stdout.log | sort -u
working-directory: dsBaseClient
- name: Run dsBase tests with coverage & JUnit report
run: |
R -q -e "devtools::load_all();"
R -q -e '
cov <- covr::package_coverage(
type = c("none"),
code = c('"'"'
output_file <- file("test_console_output_dsbase.txt");
sink(output_file, split = TRUE);
junit_rep <- testthat::JunitReporter$new(file = file.path(getwd(), "test_results_dsbase.xml"));
progress_rep <- testthat::ProgressReporter$new(max_failures = 999999);
multi_rep <- testthat::MultiReporter$new(reporters = list(progress_rep, junit_rep));
options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}");
testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ matrix.filter }}", reporter = multi_rep, stop_on_failure = FALSE)'"'"'
)
)
saveRDS(cov, "coverage.rds")
write.csv(covr::coverage_to_list(cov), "coveragelist.csv")' || R_EXIT=$?
cat test_console_output_dsbase.txt
exit "${R_EXIT:-0}"
working-directory: dsBaseClient
- name: Upload shard results
uses: actions/upload-artifact@v4
with:
name: armadillo-dsbase-${{ matrix.category }}
path: |
dsBaseClient/test_results_dsbase.xml
dsBaseClient/test_console_output_dsbase.txt
dsBaseClient/coveragelist.csv
dsBaseClient/coverage.rds
################################################################################
# Armadillo - dsDanger suite. Small, so it stays a single job.
################################################################################
armadillo-dsdanger:
name: Armadillo dsDanger tests
runs-on: ubuntu-latest
timeout-minutes: 60
env:
DS_DRIVER: ArmadilloDriver
DSBASE_TARBALL: dsBase_7.0.0-permissive.tar.gz
DSDANGER_TARBALL: dsDanger_6.3.4.tar.gz
steps:
- name: Checkout dsBaseClient
uses: actions/checkout@v4
with:
path: dsBaseClient
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Download and start Armadillo (jar)
run: |
mkdir -p armadillo_home/config armadillo_home/storage armadillo_home/logs
cat > armadillo_home/config/application.yml <<EOF
armadillo:
docker-management-enabled: true
docker-run-in-container: false
profiles:
- name: default
image: datashield/rock_citest-permissive:latest
port: 8085
package-whitelist:
- dsBase
- dsTidyverse
- resourcer
function-blacklist: [ ]
options:
datashield:
seed: 342325352
spring:
security:
user:
name: admin
password: admin
servlet:
multipart:
max-file-size: 1000MB
max-request-size: 1000MB
storage:
root-dir: $(pwd)/armadillo_home/storage
audit.log.path: '$(pwd)/armadillo_home/logs/audit.log'
stdout.log.path: '$(pwd)/armadillo_home/logs/armadillo.log'
logging:
level:
root: "warn"
org.molgenis: "warn"
EOF
release_json=$(curl -sf -H "Authorization: Bearer ${{ env.GITHUB_TOKEN }}" https://api.github.com/repos/molgenis/molgenis-service-armadillo/releases/latest) || { curl -s -H "Authorization: Bearer ${{ env.GITHUB_TOKEN }}" https://api.github.com/repos/molgenis/molgenis-service-armadillo/releases/latest; exit 1; }
jar_url=$(echo "$release_json" | jq -r '.assets[] | select(.name | endswith(".jar")) | .browser_download_url')
echo "Installing Armadillo $(echo "$release_json" | jq -r '.tag_name')"
curl -fSL -o armadillo_home/armadillo.jar "$jar_url"
JAVA_TOOL_OPTIONS="-Xms1G -Xmx2G -XX:+UseG1GC" \
nohup java -jar armadillo_home/armadillo.jar \
--server.port=8080 \
--spring.config.additional-location=file:$(pwd)/armadillo_home/config/application.yml \
> armadillo_home/logs/stdout.log 2>&1 &
echo $! > armadillo_home/armadillo.pid
docker pull datashield/rock_citest-permissive:latest
working-directory: dsBaseClient
- name: Uninstall default MySQL
run: |
sudo service mysql stop || true
sudo apt-get update
sudo apt-get remove --purge mysql-client mysql-server mysql-common -y
sudo apt-get autoremove -y
sudo apt-get autoclean -y
sudo rm -rf /var/lib/mysql/
- uses: r-lib/actions/setup-pandoc@v2
- uses: r-lib/actions/setup-r@v2
with:
r-version: release
http-user-agent: release
use-public-rspm: true
- name: Install system libraries
run: |
sudo apt-get update -qq
sudo apt-get install -qq libxml2-dev libcurl4-openssl-dev libssl-dev libgsl-dev libgit2-dev -y
sudo apt-get install -qq libharfbuzz-dev libfribidi-dev libmagick++-dev libudunits2-dev libuv1-dev -y
- uses: r-lib/actions/setup-r-dependencies@v2
with:
dependencies: 'c("Depends", "Imports", "LinkingTo")'
extra-packages: |
cran::devtools
cran::usethis
cran::git2r
cran::covr
cran::fields
cran::meta
cran::metafor
cran::ggplot2
cran::gridExtra
cran::data.table
cran::DSI
cran::opalr
cran::DSOpal
cran::DSLite
cran::MolgenisAuth
cran::MolgenisArmadillo
cran::DSMolgenisArmadillo
cran::DescTools
cran::e1071
github::datashield/dsDangerClient
needs: check
- name: Install dsDangerClient
run: |
R -q -e "
ref <- Sys.getenv('BRANCH_NAME')
ok <- tryCatch({ pak::pkg_install(sprintf('github::datashield/dsDangerClient@%s', ref)); TRUE }, error = function(e) FALSE)
if (!ok) pak::pkg_install('github::datashield/dsDangerClient')"
- name: Wait for Armadillo to be ready
run: |
for i in $(seq 1 60); do
if curl -fsS http://localhost:8080/actuator/health 2>/dev/null | grep -q '"status":"UP"'; then
echo "Armadillo is ready"
exit 0
fi
sleep 5
done
echo "Armadillo did not become ready in time"
cat armadillo_home/logs/stdout.log || true
exit 1
working-directory: dsBaseClient
- name: Install test datasets to Armadillo
run: R -q -f "molgenis_armadillo-upload_testing_datasets.R"
working-directory: dsBaseClient/tests/testthat/data_files
- name: Install dsBase to Armadillo
run: |
curl -sf -u admin:admin -X POST http://localhost:8080/ds-profiles/default/start
profile_status=""
for i in $(seq 1 30); do
profile_status=$(curl -sf -u admin:admin http://localhost:8080/ds-profiles/status | jq -r '.[] | select(.name=="default") | .status')
[ "$profile_status" == "RUNNING" ] && break
sleep 10
done
if [ "$profile_status" != "RUNNING" ]; then
echo "default profile container did not become RUNNING in time (status: $profile_status)"
exit 1
fi
install_status=$(curl -u admin:admin -H 'Content-Type: multipart/form-data' -F "file=@${{ env.DSBASE_TARBALL }}" -o /dev/null -w '%{http_code}' -X POST http://localhost:8080/install-package)
if [ "$install_status" != "200" ]; then
echo "dsBase install request failed with HTTP status $install_status"
exit 1
fi
expected_version=$(tar -xOzf ${{ env.DSBASE_TARBALL }} dsBase/DESCRIPTION | sed -n 's/^Version: //p' | tr -d '\r')
if [ -z "$expected_version" ]; then
echo "Could not determine expected dsBase version from ${{ env.DSBASE_TARBALL }}"
exit 1
fi
echo "Expected dsBase version: $expected_version"
actual_version=""
for i in $(seq 1 30); do
packages_json=$(curl -sf -u admin:admin -X GET http://localhost:8080/packages || true)
actual_version=$(echo "$packages_json" | jq -r '.[] | select(.name == "dsBase") | .version' 2>/dev/null || true)
if [ "$actual_version" == "$expected_version" ]; then
break
fi
sleep 10
done
echo "Installed dsBase version: $actual_version"
if [ "$actual_version" != "$expected_version" ]; then
echo "dsBase version mismatch: expected $expected_version, found '$actual_version'"
exit 1
fi
curl -u admin:admin -X POST http://localhost:8080/whitelist/dsBase
working-directory: dsBaseClient
- name: Dump Armadillo server log
if: failure()
run: grep "Caused by:" armadillo_home/logs/stdout.log | sort -u
working-directory: dsBaseClient
- name: Install dsDanger package on Armadillo server
run: |
curl -u admin:admin http://localhost:8080/whitelist
install_status=$(curl -u admin:admin -H 'Content-Type: multipart/form-data' -F "file=@${{ env.DSDANGER_TARBALL }}" -o /dev/null -w '%{http_code}' -X POST http://localhost:8080/install-package)
if [ "$install_status" != "200" ]; then
echo "dsDanger install request failed with HTTP status $install_status"
exit 1
fi
for i in $(seq 1 30); do
packages_json=$(curl -sf -u admin:admin -X GET http://localhost:8080/packages || true)
if echo "$packages_json" | jq -e '.[] | select(.name == "dsDanger")' >/dev/null 2>&1; then
break
fi
sleep 10
done
curl -u admin:admin -X POST http://localhost:8080/whitelist/dsDanger
curl -u admin:admin http://localhost:8080/whitelist
working-directory: dsBaseClient
- name: Run dsDanger tests with JUnit report
run: |
R -q -e '
library(testthat);
output_file <- file("test_console_output_dsdanger.txt");
sink(output_file, split = TRUE);
junit_rep <- JunitReporter$new(file = "test_results_dsdanger.xml");
progress_rep <- ProgressReporter$new(max_failures = 999999);
multi_rep <- MultiReporter$new(reporters = list(progress_rep, junit_rep));
options("datashield.return_errors" = FALSE, "default_driver" = "${{ env.DS_DRIVER }}");
testthat::test_package("${{ env.PROJECT_NAME }}", filter = "${{ env.TEST_FILTER_DSDANGER }}", reporter = multi_rep, stop_on_failure = FALSE)' || R_EXIT=$?
cat test_console_output_dsdanger.txt
exit "${R_EXIT:-0}"
working-directory: dsBaseClient
- name: Upload dsDanger results
uses: actions/upload-artifact@v4
with:
name: armadillo-dsdanger
path: |
dsBaseClient/test_results_dsdanger.xml
dsBaseClient/test_console_output_dsdanger.txt
################################################################################
# Armadillo - merge all shard + dsDanger results and publish the report.
################################################################################
armadillo-report:
name: Armadillo report
needs: [armadillo-dsbase, armadillo-dsdanger]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout dsBaseClient
uses: actions/checkout@v4
with:
path: dsBaseClient
- uses: r-lib/actions/setup-r@v2
with:
r-version: release
use-public-rspm: true
- uses: r-lib/actions/setup-r-dependencies@v2
with:
dependencies: 'c("Depends", "Imports", "LinkingTo")'
extra-packages: |
cran::xml2
cran::covr
github::datashield/dsDangerClient
- name: Download shard/dsdanger results
uses: actions/download-artifact@v4
with:
pattern: 'armadillo-*'
path: dsBaseClient/artifacts
- name: Merge results and write job summary
run: |
mkdir -p logs
cat artifacts/*/test_console_output_*.txt > logs/test_console_output.txt
Rscript -e '
xml_files <- list.files("artifacts", pattern = "^test_results_.*\\.xml$", recursive = TRUE, full.names = TRUE)
docs <- lapply(xml_files, xml2::read_xml)
root <- xml2::xml_new_root("testsuites")
for (doc in docs) {
for (s in xml2::xml_find_all(doc, ".//testsuite")) xml2::xml_add_child(root, s)
}
xml2::write_xml(root, "logs/test_results.xml")
rds_files <- list.files("artifacts", pattern = "coverage\\.rds$", recursive = TRUE, full.names = TRUE)
if (length(rds_files) > 0) {
tallies <- lapply(rds_files, function(f) covr::tally_coverage(readRDS(f)))
agg <- aggregate(value ~ filename + line, data = do.call(rbind, tallies), FUN = sum)
filecoverage <- tapply(agg$value, agg$filename, function(x) round(sum(x > 0) / length(x) * 100, 2))
totalcoverage <- round(sum(agg$value > 0) / nrow(agg) * 100, 2)
cl <- list(filecoverage = filecoverage, totalcoverage = totalcoverage)
write.csv(cl, "logs/coveragelist.csv")
low <- head(sort(cl$filecoverage), 5)
cov_lines <- c(
sprintf("**Combined coverage: %.1f%%**", cl$totalcoverage),
"",
"Lowest-covered files:",
paste0("- `", names(low), "`: ", low, "%")
)
} else {
cov_lines <- "No coverage data found."
}
doc <- xml2::read_xml("logs/test_results.xml")
suites <- xml2::xml_find_all(doc, ".//testsuite")
n_tests <- sum(as.integer(xml2::xml_attr(suites, "tests")), na.rm = TRUE)
n_failures <- sum(as.integer(xml2::xml_attr(suites, "failures")), na.rm = TRUE)
n_errors <- sum(as.integer(xml2::xml_attr(suites, "errors")), na.rm = TRUE)
n_skipped <- sum(as.integer(xml2::xml_attr(suites, "skipped")), na.rm = TRUE)
failed <- xml2::xml_find_all(doc, ".//testcase[failure or error]")
fail_lines <- if (length(failed) > 0) {
msgs <- vapply(failed, function(tc) {
node <- xml2::xml_find_first(tc, "failure|error")
m <- xml2::xml_attr(node, "message")
if (is.na(m) || !nzchar(m)) m <- trimws(xml2::xml_text(node))
m
}, character(1))
c("", "### Failed tests",
paste0("- `", xml2::xml_attr(failed, "classname"), "::", xml2::xml_attr(failed, "name"), "` - ", msgs))
} else {
character(0)
}
summary <- c(
"## Armadillo dsBase/dsDanger test results",
"",
sprintf("**%d tests** - %d failures, %d errors, %d skipped", n_tests, n_failures, n_errors, n_skipped),
"",
cov_lines,
fail_lines
)
writeLines(summary, Sys.getenv("GITHUB_STEP_SUMMARY"))
'
working-directory: dsBaseClient
- name: Upload merged results
uses: actions/upload-artifact@v4
with:
name: armadillo-report-results
path: |
dsBaseClient/logs/test_results.xml
dsBaseClient/logs/test_console_output.txt
dsBaseClient/logs/coveragelist.csv