diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 82846d52e..cc10e61fb 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -185,6 +185,7 @@ jobs: Rscript -e "rhino::build_sass()" Rscript -e "rhino::build_js()" Rscript -e "rhino::test_r()" + Rscript -e "rhino::covr_r()" - name: Cypress e2e tests should confirm RhinoApp works if: runner.os != 'Windows' diff --git a/DESCRIPTION b/DESCRIPTION index cda222c2c..dc78e613d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: rhino Title: A Framework for Enterprise Shiny Applications -Version: 1.11.0.9000 +Version: 1.11.0.9001 Authors@R: c( person("Kamil", "Żyła", role = c("aut", "cre"), email = "opensource+kamil@appsilon.com"), @@ -19,7 +19,6 @@ BugReports: https://github.com/Appsilon/rhino/issues License: LGPL-3 Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.2 Depends: R (>= 2.10) Imports: @@ -29,6 +28,7 @@ Imports: callr, cli, config, + covr (>= 3.6.5), fs, glue, lintr (>= 3.0.0), @@ -44,7 +44,8 @@ Imports: withr, yaml Suggests: - covr, + DT, + htmltools, knitr, lifecycle, mockery, @@ -58,3 +59,4 @@ LazyData: true Config/testthat/edition: 3 Config/testthat/parallel: true Language: en-US +Config/roxygen2/version: 8.0.0 diff --git a/NAMESPACE b/NAMESPACE index 4e46ae0ed..38f0d9cf7 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -5,6 +5,8 @@ export(app) export(auto_test_r) export(build_js) export(build_sass) +export(covr_r) +export(covr_report) export(devmode) export(diagnostics) export(format_js) diff --git a/NEWS.md b/NEWS.md index 28d1fa01d..dd13daf0e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,7 @@ 1. Added `AGENTS.md` with repository guidance for AI coding agents. 2. Fixed: `main.R` template imports functions in alphabetical order. +3. Added `covr_r()` and `covr_report()` to execute test coverage for Rhino applications using `{covr}`. # [rhino 1.11.0](https://github.com/Appsilon/rhino/releases/tag/v1.11.0) @@ -94,6 +95,8 @@ to allow using `npm` alternatives like `bun` and `pnpm`. This removes the need for `box::reload()` calls in tests. 2. Added support for `shinymanager`. +1. Adds `covr` support for `rhino` apps. + # [rhino 1.3.0](https://github.com/Appsilon/rhino/releases/tag/v1.3.0) 1. Rhino now works with `shinytest2` out of the box. diff --git a/R/tools.R b/R/tools.R index 5cb815cc1..d09dd0b5a 100644 --- a/R/tools.R +++ b/R/tools.R @@ -547,6 +547,82 @@ test_e2e <- function(interactive = FALSE) { } } +#' Run a unit test coverage check +#' +#' Uses the `{covr}` package to produce unit test coverage reports. +#' Uses the `{testhat}` package to run all unit tests in `tests/testthat` directory. +#' +#' @param source_files Character vector of source files with function definitions to measure +#' coverage. Defaults to all `.R` files in the `app` tree. +#' @param test_files Character vector of test files with code to test the functions. Defaults to +#' all test files in `tests/testthat` with the `test-.R` filename pattern. +#' @param line_exclusions passed to `covr::file_coverage` +#' @param function_exclusions passed to `covr::file_coverage` +#' @return A `covr` coverage dataset. +#' +#' @examples +#' if (interactive()) { +#' # Run a test coverage check for the entire rhino app +#' # using all tests in the `tests/testthat` directory. +#' covr_r() +#' } +#' +#' @export +covr_r <- function( + source_files = list.files("app", + pattern = "\\.[rR]$", + full.names = TRUE, + recursive = TRUE), + test_files = list.files("tests/testthat", + pattern = "^test-.*\\.R", + full.names = TRUE, + recursive = TRUE), + line_exclusions = NULL, + function_exclusions = NULL) { + + withr::with_file("box_loader.R", { + module_list <- sub( + "__init__", + "`__init__`", + paste0(tools::file_path_sans_ext(source_files), ",") + ) + + loader_lines <- c("box::use(", module_list, ")") + + writeLines(loader_lines, "box_loader.R") + + coverage <- covr::file_coverage( + source_files = "box_loader.R", + test_files = test_files, + line_exclusions = line_exclusions, + function_exclusions = function_exclusions + ) + }) + + coverage +} + +#' Display rhino test coverage results using a standalone report +#' +#' Uses the `{covr}` package to produce unit test coverage reports. +#' Uses the `{testhat}` package to run all unit tests in `tests/testthat` directory. +#' +#' @param rhino_coverage a rhino coverage dataset, defaults to `covr_r()`. +#' @param ... additional arguments to pass to +#' [`covr::report()`](https://covr.r-lib.org/reference/report.html) +#' @return None. This function is called for side effects. +#' +#' @examples +#' if (interactive()) { +#' # Run a test coverage report on a rhino app +#' covr_report() +#' } +#' +#' @export +covr_report <- function(rhino_coverage = covr_r(), ...) { + covr::report(x = rhino_coverage, ...) +} + #' Development mode #' #' Run application in development mode with automatic rebuilding and reloading. diff --git a/man/covr_r.Rd b/man/covr_r.Rd new file mode 100644 index 000000000..50deecefe --- /dev/null +++ b/man/covr_r.Rd @@ -0,0 +1,41 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tools.R +\name{covr_r} +\alias{covr_r} +\title{Run a unit test coverage check} +\usage{ +covr_r( + source_files = list.files("app", pattern = "\\\\.[rR]$", full.names = TRUE, recursive = + TRUE), + test_files = list.files("tests/testthat", pattern = "^test-.*\\\\.R", full.names = + TRUE, recursive = TRUE), + line_exclusions = NULL, + function_exclusions = NULL +) +} +\arguments{ +\item{source_files}{Character vector of source files with function definitions to measure +coverage. Defaults to all \code{.R} files in the \code{app} tree.} + +\item{test_files}{Character vector of test files with code to test the functions. Defaults to +all test files in \code{tests/testthat} with the \verb{test-.R} filename pattern.} + +\item{line_exclusions}{passed to \code{covr::file_coverage}} + +\item{function_exclusions}{passed to \code{covr::file_coverage}} +} +\value{ +A \code{covr} coverage dataset. +} +\description{ +Uses the \code{{covr}} package to produce unit test coverage reports. +Uses the \code{{testhat}} package to run all unit tests in \code{tests/testthat} directory. +} +\examples{ +if (interactive()) { + # Run a test coverage check for the entire rhino app + # using all tests in the `tests/testthat` directory. + covr_r() +} + +} diff --git a/man/covr_report.Rd b/man/covr_report.Rd new file mode 100644 index 000000000..d813c2188 --- /dev/null +++ b/man/covr_report.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tools.R +\name{covr_report} +\alias{covr_report} +\title{Display rhino test coverage results using a standalone report} +\usage{ +covr_report(rhino_coverage = covr_r(), ...) +} +\arguments{ +\item{rhino_coverage}{a rhino coverage dataset, defaults to \code{covr_r()}.} + +\item{...}{additional arguments to pass to +\href{https://covr.r-lib.org/reference/report.html}{\code{covr::report()}}} +} +\value{ +None. This function is called for side effects. +} +\description{ +Uses the \code{{covr}} package to produce unit test coverage reports. +Uses the \code{{testhat}} package to run all unit tests in \code{tests/testthat} directory. +} +\examples{ +if (interactive()) { + # Run a test coverage report on a rhino app + covr_report() +} + +} diff --git a/tests/e2e/app-files/main.R b/tests/e2e/app-files/main.R index ce3bd35d8..2572f70cb 100644 --- a/tests/e2e/app-files/main.R +++ b/tests/e2e/app-files/main.R @@ -1,3 +1,4 @@ +# nolint start box::use( rhino[log, react_component], shiny, @@ -23,3 +24,4 @@ server <- function(id) { hello$server("hello") }) } +# nolint end \ No newline at end of file diff --git a/tests/e2e/test-covr_r.R b/tests/e2e/test-covr_r.R new file mode 100644 index 000000000..66d04108a --- /dev/null +++ b/tests/e2e/test-covr_r.R @@ -0,0 +1,3 @@ + +expect_no_error(rhino::covr_r()) +expect_no_error(rhino::covr_report()) diff --git a/tests/testthat/helpers/hello.R b/tests/testthat/helpers/hello.R new file mode 100644 index 000000000..b13875a8c --- /dev/null +++ b/tests/testthat/helpers/hello.R @@ -0,0 +1,4 @@ +#' @export +hello <- function() { + "Check out Rhino docs!" +} diff --git a/tests/testthat/helpers/main.R b/tests/testthat/helpers/main.R new file mode 100644 index 000000000..d0dd3e629 --- /dev/null +++ b/tests/testthat/helpers/main.R @@ -0,0 +1,34 @@ +# nolint start +box::use( + shiny[NS, bootstrapPage, div, moduleServer, renderUI, tags, uiOutput], +) + +box::use( + app/logic/hello[hello], +) + +#' @export +ui <- function(id) { + ns <- NS(id) + bootstrapPage( + uiOutput(ns("message")) + ) +} + +#' @export +server <- function(id) { + moduleServer(id, function(input, output, session) { + output$message <- renderUI({ + div( + style = "display: flex; justify-content: center; align-items: center; height: 100vh;", + tags$h1( + tags$a( + hello(), + href = "https://appsilon.github.io/rhino/" + ) + ) + ) + }) + }) +} +# nolint end diff --git a/tests/testthat/helpers/test-hello.R b/tests/testthat/helpers/test-hello.R new file mode 100644 index 000000000..f022913cd --- /dev/null +++ b/tests/testthat/helpers/test-hello.R @@ -0,0 +1,8 @@ +box::use(testthat[describe, expect_identical, it], ) +box::use(app/logic/hello[hello], ) + +describe("hello()", { + it("should return the welcome message", { + expect_identical(hello(), "Check out Rhino docs!") + }) +}) diff --git a/tests/testthat/test-tools.R b/tests/testthat/test-tools.R index c0d1d6c44..ead17ccff 100644 --- a/tests/testthat/test-tools.R +++ b/tests/testthat/test-tools.R @@ -28,3 +28,35 @@ test_that("build_sass_r builds a minified CSS file out of a Sass file", { ".components-container{display:inline-grid;grid-template-columns:1fr 1fr;width:100%}.components-container .component-box{padding:10px;margin:10px}" # nolint: line_length_linter ) }) + +test_that("covr_r runs a coverage test on a rhino app.", { + skip_on_covr() + wd <- getwd() + + withr::with_tempdir({ + fs::dir_create("app") + fs::dir_create("app", "logic") + fs::dir_create("tests", "testthat") + + fs::file_copy( + fs::path(wd, "helpers", "main.R"), + fs::path("app", "main.R") + ) + + fs::file_copy( + fs::path(wd, "helpers", "hello.R"), + fs::path("app", "logic", "hello.R") + ) + + fs::file_copy( + fs::path(wd, "helpers", "test-hello.R"), + fs::path("tests", "testthat", "test-hello.R") + ) + + box::purge_cache() + configure_box() + + expect_no_error(covr_r()) + expect_no_error(covr_report()) + }) +})