diff --git a/DESCRIPTION b/DESCRIPTION index 3cecc67..a7127b3 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Type: Package Package: audit.workbench Title: RStudio/Workbench User Acceptance Tests -Version: 0.6.27 +Version: 0.6.28 Authors@R: person("Jumping", "Rivers", , "info@jumpingrivers.com", role = c("aut", "cre")) Description: Testing whether data scientists can do what they expect in @@ -29,6 +29,8 @@ Imports: withr, yaml Suggests: + gt, + gtExtras, reticulate, rstudioapi, testthat (>= 3.0.0) diff --git a/NEWS.md b/NEWS.md index a969d73..1330b59 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,10 @@ +# audit.workbench 0.6.28 + +## Bug fixes and minor improvements + +* The report names the user who ran the audit, lists skipped checks in their own group, and strips `http://` from the server name. Its wording no longer comes from Connect, the securityheaders.com link points at securityheaders.com, `gt` and `gtExtras` are in Suggests, and the README says how to render it (#40). +* The Quarto Python and pip checks prefer the newest Python in `/opt/python` over `python3` on the `PATH`, which on Debian and Ubuntu can't make a virtual environment without `python3-venv`. If a virtual environment can't be made, the error names the Python it tried (#41). + # audit.workbench 0.6.27 ## Bug fixes and minor improvements diff --git a/R/check_python.R b/R/check_python.R index 8dd0650..ef27fe3 100644 --- a/R/check_python.R +++ b/R/check_python.R @@ -47,11 +47,10 @@ check_python_reticulate = R6::R6Class( # Installs numpy into a temporary venv, so the user's own Python is never changed python_pip_tests = function(python = find_python()) { if (!nzchar(python)) { - cli::cli_abort("Can't find Python: looked for python3 on the PATH and in /opt/python") + cli::cli_abort("Can't find Python: looked in /opt/python and for python3 on the PATH") } venv = withr::local_tempdir(pattern = "pip-venv-") - processx::run(python, c("-m", "venv", venv)) - venv_python = file.path(venv, "bin", "python") + venv_python = make_venv(python, venv) pip_version = processx::run(venv_python, c("-m", "pip", "--version"))$stdout stopifnot("pip is older than version 20" = grepl("pip [2-9][0-9]", pip_version)) diff --git a/R/check_quarto_python.R b/R/check_quarto_python.R index cb9ff78..68cfe3f 100644 --- a/R/check_quarto_python.R +++ b/R/check_quarto_python.R @@ -29,33 +29,40 @@ check_quarto_python = R6::R6Class( ) ) -# python3 on the PATH, else the newest Python in /opt/python, where Posit installs it +# The newest Python in /opt/python, where Posit installs it, else python3 on the PATH. +# A distribution's python3 may lack venv (python3-venv on Debian and Ubuntu). find_python = function(root_dir = "/opt/python") { - python = unname(Sys.which("python3")) - if (nzchar(python)) { - return(python) - } pythons = Sys.glob(file.path(root_dir, "*", "bin", "python3")) versions = basename(dirname(dirname(pythons))) pythons = pythons[grepl("^[0-9]+\\.[0-9]+", versions)] if (length(pythons) == 0L) { - return("") + return(unname(Sys.which("python3"))) } versions = numeric_version(basename(dirname(dirname(pythons))), strict = FALSE) # order() rather than which.max(), which can't sort versions before R 4.6 pythons[order(versions, decreasing = TRUE)[1]] } +make_venv = function(python, venv) { + made = processx::run(python, c("-m", "venv", venv), error_on_status = FALSE) + if (made$status != 0L) { + cli::cli_abort(c( + "Can't make a virtual environment with {.path {python}}", + "i" = trimws(made$stderr) + )) + } + invisible(file.path(venv, "bin", "python")) +} + render_quarto_python = function(quarto_dir, debug_level, python = find_python()) { if (!nzchar(python)) { - cli::cli_abort("Can't find Python: looked for python3 on the PATH and in /opt/python") + cli::cli_abort("Can't find Python: looked in /opt/python and for python3 on the PATH") } venv = file.path(tempdir(), "quarto-python-venv") on.exit({ if (debug_level < 2L && fs::dir_exists(venv)) fs::dir_delete(venv) }) #nolint - processx::run(python, c("-m", "venv", venv)) - venv_python = file.path(venv, "bin", "python") + venv_python = make_venv(python, venv) processx::run( venv_python, c("-m", "pip", "install", "--quiet", "-r", file.path(quarto_dir, "requirements.txt")) diff --git a/README.Rmd b/README.Rmd index e604d00..777bbf8 100644 --- a/README.Rmd +++ b/README.Rmd @@ -40,3 +40,21 @@ Running the test is straightforward library("audit.workbench") check(server = "https://www.server.name/") ``` + +## Rendering the report + +Save the output of `check()`, then render the Quarto report that comes with the package. +The report needs {gt} and {gtExtras}, so install those first. + +```{r, eval = FALSE} +out = check(server = "https://www.server.name/") +saveRDS(out, "auditworkbench.rds") + +install.packages(c("gt", "gtExtras")) +report = system.file("extdata", "report", "report.qmd", package = "audit.workbench") +file.copy(report, ".") +quarto::quarto_render( + "report.qmd", + execute_params = list(input = normalizePath("auditworkbench.rds")) +) +``` diff --git a/README.md b/README.md index 49f093c..02f4fad 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,21 @@ Running the test is straightforward library("audit.workbench") check(server = "https://www.server.name/") ``` + +## Rendering the report + +Save the output of `check()`, then render the Quarto report that comes with the package. +The report needs {gt} and {gtExtras}, so install those first. + +``` r +out = check(server = "https://www.server.name/") +saveRDS(out, "auditworkbench.rds") + +install.packages(c("gt", "gtExtras")) +report = system.file("extdata", "report", "report.qmd", package = "audit.workbench") +file.copy(report, ".") +quarto::quarto_render( + "report.qmd", + execute_params = list(input = normalizePath("auditworkbench.rds")) +) +``` diff --git a/inst/extdata/report/report.qmd b/inst/extdata/report/report.qmd index 0d33a61..1781d0a 100644 --- a/inst/extdata/report/report.qmd +++ b/inst/extdata/report/report.qmd @@ -25,21 +25,19 @@ Yes, I know the code is horrible - see above comments :) ```{r} #| echo: false # quarto render report.qmd -P input:output.rds -library("lubridate", quietly = TRUE, warn.conflicts = FALSE) library("gt") library("audit.workbench") out = readRDS(params$input) -audit_details = out$audit_details severe = "#f59d9a" moderate = "#f5cf9a" trivial = "#c0d1ed" audit_details = out$audit_details -server_name = stringr::str_remove_all(audit_details$server, "(^http?s://|/$)") +server_name = stringr::str_remove_all(audit_details$server, "(^https?://|/$)") ``` ## Overview (`r server_name`) -This audit was run by user `r glue::glue("{audit_details$name}")`, using {audit.workbench} R package (version `r audit_details$version`). +This audit was run by user `r audit_details$user`, using {audit.workbench} R package (version `r audit_details$version`). `r audit.base::get_quarto_posit_version_msg(out, "workbench")` @@ -63,7 +61,7 @@ if (isTRUE(out$timezone)) { This audit examines the server headings of the Posit server (or the server sitting in front of Posit). It is important to note, that there may be cases where you may want to deviate from setting these headings. However, this is the exception and not the rule. -The headings listed below follow the recommendations of [securityheaders.com](https://serverheaders.com/) and (to a certain extent) +The headings listed below follow the recommendations of [securityheaders.com](https://securityheaders.com/) and (to a certain extent) Mozilla. Where the heading has not been set, we provide links to documentation. ```{r} @@ -92,8 +90,7 @@ gt::gt(headers) |> ## System Dependencies -Posit allows users to push dashboards and markdown documents. -However, these applications contain R packages that have system level dependencies. +Users install R packages on Workbench, and many of these packages have system level dependencies. This check tries to determine which R packages would fail to install. @@ -154,18 +151,19 @@ gt::gt(software) |> ) ``` -## Deployments +## Checks -This audit deploys a variety of applications, ranging from Shiny apps to quarto documents. -The test determines if the application has been successful launched. +This audit runs a set of checks, ranging from installing R packages to rendering Quarto documents. +Each check records whether it passed, failed or was skipped. ```{r} #| echo: false deploy = audit.base::get_quarto_deploy(out) gt::gt(deploy) |> - gt::cols_label("context" = "Deployment", "time_taken" = "Time (secs)") |> + gt::cols_label("context" = "Check", "time_taken" = "Time (secs)") |> gt::cols_align("right", "time_taken") |> gt::cols_hide(c("passed", "group", "short")) |> + gt::tab_row_group(label = "Skipped", rows = which(is.na(deploy$passed))) |> gt::tab_row_group(label = "Failed", rows = which(!deploy$passed)) |> gt::tab_row_group(label = "Looks good", rows = which(deploy$passed)) |> gtExtras::gt_merge_stack( diff --git a/tests/testthat/test-check_quarto_python.R b/tests/testthat/test-check_quarto_python.R index d7339f8..4a1aa9f 100644 --- a/tests/testthat/test-check_quarto_python.R +++ b/tests/testthat/test-check_quarto_python.R @@ -1,12 +1,31 @@ -test_that("find_python() falls back to the newest Python in /opt/python", { - root = withr::local_tempdir() +test_that("find_python() prefers the newest Python in /opt/python, then the PATH", { + opt = withr::local_tempdir() for (v in c("3.9.1", "3.12.4", "3.10.2", "current")) { - dir.create(file.path(root, v, "bin"), recursive = TRUE) - file.create(file.path(root, v, "bin", "python3")) + dir.create(file.path(opt, v, "bin"), recursive = TRUE) + file.create(file.path(opt, v, "bin", "python3")) } - withr::local_envvar(PATH = "") - expect_identical(find_python(root), file.path(root, "3.12.4", "bin", "python3")) - expect_identical(find_python(withr::local_tempdir()), "") + path = withr::local_tempdir() + file.create(file.path(path, "python3")) + Sys.chmod(file.path(path, "python3"), "755") + empty = withr::local_tempdir() + + cases = list( + list(root = opt, path = path, expected = file.path(opt, "3.12.4", "bin", "python3")), + list(root = empty, path = path, expected = file.path(path, "python3")), + list(root = empty, path = empty, expected = "") + ) + for (case in cases) { + withr::with_envvar(c(PATH = case$path), { + expect_identical(find_python(case$root), case$expected) + }) + } +}) + +test_that("make_venv() names the Python that can't make a virtual environment", { + python = file.path(withr::local_tempdir(), "python3") + writeLines(c("#!/bin/sh", "echo 'ensurepip is not available' >&2", "exit 1"), python) + Sys.chmod(python, "755") + expect_error(make_venv(python, withr::local_tempdir()), "Can't make a virtual environment with") }) test_that("render_quarto_python() needs a Python", {