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
4 changes: 3 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -29,6 +29,8 @@ Imports:
withr,
yaml
Suggests:
gt,
gtExtras,
reticulate,
rstudioapi,
testthat (>= 3.0.0)
Expand Down
7 changes: 7 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 2 additions & 3 deletions R/check_python.R
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
25 changes: 16 additions & 9 deletions R/check_quarto_python.R
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
18 changes: 18 additions & 0 deletions README.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
)
```
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
)
```
20 changes: 9 additions & 11 deletions inst/extdata/report/report.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -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")`

Expand All @@ -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}
Expand Down Expand Up @@ -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.


Expand Down Expand Up @@ -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(
Expand Down
33 changes: 26 additions & 7 deletions tests/testthat/test-check_quarto_python.R
Original file line number Diff line number Diff line change
@@ -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", {
Expand Down
Loading