From 3e1f7cb11f053fd37639b407cbb97622e835d5fe Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 18 May 2022 14:10:04 -0300 Subject: [PATCH 001/225] Increasing version and adding Imports --- DESCRIPTION | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index f067e11..fb2a72b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: shiny.performance Title: Compare performance of several versions of a shiny app -Version: 0.1.0 +Version: 0.1.1 Authors@R: c( person(given = "Douglas", family = "Azevedo", email = "douglas@appsilon.com", role = "aut"), @@ -15,7 +15,12 @@ SystemRequirements: yarn 1.22.17 or higher, cypress 9.4.1 or higher, xvfb Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.1.2 +RoxygenNote: 7.2.0 VignetteBuilder: knitr Depends: R (>= 3.1.0) +Imports: + glue, + shinytest2, + testthat + From 777d7b9208e7487c59aabc40ac48d302ff824387 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 18 May 2022 14:11:20 -0300 Subject: [PATCH 002/225] New functions to create a structure based on shinytest2 package and updating function's names to match cypress or shinytest2 --- NAMESPACE | 5 +- R/performance_tests.R | 158 +++++++++++++----- R/utils.R | 30 +++- ...ructure.Rd => create_cypress_structure.Rd} | 6 +- man/create_shinytest2_structure.Rd | 14 ++ man/performance_tests.Rd | 5 +- man/run_cypress_performance_test.Rd | 20 +++ ....Rd => run_shinytest2_performance_test.Rd} | 14 +- 8 files changed, 193 insertions(+), 59 deletions(-) rename man/{create_tests_structure.Rd => create_cypress_structure.Rd} (79%) create mode 100644 man/create_shinytest2_structure.Rd create mode 100644 man/run_cypress_performance_test.Rd rename man/{run_performance_test.Rd => run_shinytest2_performance_test.Rd} (63%) diff --git a/NAMESPACE b/NAMESPACE index aa9a7a4..f22e55f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,8 +1,11 @@ # Generated by roxygen2: do not edit by hand export(performance_tests) -export(run_performance_test) +export(run_cypress_performance_test) +export(run_shinytest2_performance_test) importFrom(git2r,checkout) importFrom(glue,glue) importFrom(jsonlite,write_json) +importFrom(shinytest2,test_app) importFrom(stringr,str_trim) +importFrom(testthat,ListReporter) diff --git a/R/performance_tests.R b/R/performance_tests.R index a4506b4..65228e9 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -2,6 +2,7 @@ #' #' @param commit_list A list of commit hash codes, branches' names or anything else you can use with git checkout [...] #' @param cypress_file The path to the .js file containing cypress tests to be recorded +#' @param shinytest2_dir The directory with tests recorded by shinytest2 #' @param app_dir The path to the application root #' @param port Port to run the app #' @param debug Logical. TRUE to display all the system messages on runtime @@ -9,59 +10,104 @@ #' @importFrom git2r checkout #' #' @export -performance_tests <- function(commit_list, cypress_file, app_dir = getwd(), port = 3333, debug = FALSE) { +performance_tests <- function( + commit_list, + cypress_file = NULL, + shinytest2_dir = NULL, + app_dir = getwd(), + port = 3333, + debug = FALSE +) { + # Test whether we have everything we need + if (is.null(cypress_file) & is.null(shinytest2_dir)) + stop("You must provide a cypress_file or the shinytest2_dir") + + if (!is.null(cypress_file) & !is.null(shinytest2_dir)) { + message("Using the cypress file only") + shinytest2_dir <- NULL + } + + type <- ifelse(!is.null(cypress_file), "cypress", "shinytest2") + # getting the current branch current_branch <- get_commit_hash() - # creating the structure - project_path <- create_tests_structure(app_dir = app_dir, port = port, debug = debug) - - # copy the cypress test file from the current location and store it - cypress_file_cp <- file.path(project_path, "cypress_tests.js") - file.copy(from = cypress_file, to = cypress_file_cp) - - # apply the tests for each branch/commit - perf_list <- tryCatch( - expr = { - lapply( - X = commit_list, - FUN = run_performance_test, - project_path = project_path, - cypress_file = cypress_file_cp, - debug = debug - ) - }, - error = function(e) { - message(e) - }, - finally = { - checkout(branch = current_branch) - message(glue("Switched back to {current_branch}")) - - # Cleaning the temporary directory - unlink( - x = c( - file.path(project_path, "node"), - file.path(project_path, "tests") - ), - recursive = TRUE - ) - } - ) + if (type == "cypress") { + # creating the structure + project_path <- create_cypress_structure(app_dir = app_dir, port = port, debug = debug) + + # copy the cypress test file from the current location and store it + cypress_file_cp <- file.path(project_path, "cypress_tests.js") + file.copy(from = cypress_file, to = cypress_file_cp) + + # apply the tests for each branch/commit + perf_list <- tryCatch( + expr = { + lapply( + X = commit_list, + FUN = run_cypress_performance_test, + project_path = project_path, + cypress_file = cypress_file_cp, + debug = debug + ) + }, + error = function(e) { + message(e) + }, + finally = { + checkout(branch = current_branch) + message(glue("Switched back to {current_branch}")) + + # Cleaning the temporary directory + unlink( + x = c( + file.path(project_path, "node"), + file.path(project_path, "tests") + ), + recursive = TRUE + ) + } + ) + } else { + # creating the structure + project_path <- create_shinytest2_structure(shinytest2_dir = shinytest2_dir) + + # apply the tests for each branch/commit + perf_list <- tryCatch( + expr = { + lapply( + X = commit_list, + FUN = run_shinytest2_performance_test, + app_dir = app_dir, + project_path = project_path, + debug = debug + ) + }, + error = function(e) { + message(e) + }, + finally = { + checkout(branch = current_branch) + message(glue("Switched back to {current_branch}")) + + # Cleaning the temporary directory + unlink(x = file.path(project_path, "tests"), recursive = TRUE) + } + ) + } return(perf_list) } -#' @title Run the performance test based on a single commit +#' @title Run the performance test based on a single commit using Cypress #' #' @param commit A commit hash code or a branch's name #' @param project_path The path to the project with all needed packages installed #' @param cypress_file The path to the .js file conteining cypress tests to be recorded -#' @param txt_file The path to the file where it is aimed to save the times #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @export -run_performance_test <- function(commit, project_path, cypress_file, txt_file, debug) { +run_cypress_performance_test <- function(commit, project_path, cypress_file, debug) { files <- create_cypress_tests(project_path = project_path, cypress_file = cypress_file) js_file <- files$js_file txt_file <- files$txt_file @@ -89,3 +135,37 @@ run_performance_test <- function(commit, project_path, cypress_file, txt_file, d # return times return(perf_file) } + +#' @title Run the performance test based on a single commit using shinytest2 +#' +#' @param commit A commit hash code or a branch's name +#' @param project_path The path to the project with all needed packages installed +#' @param cypress_file The path to the .js file conteining cypress tests to be recorded +#' @param txt_file The path to the file where it is aimed to save the times +#' @param debug Logical. TRUE to display all the system messages on runtime +#' +#' @importFrom testthat ListReporter +#' @importFrom shinytest2 test_app +#' @export +run_shinytest2_performance_test <- function(commit, app_dir, project_path, debug) { + # checkout to the desired commit + checkout(branch = commit) + date <- get_commit_date(branch = commit) + message(glue("Switched to {commit}")) + + # run tests there + my_reporter <- ListReporter$new() + test_app(app_dir = app_dir, reporter = my_reporter, stop_on_failure = FALSE, stop_on_warning = FALSE) + perf_file <- as.data.frame(my_reporter$get_results()) + perf_file <- perf_file[, c("test", "real")] + perf_file$test <- gsub(x = perf_file$test, pattern = "\\{shinytest2\\} recording: ", replacement = "") + + perf_file <- cbind.data.frame(date = date, perf_file) + colnames(perf_file) <- c("date", "test_name", "duration_ms") + + # removing anything new in the github repo + checkout_files() + + # return times + return(perf_file) +} diff --git a/R/utils.R b/R/utils.R index 72d5896..35d9fae 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1,3 +1,18 @@ +#' @title Create a temporary directory to store everything needed by shinytest2 +#' +#' @param shinytest2_dir The path to the shinytest2 tests +create_shinytest2_structure <- function(shinytest2_dir) { + # temp dir to run the tests + dir_tests <- tempdir() + + # copy everything to the temporary directory + system(glue("cp -r {shinytest2_dir} {dir_tests}")) + + # returning the project folder + message(glue("Structure created at {dir_tests}")) + return(dir_tests) +} + #' @title Create a temporary directory to store everything needed by Cypress #' #' @param app_dir The path to the application root @@ -5,16 +20,16 @@ #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @importFrom jsonlite write_json -create_tests_structure <- function(app_dir, port, debug) { +create_cypress_structure <- function(app_dir, port, debug) { # temp dir to run the tests - dir_cypress <- tempdir() + dir_tests <- tempdir() # node path - node_path <- file.path(dir_cypress, "node") + node_path <- file.path(dir_tests, "node") root_path <- file.path(node_path, "root") # test path - tests_path <- file.path(dir_cypress, "tests") + tests_path <- file.path(dir_tests, "tests") cypress_path <- file.path(tests_path, "cypress") integration_path <- file.path(cypress_path, "integration") plugins_path <- file.path(cypress_path, "plugins") @@ -27,7 +42,7 @@ create_tests_structure <- function(app_dir, port, debug) { dir.create(path = plugins_path, showWarnings = FALSE) # create a path root linked to the main directory app - symlink_cmd <- glue("cd {dir_cypress}; ln -s {app_dir} {root_path}") + symlink_cmd <- glue("cd {dir_tests}; ln -s {app_dir} {root_path}") system(symlink_cmd) # create the packages.json file @@ -50,9 +65,8 @@ create_tests_structure <- function(app_dir, port, debug) { write_json(x = json_txt, path = json_file, pretty = TRUE, auto_unbox = TRUE) # returning the project folder - message(glue("Structure created at {dir_cypress}")) - - return(dir_cypress) + message(glue("Structure created at {dir_tests}")) + return(dir_tests) } #' @title Create the list of needed libraries diff --git a/man/create_tests_structure.Rd b/man/create_cypress_structure.Rd similarity index 79% rename from man/create_tests_structure.Rd rename to man/create_cypress_structure.Rd index 8d22fdc..f03c8d5 100644 --- a/man/create_tests_structure.Rd +++ b/man/create_cypress_structure.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R -\name{create_tests_structure} -\alias{create_tests_structure} +\name{create_cypress_structure} +\alias{create_cypress_structure} \title{Create a temporary directory to store everything needed by Cypress} \usage{ -create_tests_structure(app_dir, port, debug) +create_cypress_structure(app_dir, port, debug) } \arguments{ \item{app_dir}{The path to the application root} diff --git a/man/create_shinytest2_structure.Rd b/man/create_shinytest2_structure.Rd new file mode 100644 index 0000000..32024be --- /dev/null +++ b/man/create_shinytest2_structure.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{create_shinytest2_structure} +\alias{create_shinytest2_structure} +\title{Create a temporary directory to store everything needed by shinytest2} +\usage{ +create_shinytest2_structure(shinytest2_dir) +} +\arguments{ +\item{shinytest2_dir}{The path to the shinytest2 tests} +} +\description{ +Create a temporary directory to store everything needed by shinytest2 +} diff --git a/man/performance_tests.Rd b/man/performance_tests.Rd index 586d634..96bc4f8 100644 --- a/man/performance_tests.Rd +++ b/man/performance_tests.Rd @@ -6,7 +6,8 @@ \usage{ performance_tests( commit_list, - cypress_file, + cypress_file = NULL, + shinytest2_dir = NULL, app_dir = getwd(), port = 3333, debug = FALSE @@ -17,6 +18,8 @@ performance_tests( \item{cypress_file}{The path to the .js file containing cypress tests to be recorded} +\item{shinytest2_dir}{The directory with tests recorded by shinytest2} + \item{app_dir}{The path to the application root} \item{port}{Port to run the app} diff --git a/man/run_cypress_performance_test.Rd b/man/run_cypress_performance_test.Rd new file mode 100644 index 0000000..6f04d31 --- /dev/null +++ b/man/run_cypress_performance_test.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/performance_tests.R +\name{run_cypress_performance_test} +\alias{run_cypress_performance_test} +\title{Run the performance test based on a single commit using Cypress} +\usage{ +run_cypress_performance_test(commit, project_path, cypress_file, debug) +} +\arguments{ +\item{commit}{A commit hash code or a branch's name} + +\item{project_path}{The path to the project with all needed packages installed} + +\item{cypress_file}{The path to the .js file conteining cypress tests to be recorded} + +\item{debug}{Logical. TRUE to display all the system messages on runtime} +} +\description{ +Run the performance test based on a single commit using Cypress +} diff --git a/man/run_performance_test.Rd b/man/run_shinytest2_performance_test.Rd similarity index 63% rename from man/run_performance_test.Rd rename to man/run_shinytest2_performance_test.Rd index 2271ac9..cbafc7b 100644 --- a/man/run_performance_test.Rd +++ b/man/run_shinytest2_performance_test.Rd @@ -1,22 +1,22 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/performance_tests.R -\name{run_performance_test} -\alias{run_performance_test} -\title{Run the performance test based on a single commit} +\name{run_shinytest2_performance_test} +\alias{run_shinytest2_performance_test} +\title{Run the performance test based on a single commit using shinytest2} \usage{ -run_performance_test(commit, project_path, cypress_file, txt_file, debug) +run_shinytest2_performance_test(commit, app_dir, project_path, debug) } \arguments{ \item{commit}{A commit hash code or a branch's name} \item{project_path}{The path to the project with all needed packages installed} +\item{debug}{Logical. TRUE to display all the system messages on runtime} + \item{cypress_file}{The path to the .js file conteining cypress tests to be recorded} \item{txt_file}{The path to the file where it is aimed to save the times} - -\item{debug}{Logical. TRUE to display all the system messages on runtime} } \description{ -Run the performance test based on a single commit +Run the performance test based on a single commit using shinytest2 } From c6133255ba474f23cd0052b16123ebf2dff90ed1 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 18 May 2022 14:11:34 -0300 Subject: [PATCH 003/225] Minimal example for shinytest2 --- NEWS.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 NEWS.md diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..d253fef --- /dev/null +++ b/NEWS.md @@ -0,0 +1,3 @@ +# 0.1.1 + +Adding a minimal example using `shinytest2` From 0c05aec2f70c3827c6b60a11299853294fef71d1 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Tue, 18 Oct 2022 13:36:23 +0200 Subject: [PATCH 004/225] Adding missing parameter's documentation~ --- NAMESPACE | 1 + R/performance_tests.R | 1 + R/utils.R | 2 ++ man/create_cypress_list.Rd | 2 ++ man/create_node_list.Rd | 2 ++ 5 files changed, 8 insertions(+) diff --git a/NAMESPACE b/NAMESPACE index aa9a7a4..caab004 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -6,3 +6,4 @@ importFrom(git2r,checkout) importFrom(glue,glue) importFrom(jsonlite,write_json) importFrom(stringr,str_trim) +importFrom(utils,read.table) diff --git a/R/performance_tests.R b/R/performance_tests.R index a4506b4..26b2c93 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -60,6 +60,7 @@ performance_tests <- function(commit_list, cypress_file, app_dir = getwd(), port #' @param txt_file The path to the file where it is aimed to save the times #' @param debug Logical. TRUE to display all the system messages on runtime #' +#' @importFrom utils read.table #' @export run_performance_test <- function(commit, project_path, cypress_file, txt_file, debug) { files <- create_cypress_tests(project_path = project_path, cypress_file = cypress_file) diff --git a/R/utils.R b/R/utils.R index 72d5896..9193413 100644 --- a/R/utils.R +++ b/R/utils.R @@ -58,6 +58,7 @@ create_tests_structure <- function(app_dir, port, debug) { #' @title Create the list of needed libraries #' #' @param tests_path The path to project +#' @param port Port to run the app create_node_list <- function(tests_path, port) { json_list <- list( private = TRUE, @@ -78,6 +79,7 @@ create_node_list <- function(tests_path, port) { #' @title Create the cypress configuration list #' #' @param plugins_file The path to the Cypress plugins +#' @param port Port to run the app create_cypress_list <- function(plugins_file, port) { json_list <- list( baseUrl = glue("http://localhost:{port}"), diff --git a/man/create_cypress_list.Rd b/man/create_cypress_list.Rd index 104eb3a..c1b9239 100644 --- a/man/create_cypress_list.Rd +++ b/man/create_cypress_list.Rd @@ -8,6 +8,8 @@ create_cypress_list(plugins_file, port) } \arguments{ \item{plugins_file}{The path to the Cypress plugins} + +\item{port}{Port to run the app} } \description{ Create the cypress configuration list diff --git a/man/create_node_list.Rd b/man/create_node_list.Rd index 249eaaf..1f53065 100644 --- a/man/create_node_list.Rd +++ b/man/create_node_list.Rd @@ -8,6 +8,8 @@ create_node_list(tests_path, port) } \arguments{ \item{tests_path}{The path to project} + +\item{port}{Port to run the app} } \description{ Create the list of needed libraries From 6c0de3b0cb0915312612f147d4ee3a6d3c4f2227 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Tue, 18 Oct 2022 13:37:07 +0200 Subject: [PATCH 005/225] Updating Description, license, imports and suggests fields to fix the error messages --- DESCRIPTION | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index f067e11..51e8e4b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -8,14 +8,21 @@ Authors@R: person("Developers", "Appsilon", email = "support+opensource@appsilon.com", role = "cre"), person(family = "Appsilon Sp. z o.o.", role = "cph") ) -Description: Compare performance of several versions of a shiny app based on commit hashs -License: LGPL-3 + file LICENSE +Description: Compare performance of several versions of a shiny app based on commit hashs. +License: LGPL-3 URL: https://github.com/Appsilon/shiny.performance SystemRequirements: yarn 1.22.17 or higher, cypress 9.4.1 or higher, xvfb Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.1.2 +RoxygenNote: 7.2.0 VignetteBuilder: knitr Depends: R (>= 3.1.0) +Imports: + git2r, + glue, + jsonlite, + stringr +Suggests: + knitr From 37d8e8cc521f014fdf594af466e5ac32190ce6bf Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Tue, 18 Oct 2022 14:12:27 +0200 Subject: [PATCH 006/225] Add basic CI. --- .github/workflows/main.yml | 45 ++++++++++++++++++++++++++++++++++++++ DESCRIPTION | 4 ++++ 2 files changed, 49 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..2a4a55a --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,45 @@ +on: push + +name: R-CMD-check + +jobs: + main: + name: ${{ matrix.config.os }} (${{ matrix.config.r }}) + + runs-on: ${{ matrix.config.os }} + + timeout-minutes: 30 + + strategy: + fail-fast: false + matrix: + config: + - {os: ubuntu-22.04, r: 'release'} + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install R + uses: r-lib/actions/setup-r@v2 + with: + r-version: ${{ matrix.config.r }} + + - name: Install R package dependencies + uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: local::. # Necessary to avoid object usage linter errors. + + - name: R CMD check + if: always() + uses: r-lib/actions/check-r-package@v2 + with: + error-on: '"note"' + + - name: Lint + if: always() + shell: Rscript {0} + run: | + lints <- lintr::lint_package() + for (lint in lints) print(lint) + quit(status = length(lints) > 0) diff --git a/DESCRIPTION b/DESCRIPTION index f067e11..08a1dc4 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,5 +17,9 @@ LazyData: true Roxygen: list(markdown = TRUE) RoxygenNote: 7.1.2 VignetteBuilder: knitr +Suggests: + lintr, + rcmdcheck, + testthat Depends: R (>= 3.1.0) From 2f718d1bc1ee28772044e95cb42994ceec1359fe Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Tue, 18 Oct 2022 14:24:37 +0200 Subject: [PATCH 007/225] Add linter configuration. --- .lintr | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .lintr diff --git a/.lintr b/.lintr new file mode 100644 index 0000000..51a8fad --- /dev/null +++ b/.lintr @@ -0,0 +1,8 @@ +linters: + linters_with_defaults( + line_length_linter = line_length_linter(100) + ) +exclusions: + c( + "vignettes" + ) From 7877964eec6f10dad20764673d96fbc13c27d84a Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Tue, 18 Oct 2022 14:41:00 +0200 Subject: [PATCH 008/225] Fixing merge issues --- DESCRIPTION | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index c8f0617..8068c7f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -18,9 +18,9 @@ Roxygen: list(markdown = TRUE) RoxygenNote: 7.2.0 VignetteBuilder: knitr Suggests: + knitr, lintr, - rcmdcheck, - testthat + rcmdcheck Depends: R (>= 3.1.0) Imports: @@ -30,5 +30,3 @@ Imports: shinytest2, stringr, testthat -Suggests: - knitr From 2a524658a06354644373113758ffd85c0c47c7c7 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Tue, 18 Oct 2022 14:41:19 +0200 Subject: [PATCH 009/225] ignoring .github and .lintr during package checks --- .Rbuildignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.Rbuildignore b/.Rbuildignore index 91114bf..78dd63f 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -1,2 +1,4 @@ ^.*\.Rproj$ ^\.Rproj\.user$ +.github +.lintr From e81f83d9119049b691e341a383f2894ade945091 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Tue, 18 Oct 2022 15:21:22 +0200 Subject: [PATCH 010/225] Fixing lint issues to make CI to run smoothly --- NAMESPACE | 4 +- R/performance_tests.R | 61 +++++++++++++------ R/utils.R | 33 +++++++--- man/create_cypress_tests.Rd | 6 +- man/performance_tests.Rd | 6 +- ...rformance_test.Rd => run_cypress_ptest.Rd} | 12 ++-- ...rmance_test.Rd => run_shinytest2_ptest.Rd} | 12 ++-- 7 files changed, 94 insertions(+), 40 deletions(-) rename man/{run_cypress_performance_test.Rd => run_cypress_ptest.Rd} (74%) rename man/{run_shinytest2_performance_test.Rd => run_shinytest2_ptest.Rd} (76%) diff --git a/NAMESPACE b/NAMESPACE index f22e55f..eb921b0 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,8 +1,8 @@ # Generated by roxygen2: do not edit by hand export(performance_tests) -export(run_cypress_performance_test) -export(run_shinytest2_performance_test) +export(run_cypress_ptest) +export(run_shinytest2_ptest) importFrom(git2r,checkout) importFrom(glue,glue) importFrom(jsonlite,write_json) diff --git a/R/performance_tests.R b/R/performance_tests.R index 65228e9..a9b2fb5 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -1,7 +1,9 @@ #' @title Execute performance tests for a list of commits #' -#' @param commit_list A list of commit hash codes, branches' names or anything else you can use with git checkout [...] -#' @param cypress_file The path to the .js file containing cypress tests to be recorded +#' @param commit_list A list of commit hash codes, branches' names or anything +#' else you can use with git checkout [...] +#' @param cypress_file The path to the .js file containing cypress tests to +#' be recorded #' @param shinytest2_dir The directory with tests recorded by shinytest2 #' @param app_dir The path to the application root #' @param port Port to run the app @@ -19,10 +21,10 @@ performance_tests <- function( debug = FALSE ) { # Test whether we have everything we need - if (is.null(cypress_file) & is.null(shinytest2_dir)) + if (is.null(cypress_file) && is.null(shinytest2_dir)) stop("You must provide a cypress_file or the shinytest2_dir") - if (!is.null(cypress_file) & !is.null(shinytest2_dir)) { + if (!is.null(cypress_file) && !is.null(shinytest2_dir)) { message("Using the cypress file only") shinytest2_dir <- NULL } @@ -34,7 +36,11 @@ performance_tests <- function( if (type == "cypress") { # creating the structure - project_path <- create_cypress_structure(app_dir = app_dir, port = port, debug = debug) + project_path <- create_cypress_structure( + app_dir = app_dir, + port = port, + debug = debug + ) # copy the cypress test file from the current location and store it cypress_file_cp <- file.path(project_path, "cypress_tests.js") @@ -45,7 +51,7 @@ performance_tests <- function( expr = { lapply( X = commit_list, - FUN = run_cypress_performance_test, + FUN = run_cypress_ptest, project_path = project_path, cypress_file = cypress_file_cp, debug = debug @@ -77,7 +83,7 @@ performance_tests <- function( expr = { lapply( X = commit_list, - FUN = run_shinytest2_performance_test, + FUN = run_shinytest2_ptest, app_dir = app_dir, project_path = project_path, debug = debug @@ -102,13 +108,19 @@ performance_tests <- function( #' @title Run the performance test based on a single commit using Cypress #' #' @param commit A commit hash code or a branch's name -#' @param project_path The path to the project with all needed packages installed -#' @param cypress_file The path to the .js file conteining cypress tests to be recorded +#' @param project_path The path to the project with all needed packages +#' installed +#' @param cypress_file The path to the .js file conteining cypress tests to +#' be recorded #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @export -run_cypress_performance_test <- function(commit, project_path, cypress_file, debug) { - files <- create_cypress_tests(project_path = project_path, cypress_file = cypress_file) +run_cypress_ptest <- function(commit, project_path, cypress_file, debug) { + files <- create_cypress_tests( + project_path = project_path, + cypress_file = cypress_file + ) + js_file <- files$js_file txt_file <- files$txt_file @@ -118,7 +130,10 @@ run_cypress_performance_test <- function(commit, project_path, cypress_file, deb message(glue("Switched to {commit}")) # run tests there - command <- glue("cd {project_path}; set -eu; exec yarn --cwd node performance-test") + command <- glue( + "cd {project_path}; ", + "set -eu; exec yarn --cwd node performance-test" + ) system(command, ignore.stdout = !debug, ignore.stderr = !debug) # read the file saved by cypress @@ -139,15 +154,17 @@ run_cypress_performance_test <- function(commit, project_path, cypress_file, deb #' @title Run the performance test based on a single commit using shinytest2 #' #' @param commit A commit hash code or a branch's name -#' @param project_path The path to the project with all needed packages installed -#' @param cypress_file The path to the .js file conteining cypress tests to be recorded +#' @param project_path The path to the project with all needed packages +#' installed +#' @param cypress_file The path to the .js file conteining cypress tests to +#' be recorded #' @param txt_file The path to the file where it is aimed to save the times #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @importFrom testthat ListReporter #' @importFrom shinytest2 test_app #' @export -run_shinytest2_performance_test <- function(commit, app_dir, project_path, debug) { +run_shinytest2_ptest <- function(commit, app_dir, project_path, debug) { # checkout to the desired commit checkout(branch = commit) date <- get_commit_date(branch = commit) @@ -155,10 +172,20 @@ run_shinytest2_performance_test <- function(commit, app_dir, project_path, debug # run tests there my_reporter <- ListReporter$new() - test_app(app_dir = app_dir, reporter = my_reporter, stop_on_failure = FALSE, stop_on_warning = FALSE) + test_app( + app_dir = app_dir, + reporter = my_reporter, + stop_on_failure = FALSE, + stop_on_warning = FALSE + ) + perf_file <- as.data.frame(my_reporter$get_results()) perf_file <- perf_file[, c("test", "real")] - perf_file$test <- gsub(x = perf_file$test, pattern = "\\{shinytest2\\} recording: ", replacement = "") + perf_file$test <- gsub( + x = perf_file$test, + pattern = "\\{shinytest2\\} recording: ", + replacement = "" + ) perf_file <- cbind.data.frame(date = date, perf_file) colnames(perf_file) <- c("date", "test_name", "duration_ms") diff --git a/R/utils.R b/R/utils.R index 35d9fae..3cb9250 100644 --- a/R/utils.R +++ b/R/utils.R @@ -26,7 +26,7 @@ create_cypress_structure <- function(app_dir, port, debug) { # node path node_path <- file.path(dir_tests, "node") - root_path <- file.path(node_path, "root") + root_path <- file.path(node_path, "root") # nolint # test path tests_path <- file.path(dir_tests, "tests") @@ -76,7 +76,9 @@ create_node_list <- function(tests_path, port) { json_list <- list( private = TRUE, scripts = list( - "performance-test" = glue("start-server-and-test run-app http://localhost:{port} run-cypress"), + "performance-test" = glue( + "start-server-and-test run-app http://localhost:{port} run-cypress" + ), "run-app" = glue("cd root && Rscript -e 'shiny::runApp(port = {port})'"), "run-cypress" = glue("cypress run --project {tests_path}") ), @@ -109,7 +111,11 @@ create_cypress_plugins <- function() { module.exports = (on, config) => { on('task', { performanceTimes (attributes) { - fs.writeFile(attributes.fileOut, `${ attributes.title }; ${ attributes.duration }\n`, { flag: 'a' }) + fs.writeFile( + attributes.fileOut, + `${ attributes.title }; ${ attributes.duration }\n`, + { flag: 'a' } + ) return null } }) @@ -120,11 +126,20 @@ create_cypress_plugins <- function() { #' @title Create the cypress files under project directory #' -#' @param project_path The path to the project with all needed packages installed -#' @param cypress_file The path to the .js file conteining cypress tests to be recorded +#' @param project_path The path to the project with all needed packages +#' installed +#' @param cypress_file The path to the .js file conteining cypress tests to +#' be recorded create_cypress_tests <- function(project_path, cypress_file) { # creating a copy to be able to edit the js file - js_file <- file.path(project_path, "tests", "cypress", "integration", "app.spec.js") + js_file <- file.path( + project_path, + "tests", + "cypress", + "integration", + "app.spec.js" + ) + file.copy(from = cypress_file, to = js_file, overwrite = TRUE) # file to store the times @@ -200,7 +215,11 @@ get_commit_hash <- function() { ) branch <- str_trim( - string = gsub(x = branch[length(branch)], pattern = "\\*\\s", replacement = ""), + string = gsub( + x = branch[length(branch)], + pattern = "\\*\\s", + replacement = "" + ), side = "both" ) diff --git a/man/create_cypress_tests.Rd b/man/create_cypress_tests.Rd index 789df5e..6d52228 100644 --- a/man/create_cypress_tests.Rd +++ b/man/create_cypress_tests.Rd @@ -7,9 +7,11 @@ create_cypress_tests(project_path, cypress_file) } \arguments{ -\item{project_path}{The path to the project with all needed packages installed} +\item{project_path}{The path to the project with all needed packages +installed} -\item{cypress_file}{The path to the .js file conteining cypress tests to be recorded} +\item{cypress_file}{The path to the .js file conteining cypress tests to +be recorded} } \description{ Create the cypress files under project directory diff --git a/man/performance_tests.Rd b/man/performance_tests.Rd index 96bc4f8..a4a26e8 100644 --- a/man/performance_tests.Rd +++ b/man/performance_tests.Rd @@ -14,9 +14,11 @@ performance_tests( ) } \arguments{ -\item{commit_list}{A list of commit hash codes, branches' names or anything else you can use with git checkout \link{...}} +\item{commit_list}{A list of commit hash codes, branches' names or anything +else you can use with git checkout \link{...}} -\item{cypress_file}{The path to the .js file containing cypress tests to be recorded} +\item{cypress_file}{The path to the .js file containing cypress tests to +be recorded} \item{shinytest2_dir}{The directory with tests recorded by shinytest2} diff --git a/man/run_cypress_performance_test.Rd b/man/run_cypress_ptest.Rd similarity index 74% rename from man/run_cypress_performance_test.Rd rename to man/run_cypress_ptest.Rd index 6f04d31..4aace05 100644 --- a/man/run_cypress_performance_test.Rd +++ b/man/run_cypress_ptest.Rd @@ -1,17 +1,19 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/performance_tests.R -\name{run_cypress_performance_test} -\alias{run_cypress_performance_test} +\name{run_cypress_ptest} +\alias{run_cypress_ptest} \title{Run the performance test based on a single commit using Cypress} \usage{ -run_cypress_performance_test(commit, project_path, cypress_file, debug) +run_cypress_ptest(commit, project_path, cypress_file, debug) } \arguments{ \item{commit}{A commit hash code or a branch's name} -\item{project_path}{The path to the project with all needed packages installed} +\item{project_path}{The path to the project with all needed packages +installed} -\item{cypress_file}{The path to the .js file conteining cypress tests to be recorded} +\item{cypress_file}{The path to the .js file conteining cypress tests to +be recorded} \item{debug}{Logical. TRUE to display all the system messages on runtime} } diff --git a/man/run_shinytest2_performance_test.Rd b/man/run_shinytest2_ptest.Rd similarity index 76% rename from man/run_shinytest2_performance_test.Rd rename to man/run_shinytest2_ptest.Rd index cbafc7b..0ebc552 100644 --- a/man/run_shinytest2_performance_test.Rd +++ b/man/run_shinytest2_ptest.Rd @@ -1,19 +1,21 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/performance_tests.R -\name{run_shinytest2_performance_test} -\alias{run_shinytest2_performance_test} +\name{run_shinytest2_ptest} +\alias{run_shinytest2_ptest} \title{Run the performance test based on a single commit using shinytest2} \usage{ -run_shinytest2_performance_test(commit, app_dir, project_path, debug) +run_shinytest2_ptest(commit, app_dir, project_path, debug) } \arguments{ \item{commit}{A commit hash code or a branch's name} -\item{project_path}{The path to the project with all needed packages installed} +\item{project_path}{The path to the project with all needed packages +installed} \item{debug}{Logical. TRUE to display all the system messages on runtime} -\item{cypress_file}{The path to the .js file conteining cypress tests to be recorded} +\item{cypress_file}{The path to the .js file conteining cypress tests to +be recorded} \item{txt_file}{The path to the file where it is aimed to save the times} } From 1c6293f1601ebcde44a240aaf658f31fa3acaa4b Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Tue, 18 Oct 2022 15:43:46 +0200 Subject: [PATCH 011/225] Addint PR template to our project --- .github/PULL_REQUEST_TEMPLATE.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..c2e55fe --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ +### Link to issue + +... + +### Definition of Done + +... + +### How to test changes + +... + + +### Tasks for PR author + +- [ ] Test your change and ensure there is no regression +- [ ] Change has a corresponding issue. ***Ensure it is linked in GitHub*** +- [ ] Author of the change opened a pull request and assigned a reviewer + +### General policy: + +- If applicable - add instructions for testing +- If there’s no issue, create it. Each issue needs to be well defined and described. +- All interaction with a user, user-facing messages, plots, reports etc. are written from the perspective of the person using or receiving it. They are understandable and helpful to this person. If a user sees an error message, there is a call to action, i.e. the user knows what to do to fix it. +- README, other documentation and code comments that we have is updated with all information related to the change. +- All code has been peer-reviewed before merging into any main branch +- All changes have been merged into the main branch we use for development. +- Continuous integration checks (linter, unit tests, integration tests) are configured and pass. +- Optional: unit tests added for all new or changed logic. +- All task requirements satisfied. If not describe it here. The reviewer is responsible to verify each aspect of the task. +- Change covers only things in task. Please create new PR if you want to fix something else. From 8a5ce4bd92338bb81ff35c6b292ba2bf41559231 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Tue, 18 Oct 2022 15:45:11 +0200 Subject: [PATCH 012/225] small fix --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c2e55fe..16a9edf 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,4 @@ -### Link to issue +### Link to the Issue ... From 483cd5844fd6ba89ff6627c25174d5e3ce39438c Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Tue, 18 Oct 2022 16:08:47 +0200 Subject: [PATCH 013/225] Removing git2r as dependency and creating a simple checkout function --- NAMESPACE | 1 - R/performance_tests.R | 2 -- R/utils.R | 9 +++++++++ man/checkout.Rd | 11 +++++++++++ 4 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 man/checkout.Rd diff --git a/NAMESPACE b/NAMESPACE index f22e55f..6f44810 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,7 +3,6 @@ export(performance_tests) export(run_cypress_performance_test) export(run_shinytest2_performance_test) -importFrom(git2r,checkout) importFrom(glue,glue) importFrom(jsonlite,write_json) importFrom(shinytest2,test_app) diff --git a/R/performance_tests.R b/R/performance_tests.R index 65228e9..e798c9d 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -7,8 +7,6 @@ #' @param port Port to run the app #' @param debug Logical. TRUE to display all the system messages on runtime #' -#' @importFrom git2r checkout -#' #' @export performance_tests <- function( commit_list, diff --git a/R/utils.R b/R/utils.R index 35d9fae..596023e 100644 --- a/R/utils.R +++ b/R/utils.R @@ -223,3 +223,12 @@ get_commit_hash <- function() { checkout_files <- function() { system("git checkout .") } + +#' @title Checkout GitHub branch +#' +#' @description checkout and go to a different branch +checkout <- function(branch) { + system( + glue("git checkout {branch}") + ) +} diff --git a/man/checkout.Rd b/man/checkout.Rd new file mode 100644 index 0000000..f8e5a5e --- /dev/null +++ b/man/checkout.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{checkout} +\alias{checkout} +\title{Checkout GitHub branch} +\usage{ +checkout(branch) +} +\description{ +checkout and go to a different branch +} From 9ad530a8b2cb81f0ea8e12078a5a107d1ff84f22 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Tue, 18 Oct 2022 22:00:38 +0200 Subject: [PATCH 014/225] Adding renv in Imports --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index fb2a72b..5ce48fe 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -21,6 +21,6 @@ Depends: R (>= 3.1.0) Imports: glue, + renv, shinytest2, testthat - From b1e3365c8116db55c44a6d8e6c47558cd92f75e4 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Tue, 18 Oct 2022 22:01:30 +0200 Subject: [PATCH 015/225] Adding use_renv parameter to allow the user to restore packages in different app's version~ --- R/performance_tests.R | 27 ++++++++++++++++++++++++-- man/run_shinytest2_performance_test.Rd | 6 +++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/R/performance_tests.R b/R/performance_tests.R index 65228e9..87965b0 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -5,6 +5,9 @@ #' @param shinytest2_dir The directory with tests recorded by shinytest2 #' @param app_dir The path to the application root #' @param port Port to run the app +#' @param use_renv In case it is set as TRUE, package will try to apply +#' renv::restore() in all branches. Otherwise, the current loaded list of +#' packages will be used in all branches. #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @importFrom git2r checkout @@ -16,6 +19,7 @@ performance_tests <- function( shinytest2_dir = NULL, app_dir = getwd(), port = 3333, + use_renv = TRUE, debug = FALSE ) { # Test whether we have everything we need @@ -48,6 +52,7 @@ performance_tests <- function( FUN = run_cypress_performance_test, project_path = project_path, cypress_file = cypress_file_cp, + use_renv = use_renv, debug = debug ) }, @@ -55,8 +60,10 @@ performance_tests <- function( message(e) }, finally = { + # Restore initital setup checkout(branch = current_branch) message(glue("Switched back to {current_branch}")) + if (use_renv) restore_env(branch = current_branch) # Cleaning the temporary directory unlink( @@ -80,6 +87,7 @@ performance_tests <- function( FUN = run_shinytest2_performance_test, app_dir = app_dir, project_path = project_path, + use_renv = use_renv, debug = debug ) }, @@ -87,8 +95,10 @@ performance_tests <- function( message(e) }, finally = { + # Restore initital setup checkout(branch = current_branch) message(glue("Switched back to {current_branch}")) + if (use_renv) restore_env(branch = current_branch) # Cleaning the temporary directory unlink(x = file.path(project_path, "tests"), recursive = TRUE) @@ -104,10 +114,14 @@ performance_tests <- function( #' @param commit A commit hash code or a branch's name #' @param project_path The path to the project with all needed packages installed #' @param cypress_file The path to the .js file conteining cypress tests to be recorded +#' @param use_renv In case it is set as TRUE, package will try to apply +#' renv::restore() in all branches. Otherwise, the current loaded list of +#' packages will be used in all branches. #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @export -run_cypress_performance_test <- function(commit, project_path, cypress_file, debug) { +run_cypress_performance_test <- function(commit, project_path, cypress_file, use_renv, debug) { + # create cypress test structure files <- create_cypress_tests(project_path = project_path, cypress_file = cypress_file) js_file <- files$js_file txt_file <- files$txt_file @@ -117,6 +131,9 @@ run_cypress_performance_test <- function(commit, project_path, cypress_file, deb date <- get_commit_date(branch = commit) message(glue("Switched to {commit}")) + # check if we are able to restore packages using renv + if (use_renv) restore_env(branch = commit) + # run tests there command <- glue("cd {project_path}; set -eu; exec yarn --cwd node performance-test") system(command, ignore.stdout = !debug, ignore.stderr = !debug) @@ -142,17 +159,23 @@ run_cypress_performance_test <- function(commit, project_path, cypress_file, deb #' @param project_path The path to the project with all needed packages installed #' @param cypress_file The path to the .js file conteining cypress tests to be recorded #' @param txt_file The path to the file where it is aimed to save the times +#' @param use_renv In case it is set as TRUE, package will try to apply +#' renv::restore() in all branches. Otherwise, the current loaded list of +#' packages will be used in all branches. #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @importFrom testthat ListReporter #' @importFrom shinytest2 test_app #' @export -run_shinytest2_performance_test <- function(commit, app_dir, project_path, debug) { +run_shinytest2_performance_test <- function(commit, app_dir, project_path, use_renv, debug) { # checkout to the desired commit checkout(branch = commit) date <- get_commit_date(branch = commit) message(glue("Switched to {commit}")) + # check if we are able to restore packages using renv + if (use_renv) restore_env(branch = commit) + # run tests there my_reporter <- ListReporter$new() test_app(app_dir = app_dir, reporter = my_reporter, stop_on_failure = FALSE, stop_on_warning = FALSE) diff --git a/man/run_shinytest2_performance_test.Rd b/man/run_shinytest2_performance_test.Rd index cbafc7b..42667f6 100644 --- a/man/run_shinytest2_performance_test.Rd +++ b/man/run_shinytest2_performance_test.Rd @@ -4,13 +4,17 @@ \alias{run_shinytest2_performance_test} \title{Run the performance test based on a single commit using shinytest2} \usage{ -run_shinytest2_performance_test(commit, app_dir, project_path, debug) +run_shinytest2_performance_test(commit, app_dir, project_path, use_renv, debug) } \arguments{ \item{commit}{A commit hash code or a branch's name} \item{project_path}{The path to the project with all needed packages installed} +\item{use_renv}{In case it is set as TRUE, package will try to apply +renv::restore() in all branches. Otherwise, the current loaded list of +packages will be used in all branches.} + \item{debug}{Logical. TRUE to display all the system messages on runtime} \item{cypress_file}{The path to the .js file conteining cypress tests to be recorded} From 8b5df0604afddb0ddbf340af43d828faaf32a9db Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Tue, 18 Oct 2022 22:02:20 +0200 Subject: [PATCH 016/225] Wrapping activate and restore into restore_env function --- R/utils.R | 25 ++++++++++++++++++++++++- man/restore_env.Rd | 17 +++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 man/restore_env.Rd diff --git a/R/utils.R b/R/utils.R index 35d9fae..642ff7f 100644 --- a/R/utils.R +++ b/R/utils.R @@ -218,8 +218,31 @@ get_commit_hash <- function() { #' @title Checkout GitHub files #' -#' @description checkout anything created by the app. It prevents errors when +#' @description Checkout anything created by the app. It prevents errors when #' changing branches checkout_files <- function() { system("git checkout .") } + +#' @title Check and restore renv +#' +#' @description Check whether renv is in use in the current branch. Raise error +#' if renv is not in use or apply renv:restore() in the case the package is +#' present +#' +#' @param branch Commit hash code or branch name. Useful to create an +#' informative error message +#' @importFrom glue glue +#' @importFrom renv activate restore +restore_env <- function(branch) { + # handling renv + tryCatch( + expr = { + activate() + restore() + }, + error = function(e) { + stop(glue("Unexpected error activating renv in branch {branch}: {e}\n")) + } + ) +} diff --git a/man/restore_env.Rd b/man/restore_env.Rd new file mode 100644 index 0000000..b0074c0 --- /dev/null +++ b/man/restore_env.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{restore_env} +\alias{restore_env} +\title{Check and restore renv} +\usage{ +restore_env(branch) +} +\arguments{ +\item{branch}{Commit hash code or branch name. Useful to create an +informative error message} +} +\description{ +Check whether renv is in use in the current branch. Raise error +if renv is not in use or apply renv:restore() in the case the package is +present +} From 868d9ea7c34f9f83ff07687f78abce2ffa0e9182 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Tue, 18 Oct 2022 22:02:44 +0200 Subject: [PATCH 017/225] Updating documentation --- NAMESPACE | 2 ++ man/checkout_files.Rd | 2 +- man/performance_tests.Rd | 5 +++++ man/run_cypress_performance_test.Rd | 12 +++++++++++- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index f22e55f..c9a2ddb 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -6,6 +6,8 @@ export(run_shinytest2_performance_test) importFrom(git2r,checkout) importFrom(glue,glue) importFrom(jsonlite,write_json) +importFrom(renv,activate) +importFrom(renv,restore) importFrom(shinytest2,test_app) importFrom(stringr,str_trim) importFrom(testthat,ListReporter) diff --git a/man/checkout_files.Rd b/man/checkout_files.Rd index 4827d50..3811aa9 100644 --- a/man/checkout_files.Rd +++ b/man/checkout_files.Rd @@ -7,6 +7,6 @@ checkout_files() } \description{ -checkout anything created by the app. It prevents errors when +Checkout anything created by the app. It prevents errors when changing branches } diff --git a/man/performance_tests.Rd b/man/performance_tests.Rd index 96bc4f8..84716eb 100644 --- a/man/performance_tests.Rd +++ b/man/performance_tests.Rd @@ -10,6 +10,7 @@ performance_tests( shinytest2_dir = NULL, app_dir = getwd(), port = 3333, + use_renv = TRUE, debug = FALSE ) } @@ -24,6 +25,10 @@ performance_tests( \item{port}{Port to run the app} +\item{use_renv}{In case it is set as TRUE, package will try to apply +renv::restore() in all branches. Otherwise, the current loaded list of +packages will be used in all branches.} + \item{debug}{Logical. TRUE to display all the system messages on runtime} } \description{ diff --git a/man/run_cypress_performance_test.Rd b/man/run_cypress_performance_test.Rd index 6f04d31..2fcc106 100644 --- a/man/run_cypress_performance_test.Rd +++ b/man/run_cypress_performance_test.Rd @@ -4,7 +4,13 @@ \alias{run_cypress_performance_test} \title{Run the performance test based on a single commit using Cypress} \usage{ -run_cypress_performance_test(commit, project_path, cypress_file, debug) +run_cypress_performance_test( + commit, + project_path, + cypress_file, + use_renv, + debug +) } \arguments{ \item{commit}{A commit hash code or a branch's name} @@ -13,6 +19,10 @@ run_cypress_performance_test(commit, project_path, cypress_file, debug) \item{cypress_file}{The path to the .js file conteining cypress tests to be recorded} +\item{use_renv}{In case it is set as TRUE, package will try to apply +renv::restore() in all branches. Otherwise, the current loaded list of +packages will be used in all branches.} + \item{debug}{Logical. TRUE to display all the system messages on runtime} } \description{ From 174482d2ce94eb4dd0b0e9e6d59c7e03673f918f Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 19 Oct 2022 12:20:08 +0200 Subject: [PATCH 018/225] Allowing a vector and checking the vector length --- R/performance_tests.R | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/R/performance_tests.R b/R/performance_tests.R index 65228e9..c4a1210 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -2,7 +2,9 @@ #' #' @param commit_list A list of commit hash codes, branches' names or anything else you can use with git checkout [...] #' @param cypress_file The path to the .js file containing cypress tests to be recorded +#' It can also be a vector of the same size of commit_list #' @param shinytest2_dir The directory with tests recorded by shinytest2 +#' It can also be a vector of the same size of commit_list #' @param app_dir The path to the application root #' @param port Port to run the app #' @param debug Logical. TRUE to display all the system messages on runtime @@ -18,13 +20,26 @@ performance_tests <- function( port = 3333, debug = FALSE ) { + # Number of commits to test + n_commits <- length(commit_list) + # Test whether we have everything we need if (is.null(cypress_file) & is.null(shinytest2_dir)) - stop("You must provide a cypress_file or the shinytest2_dir") + stop("You must provide cypress_file or shinytest2_dir") if (!is.null(cypress_file) & !is.null(shinytest2_dir)) { - message("Using the cypress file only") + message("Using the cypress files only") shinytest2_dir <- NULL + + if (length(cypress_file) == 1) + cypress_file <- rep(cypress_file, n_commits) + if (length(cypress_file) != n_commits) + stop("You must provide 1 or {n_commits} paths for cypress_file") + } else { + if (length(shinytest2_dir) == 1) + shinytest2_dir <- rep(shinytest2_dir, n_commits) + if (length(shinytest2_dir) != n_commits) + stop("You must provide 1 or {n_commits} paths for shinytest2_dir") } type <- ifelse(!is.null(cypress_file), "cypress", "shinytest2") @@ -36,18 +51,14 @@ performance_tests <- function( # creating the structure project_path <- create_cypress_structure(app_dir = app_dir, port = port, debug = debug) - # copy the cypress test file from the current location and store it - cypress_file_cp <- file.path(project_path, "cypress_tests.js") - file.copy(from = cypress_file, to = cypress_file_cp) - # apply the tests for each branch/commit perf_list <- tryCatch( expr = { - lapply( - X = commit_list, + mapply( + commit_list, + cypress_file, FUN = run_cypress_performance_test, project_path = project_path, - cypress_file = cypress_file_cp, debug = debug ) }, @@ -108,6 +119,10 @@ performance_tests <- function( #' #' @export run_cypress_performance_test <- function(commit, project_path, cypress_file, debug) { + # copy the cypress test file from the current location and store it + cypress_file_cp <- file.path(project_path, "cypress_tests.js") + file.copy(from = cypress_file, to = cypress_file_cp) + files <- create_cypress_tests(project_path = project_path, cypress_file = cypress_file) js_file <- files$js_file txt_file <- files$txt_file From c806737f4b0cedf8e6241e632681b70f7960bc93 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 19 Oct 2022 14:15:30 +0200 Subject: [PATCH 019/225] Adding renv_prompt parameter --- R/performance_tests.R | 18 ++++++++++++------ R/utils.R | 5 +++-- man/performance_tests.Rd | 3 +++ man/restore_env.Rd | 4 +++- man/run_cypress_performance_test.Rd | 3 +++ man/run_shinytest2_performance_test.Rd | 11 ++++++++++- 6 files changed, 34 insertions(+), 10 deletions(-) diff --git a/R/performance_tests.R b/R/performance_tests.R index 87965b0..a75ed07 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -8,6 +8,7 @@ #' @param use_renv In case it is set as TRUE, package will try to apply #' renv::restore() in all branches. Otherwise, the current loaded list of #' packages will be used in all branches. +#' @param renv_prompt Prompt the user before taking any action? #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @importFrom git2r checkout @@ -20,6 +21,7 @@ performance_tests <- function( app_dir = getwd(), port = 3333, use_renv = TRUE, + renv_prompt = TRUE, debug = FALSE ) { # Test whether we have everything we need @@ -53,6 +55,7 @@ performance_tests <- function( project_path = project_path, cypress_file = cypress_file_cp, use_renv = use_renv, + renv_prompt = renv_prompt, debug = debug ) }, @@ -63,7 +66,7 @@ performance_tests <- function( # Restore initital setup checkout(branch = current_branch) message(glue("Switched back to {current_branch}")) - if (use_renv) restore_env(branch = current_branch) + if (use_renv) restore_env(branch = current_branch, renv_prompt = renv_prompt) # Cleaning the temporary directory unlink( @@ -88,6 +91,7 @@ performance_tests <- function( app_dir = app_dir, project_path = project_path, use_renv = use_renv, + renv_prompt = renv_prompt, debug = debug ) }, @@ -98,7 +102,7 @@ performance_tests <- function( # Restore initital setup checkout(branch = current_branch) message(glue("Switched back to {current_branch}")) - if (use_renv) restore_env(branch = current_branch) + if (use_renv) restore_env(branch = current_branch, renv_prompt = renv_prompt) # Cleaning the temporary directory unlink(x = file.path(project_path, "tests"), recursive = TRUE) @@ -117,10 +121,11 @@ performance_tests <- function( #' @param use_renv In case it is set as TRUE, package will try to apply #' renv::restore() in all branches. Otherwise, the current loaded list of #' packages will be used in all branches. +#' @param renv_prompt Prompt the user before taking any action? #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @export -run_cypress_performance_test <- function(commit, project_path, cypress_file, use_renv, debug) { +run_cypress_performance_test <- function(commit, project_path, cypress_file, use_renv, renv_prompt, debug) { # create cypress test structure files <- create_cypress_tests(project_path = project_path, cypress_file = cypress_file) js_file <- files$js_file @@ -132,7 +137,7 @@ run_cypress_performance_test <- function(commit, project_path, cypress_file, use message(glue("Switched to {commit}")) # check if we are able to restore packages using renv - if (use_renv) restore_env(branch = commit) + if (use_renv) restore_env(branch = commit, renv_prompt = renv_prompt) # run tests there command <- glue("cd {project_path}; set -eu; exec yarn --cwd node performance-test") @@ -162,19 +167,20 @@ run_cypress_performance_test <- function(commit, project_path, cypress_file, use #' @param use_renv In case it is set as TRUE, package will try to apply #' renv::restore() in all branches. Otherwise, the current loaded list of #' packages will be used in all branches. +#' @param renv_prompt Prompt the user before taking any action? #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @importFrom testthat ListReporter #' @importFrom shinytest2 test_app #' @export -run_shinytest2_performance_test <- function(commit, app_dir, project_path, use_renv, debug) { +run_shinytest2_performance_test <- function(commit, app_dir, project_path, use_renv, renv_prompt, debug) { # checkout to the desired commit checkout(branch = commit) date <- get_commit_date(branch = commit) message(glue("Switched to {commit}")) # check if we are able to restore packages using renv - if (use_renv) restore_env(branch = commit) + if (use_renv) restore_env(branch = commit, renv_prompt = renv_prompt) # run tests there my_reporter <- ListReporter$new() diff --git a/R/utils.R b/R/utils.R index 642ff7f..3c20c2f 100644 --- a/R/utils.R +++ b/R/utils.R @@ -232,14 +232,15 @@ checkout_files <- function() { #' #' @param branch Commit hash code or branch name. Useful to create an #' informative error message +#' @param renv_prompt Prompt the user before taking any action? #' @importFrom glue glue #' @importFrom renv activate restore -restore_env <- function(branch) { +restore_env <- function(branch, renv_prompt) { # handling renv tryCatch( expr = { activate() - restore() + restore(prompt = renv_prompt) }, error = function(e) { stop(glue("Unexpected error activating renv in branch {branch}: {e}\n")) diff --git a/man/performance_tests.Rd b/man/performance_tests.Rd index 84716eb..4180829 100644 --- a/man/performance_tests.Rd +++ b/man/performance_tests.Rd @@ -11,6 +11,7 @@ performance_tests( app_dir = getwd(), port = 3333, use_renv = TRUE, + renv_prompt = TRUE, debug = FALSE ) } @@ -29,6 +30,8 @@ performance_tests( renv::restore() in all branches. Otherwise, the current loaded list of packages will be used in all branches.} +\item{renv_prompt}{Prompt the user before taking any action?} + \item{debug}{Logical. TRUE to display all the system messages on runtime} } \description{ diff --git a/man/restore_env.Rd b/man/restore_env.Rd index b0074c0..57de19a 100644 --- a/man/restore_env.Rd +++ b/man/restore_env.Rd @@ -4,11 +4,13 @@ \alias{restore_env} \title{Check and restore renv} \usage{ -restore_env(branch) +restore_env(branch, renv_prompt) } \arguments{ \item{branch}{Commit hash code or branch name. Useful to create an informative error message} + +\item{renv_prompt}{Prompt the user before taking any action?} } \description{ Check whether renv is in use in the current branch. Raise error diff --git a/man/run_cypress_performance_test.Rd b/man/run_cypress_performance_test.Rd index 2fcc106..aa0ad24 100644 --- a/man/run_cypress_performance_test.Rd +++ b/man/run_cypress_performance_test.Rd @@ -9,6 +9,7 @@ run_cypress_performance_test( project_path, cypress_file, use_renv, + renv_prompt, debug ) } @@ -23,6 +24,8 @@ run_cypress_performance_test( renv::restore() in all branches. Otherwise, the current loaded list of packages will be used in all branches.} +\item{renv_prompt}{Prompt the user before taking any action?} + \item{debug}{Logical. TRUE to display all the system messages on runtime} } \description{ diff --git a/man/run_shinytest2_performance_test.Rd b/man/run_shinytest2_performance_test.Rd index 42667f6..b728811 100644 --- a/man/run_shinytest2_performance_test.Rd +++ b/man/run_shinytest2_performance_test.Rd @@ -4,7 +4,14 @@ \alias{run_shinytest2_performance_test} \title{Run the performance test based on a single commit using shinytest2} \usage{ -run_shinytest2_performance_test(commit, app_dir, project_path, use_renv, debug) +run_shinytest2_performance_test( + commit, + app_dir, + project_path, + use_renv, + renv_prompt, + debug +) } \arguments{ \item{commit}{A commit hash code or a branch's name} @@ -15,6 +22,8 @@ run_shinytest2_performance_test(commit, app_dir, project_path, use_renv, debug) renv::restore() in all branches. Otherwise, the current loaded list of packages will be used in all branches.} +\item{renv_prompt}{Prompt the user before taking any action?} + \item{debug}{Logical. TRUE to display all the system messages on runtime} \item{cypress_file}{The path to the .js file conteining cypress tests to be recorded} From c5ab59f2891c2608a4b1e36bc316e12f42bdbb4d Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 20 Oct 2022 09:59:53 +0200 Subject: [PATCH 020/225] Fixing new lint and devtools::check issues --- .Rbuildignore | 1 + R/performance_tests.R | 3 ++- R/utils.R | 2 ++ man/checkout.Rd | 3 +++ man/run_shinytest2_ptest.Rd | 3 ++- 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.Rbuildignore b/.Rbuildignore index 91114bf..dd2032e 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -1,2 +1,3 @@ ^.*\.Rproj$ ^\.Rproj\.user$ +.github diff --git a/R/performance_tests.R b/R/performance_tests.R index 42b532a..08e04ed 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -154,7 +154,8 @@ run_cypress_ptest <- function(commit, project_path, cypress_file, debug) { #' #' @param commit A commit hash code or a branch's name #' @param app_dir The path to the application root -#' @param project_path The path to the project with all needed packages installed +#' @param project_path The path to the project with all needed +#' packages installed #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @importFrom testthat ListReporter diff --git a/R/utils.R b/R/utils.R index 6e960f2..4238665 100644 --- a/R/utils.R +++ b/R/utils.R @@ -248,6 +248,8 @@ checkout_files <- function() { #' @title Checkout GitHub branch #' #' @description checkout and go to a different branch +#' +#' @param branch Commit hash code or branch name checkout <- function(branch) { system( glue("git checkout {branch}") diff --git a/man/checkout.Rd b/man/checkout.Rd index f8e5a5e..0c38c46 100644 --- a/man/checkout.Rd +++ b/man/checkout.Rd @@ -6,6 +6,9 @@ \usage{ checkout(branch) } +\arguments{ +\item{branch}{Commit hash code or branch name} +} \description{ checkout and go to a different branch } diff --git a/man/run_shinytest2_ptest.Rd b/man/run_shinytest2_ptest.Rd index 1678586..c8a2300 100644 --- a/man/run_shinytest2_ptest.Rd +++ b/man/run_shinytest2_ptest.Rd @@ -11,7 +11,8 @@ run_shinytest2_ptest(commit, app_dir, project_path, debug) \item{app_dir}{The path to the application root} -\item{project_path}{The path to the project with all needed packages installed} +\item{project_path}{The path to the project with all needed +packages installed} \item{debug}{Logical. TRUE to display all the system messages on runtime} } From 4411bd888ff0812216f3934e55aee2012284b283 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 20 Oct 2022 11:50:47 +0200 Subject: [PATCH 021/225] splitting utils files --- R/performance_tests.R | 2 +- R/utils.R | 197 +---------------------------- R/utils_cypress.R | 178 ++++++++++++++++++++++++++ R/utils_shinytest2.R | 14 ++ man/add_sendtime2js.Rd | 2 +- man/create_cypress_list.Rd | 2 +- man/create_cypress_plugins.Rd | 2 +- man/create_cypress_structure.Rd | 2 +- man/create_cypress_tests.Rd | 2 +- man/create_node_list.Rd | 2 +- man/create_shinytest2_structure.Rd | 2 +- man/performance_tests.Rd | 5 +- 12 files changed, 204 insertions(+), 206 deletions(-) create mode 100644 R/utils_cypress.R create mode 100644 R/utils_shinytest2.R diff --git a/R/performance_tests.R b/R/performance_tests.R index 747dc5c..b7c623c 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -3,7 +3,7 @@ #' @param commit_list A list of commit hash codes, branches' names or anything #' else you can use with git checkout [...] #' @param cypress_file The path to the .js file containing cypress tests to -#' be recorded +#' be recorded. It can also be a vector of the same size of commit_list #' @param shinytest2_dir The directory with tests recorded by shinytest2 #' It can also be a vector of the same size of commit_list #' @param app_dir The path to the application root diff --git a/R/utils.R b/R/utils.R index 4238665..9518090 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1,197 +1,3 @@ -#' @title Create a temporary directory to store everything needed by shinytest2 -#' -#' @param shinytest2_dir The path to the shinytest2 tests -create_shinytest2_structure <- function(shinytest2_dir) { - # temp dir to run the tests - dir_tests <- tempdir() - - # copy everything to the temporary directory - system(glue("cp -r {shinytest2_dir} {dir_tests}")) - - # returning the project folder - message(glue("Structure created at {dir_tests}")) - return(dir_tests) -} - -#' @title Create a temporary directory to store everything needed by Cypress -#' -#' @param app_dir The path to the application root -#' @param port Port to run the app -#' @param debug Logical. TRUE to display all the system messages on runtime -#' -#' @importFrom jsonlite write_json -create_cypress_structure <- function(app_dir, port, debug) { - # temp dir to run the tests - dir_tests <- tempdir() - - # node path - node_path <- file.path(dir_tests, "node") - root_path <- file.path(node_path, "root") # nolint - - # test path - tests_path <- file.path(dir_tests, "tests") - cypress_path <- file.path(tests_path, "cypress") - integration_path <- file.path(cypress_path, "integration") - plugins_path <- file.path(cypress_path, "plugins") - - # creating paths - dir.create(path = node_path, showWarnings = FALSE) - dir.create(path = tests_path, showWarnings = FALSE) - dir.create(path = cypress_path, showWarnings = FALSE) - dir.create(path = integration_path, showWarnings = FALSE) - dir.create(path = plugins_path, showWarnings = FALSE) - - # create a path root linked to the main directory app - symlink_cmd <- glue("cd {dir_tests}; ln -s {app_dir} {root_path}") - system(symlink_cmd) - - # create the packages.json file - json_txt <- create_node_list(tests_path = tests_path, port = port) - json_file <- file.path(node_path, "package.json") - write_json(x = json_txt, path = json_file, pretty = TRUE, auto_unbox = TRUE) - - # install everything that is needed - install_deps <- glue("yarn --cwd {node_path}") - system(install_deps, ignore.stdout = !debug, ignore.stderr = !debug) - - # creating cypress plugin file - js_txt <- create_cypress_plugins() - js_file <- file.path(plugins_path, "index.js") - writeLines(text = js_txt, con = js_file) - - # creating cypress.json - json_txt <- create_cypress_list(plugins_file = js_file, port = port) - json_file <- file.path(tests_path, "cypress.json") - write_json(x = json_txt, path = json_file, pretty = TRUE, auto_unbox = TRUE) - - # returning the project folder - message(glue("Structure created at {dir_tests}")) - return(dir_tests) -} - -#' @title Create the list of needed libraries -#' -#' @param tests_path The path to project -#' @param port Port to run the app -create_node_list <- function(tests_path, port) { - json_list <- list( - private = TRUE, - scripts = list( - "performance-test" = glue( - "start-server-and-test run-app http://localhost:{port} run-cypress" - ), - "run-app" = glue("cd root && Rscript -e 'shiny::runApp(port = {port})'"), - "run-cypress" = glue("cypress run --project {tests_path}") - ), - "devDependencies" = list( - "cypress" = "^7.6.0", - "start-server-and-test" = "^1.12.6" - ) - ) - - return(json_list) -} - -#' @title Create the cypress configuration list -#' -#' @param plugins_file The path to the Cypress plugins -#' @param port Port to run the app -create_cypress_list <- function(plugins_file, port) { - json_list <- list( - baseUrl = glue("http://localhost:{port}"), - pluginsFile = plugins_file, - supportFile = FALSE - ) - - return(json_list) -} - -#' @title Create the JS code to track execution time -create_cypress_plugins <- function() { - js_txt <- " - const fs = require('fs') - module.exports = (on, config) => { - on('task', { - performanceTimes (attributes) { - fs.writeFile( - attributes.fileOut, - `${ attributes.title }; ${ attributes.duration }\n`, - { flag: 'a' } - ) - return null - } - }) - }" - - return(js_txt) -} - -#' @title Create the cypress files under project directory -#' -#' @param project_path The path to the project with all needed packages -#' installed -#' @param cypress_file The path to the .js file conteining cypress tests to -#' be recorded -create_cypress_tests <- function(project_path, cypress_file) { - # creating a copy to be able to edit the js file - js_file <- file.path( - project_path, - "tests", - "cypress", - "integration", - "app.spec.js" - ) - - file.copy(from = cypress_file, to = js_file, overwrite = TRUE) - - # file to store the times - txt_file <- file.path(project_path, "tests", "cypress", "performance.txt") - add_sendtime2js(js_file = js_file, txt_file = txt_file) - - # returning the file location - return(list(js_file = js_file, txt_file = txt_file)) -} - -#' @title Add the sendTime function to the .js file -#' -#' @param js_file Path to the .js file to add code -#' @param txt_file Path to the file to record the execution times -add_sendtime2js <- function(js_file, txt_file) { - lines_to_add <- glue( - " - // Returning the time for each test - // https://www.cypress.io/blog/2020/05/22/where-does-the-test-spend-its-time/ - let commands = [] - let performanceAttrs - Cypress.on('test:before:run', () => { - commands.length = 0 - }) - Cypress.on('test:after:run', (attributes) => { - performanceAttrs = { - title: attributes.title, - duration: attributes.duration, - commands: Cypress._.cloneDeep(commands), - } - }) - const sendTestTimings = () => { - if (!performanceAttrs) { - return - } - const attr = performanceAttrs - attr.fileOut = '{{txt_file}}' - performanceAttrs = null - cy.task('performanceTimes', attr) - } - // Calling the sendTestTimings function - beforeEach(sendTestTimings) - after(sendTestTimings) - ", - .open = "{{", .close = "}}" - ) - - write(x = lines_to_add, file = js_file, append = TRUE) -} - #' @title Get the commit date in POSIXct format #' #' @param branch Commit hash code or branch name @@ -207,6 +13,7 @@ get_commit_date <- function(branch) { } #' @title Find the hash code of the current commit +#' #' @importFrom glue glue #' @importFrom stringr str_trim get_commit_hash <- function() { @@ -238,7 +45,6 @@ get_commit_hash <- function() { } #' @title Checkout GitHub files -#' #' @description checkout anything created by the app. It prevents errors when #' changing branches checkout_files <- function() { @@ -246,7 +52,6 @@ checkout_files <- function() { } #' @title Checkout GitHub branch -#' #' @description checkout and go to a different branch #' #' @param branch Commit hash code or branch name diff --git a/R/utils_cypress.R b/R/utils_cypress.R new file mode 100644 index 0000000..ff4a611 --- /dev/null +++ b/R/utils_cypress.R @@ -0,0 +1,178 @@ +#' @title Create a temporary directory to store everything needed by Cypress +#' +#' @param app_dir The path to the application root +#' @param port Port to run the app +#' @param debug Logical. TRUE to display all the system messages on runtime +#' +#' @importFrom jsonlite write_json +create_cypress_structure <- function(app_dir, port, debug) { + # temp dir to run the tests + dir_tests <- tempdir() + + # node path + node_path <- file.path(dir_tests, "node") + root_path <- file.path(node_path, "root") # nolint + + # test path + tests_path <- file.path(dir_tests, "tests") + cypress_path <- file.path(tests_path, "cypress") + integration_path <- file.path(cypress_path, "integration") + plugins_path <- file.path(cypress_path, "plugins") + + # creating paths + dir.create(path = node_path, showWarnings = FALSE) + dir.create(path = tests_path, showWarnings = FALSE) + dir.create(path = cypress_path, showWarnings = FALSE) + dir.create(path = integration_path, showWarnings = FALSE) + dir.create(path = plugins_path, showWarnings = FALSE) + + # create a path root linked to the main directory app + symlink_cmd <- glue("cd {dir_tests}; ln -s {app_dir} {root_path}") + system(symlink_cmd) + + # create the packages.json file + json_txt <- create_node_list(tests_path = tests_path, port = port) + json_file <- file.path(node_path, "package.json") + write_json(x = json_txt, path = json_file, pretty = TRUE, auto_unbox = TRUE) + + # install everything that is needed + install_deps <- glue("yarn --cwd {node_path}") + system(install_deps, ignore.stdout = !debug, ignore.stderr = !debug) + + # creating cypress plugin file + js_txt <- create_cypress_plugins() + js_file <- file.path(plugins_path, "index.js") + writeLines(text = js_txt, con = js_file) + + # creating cypress.json + json_txt <- create_cypress_list(plugins_file = js_file, port = port) + json_file <- file.path(tests_path, "cypress.json") + write_json(x = json_txt, path = json_file, pretty = TRUE, auto_unbox = TRUE) + + # returning the project folder + message(glue("Structure created at {dir_tests}")) + return(dir_tests) +} + +#' @title Create the list of needed libraries +#' +#' @param tests_path The path to project +#' @param port Port to run the app +create_node_list <- function(tests_path, port) { + json_list <- list( + private = TRUE, + scripts = list( + "performance-test" = glue( + "start-server-and-test run-app http://localhost:{port} run-cypress" + ), + "run-app" = glue("cd root && Rscript -e 'shiny::runApp(port = {port})'"), + "run-cypress" = glue("cypress run --project {tests_path}") + ), + "devDependencies" = list( + "cypress" = "^7.6.0", + "start-server-and-test" = "^1.12.6" + ) + ) + + return(json_list) +} + +#' @title Create the cypress configuration list +#' +#' @param plugins_file The path to the Cypress plugins +#' @param port Port to run the app +create_cypress_list <- function(plugins_file, port) { + json_list <- list( + baseUrl = glue("http://localhost:{port}"), + pluginsFile = plugins_file, + supportFile = FALSE + ) + + return(json_list) +} + +#' @title Create the JS code to track execution time +create_cypress_plugins <- function() { + js_txt <- " + const fs = require('fs') + module.exports = (on, config) => { + on('task', { + performanceTimes (attributes) { + fs.writeFile( + attributes.fileOut, + `${ attributes.title }; ${ attributes.duration }\n`, + { flag: 'a' } + ) + return null + } + }) + }" + + return(js_txt) +} + +#' @title Create the cypress files under project directory +#' +#' @param project_path The path to the project with all needed packages +#' installed +#' @param cypress_file The path to the .js file conteining cypress tests to +#' be recorded +create_cypress_tests <- function(project_path, cypress_file) { + # creating a copy to be able to edit the js file + js_file <- file.path( + project_path, + "tests", + "cypress", + "integration", + "app.spec.js" + ) + + file.copy(from = cypress_file, to = js_file, overwrite = TRUE) + + # file to store the times + txt_file <- file.path(project_path, "tests", "cypress", "performance.txt") + add_sendtime2js(js_file = js_file, txt_file = txt_file) + + # returning the file location + return(list(js_file = js_file, txt_file = txt_file)) +} + +#' @title Add the sendTime function to the .js file +#' +#' @param js_file Path to the .js file to add code +#' @param txt_file Path to the file to record the execution times +add_sendtime2js <- function(js_file, txt_file) { + lines_to_add <- glue( + " + // Returning the time for each test + // https://www.cypress.io/blog/2020/05/22/where-does-the-test-spend-its-time/ + let commands = [] + let performanceAttrs + Cypress.on('test:before:run', () => { + commands.length = 0 + }) + Cypress.on('test:after:run', (attributes) => { + performanceAttrs = { + title: attributes.title, + duration: attributes.duration, + commands: Cypress._.cloneDeep(commands), + } + }) + const sendTestTimings = () => { + if (!performanceAttrs) { + return + } + const attr = performanceAttrs + attr.fileOut = '{{txt_file}}' + performanceAttrs = null + cy.task('performanceTimes', attr) + } + // Calling the sendTestTimings function + beforeEach(sendTestTimings) + after(sendTestTimings) + ", + .open = "{{", .close = "}}" + ) + + write(x = lines_to_add, file = js_file, append = TRUE) +} diff --git a/R/utils_shinytest2.R b/R/utils_shinytest2.R new file mode 100644 index 0000000..25d200c --- /dev/null +++ b/R/utils_shinytest2.R @@ -0,0 +1,14 @@ +#' @title Create a temporary directory to store everything needed by shinytest2 +#' +#' @param shinytest2_dir The path to the shinytest2 tests +create_shinytest2_structure <- function(shinytest2_dir) { + # temp dir to run the tests + dir_tests <- tempdir() + + # copy everything to the temporary directory + system(glue("cp -r {shinytest2_dir} {dir_tests}")) + + # returning the project folder + message(glue("Structure created at {dir_tests}")) + return(dir_tests) +} diff --git a/man/add_sendtime2js.Rd b/man/add_sendtime2js.Rd index c86b595..c34dba9 100644 --- a/man/add_sendtime2js.Rd +++ b/man/add_sendtime2js.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils.R +% Please edit documentation in R/utils_cypress.R \name{add_sendtime2js} \alias{add_sendtime2js} \title{Add the sendTime function to the .js file} diff --git a/man/create_cypress_list.Rd b/man/create_cypress_list.Rd index c1b9239..2fe110a 100644 --- a/man/create_cypress_list.Rd +++ b/man/create_cypress_list.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils.R +% Please edit documentation in R/utils_cypress.R \name{create_cypress_list} \alias{create_cypress_list} \title{Create the cypress configuration list} diff --git a/man/create_cypress_plugins.Rd b/man/create_cypress_plugins.Rd index 208e23c..cc97969 100644 --- a/man/create_cypress_plugins.Rd +++ b/man/create_cypress_plugins.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils.R +% Please edit documentation in R/utils_cypress.R \name{create_cypress_plugins} \alias{create_cypress_plugins} \title{Create the JS code to track execution time} diff --git a/man/create_cypress_structure.Rd b/man/create_cypress_structure.Rd index f03c8d5..4881488 100644 --- a/man/create_cypress_structure.Rd +++ b/man/create_cypress_structure.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils.R +% Please edit documentation in R/utils_cypress.R \name{create_cypress_structure} \alias{create_cypress_structure} \title{Create a temporary directory to store everything needed by Cypress} diff --git a/man/create_cypress_tests.Rd b/man/create_cypress_tests.Rd index 6d52228..f0f66a1 100644 --- a/man/create_cypress_tests.Rd +++ b/man/create_cypress_tests.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils.R +% Please edit documentation in R/utils_cypress.R \name{create_cypress_tests} \alias{create_cypress_tests} \title{Create the cypress files under project directory} diff --git a/man/create_node_list.Rd b/man/create_node_list.Rd index 1f53065..8b79fcf 100644 --- a/man/create_node_list.Rd +++ b/man/create_node_list.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils.R +% Please edit documentation in R/utils_cypress.R \name{create_node_list} \alias{create_node_list} \title{Create the list of needed libraries} diff --git a/man/create_shinytest2_structure.Rd b/man/create_shinytest2_structure.Rd index 32024be..82a25ba 100644 --- a/man/create_shinytest2_structure.Rd +++ b/man/create_shinytest2_structure.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils.R +% Please edit documentation in R/utils_shinytest2.R \name{create_shinytest2_structure} \alias{create_shinytest2_structure} \title{Create a temporary directory to store everything needed by shinytest2} diff --git a/man/performance_tests.Rd b/man/performance_tests.Rd index a4a26e8..64b3d7b 100644 --- a/man/performance_tests.Rd +++ b/man/performance_tests.Rd @@ -18,9 +18,10 @@ performance_tests( else you can use with git checkout \link{...}} \item{cypress_file}{The path to the .js file containing cypress tests to -be recorded} +be recorded. It can also be a vector of the same size of commit_list} -\item{shinytest2_dir}{The directory with tests recorded by shinytest2} +\item{shinytest2_dir}{The directory with tests recorded by shinytest2 +It can also be a vector of the same size of commit_list} \item{app_dir}{The path to the application root} From a7fd3d5b8e1018a4bea0c007ca4ded887c02979e Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 20 Oct 2022 12:04:59 +0200 Subject: [PATCH 022/225] Split cypress and shinytest2 functions into two files --- NAMESPACE | 2 + R/performance_tests.R | 154 ++---------------------------------- R/tests_cypress.R | 97 +++++++++++++++++++++++ R/tests_shinytest2.R | 83 +++++++++++++++++++ man/ptest_cypress.Rd | 24 ++++++ man/ptest_shinytest2.Rd | 22 ++++++ man/run_cypress_ptest.Rd | 2 +- man/run_shinytest2_ptest.Rd | 2 +- 8 files changed, 238 insertions(+), 148 deletions(-) create mode 100644 R/tests_cypress.R create mode 100644 R/tests_shinytest2.R create mode 100644 man/ptest_cypress.Rd create mode 100644 man/ptest_shinytest2.Rd diff --git a/NAMESPACE b/NAMESPACE index 75c7af8..ca11974 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,6 +1,8 @@ # Generated by roxygen2: do not edit by hand export(performance_tests) +export(ptest_cypress) +export(ptest_shinytest2) export(run_cypress_ptest) export(run_shinytest2_ptest) importFrom(glue,glue) diff --git a/R/performance_tests.R b/R/performance_tests.R index b7c623c..50cb8ba 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -47,159 +47,21 @@ performance_tests <- function( current_branch <- get_commit_hash() if (type == "cypress") { - # creating the structure - project_path <- create_cypress_structure( + perf_list <- ptest_cypress( + commit_list = commit_list, + cypress_file = cypress_file, app_dir = app_dir, port = port, debug = debug ) - - # apply the tests for each branch/commit - perf_list <- tryCatch( - expr = { - mapply( - commit_list, - cypress_file, - FUN = run_cypress_performance_test, - project_path = project_path, - debug = debug - ) - }, - error = function(e) { - message(e) - }, - finally = { - checkout(branch = current_branch) - message(glue("Switched back to {current_branch}")) - - # Cleaning the temporary directory - unlink( - x = c( - file.path(project_path, "node"), - file.path(project_path, "tests") - ), - recursive = TRUE - ) - } - ) } else { - # creating the structure - project_path <- create_shinytest2_structure(shinytest2_dir = shinytest2_dir) - - # apply the tests for each branch/commit - perf_list <- tryCatch( - expr = { - lapply( - X = commit_list, - FUN = run_shinytest2_ptest, - app_dir = app_dir, - project_path = project_path, - debug = debug - ) - }, - error = function(e) { - message(e) - }, - finally = { - checkout(branch = current_branch) - message(glue("Switched back to {current_branch}")) - - # Cleaning the temporary directory - unlink(x = file.path(project_path, "tests"), recursive = TRUE) - } + perf_list <- ptest_shinytest2( + commit_list, + shinytest2_dir, + app_dir, + debug ) } return(perf_list) } - -#' @title Run the performance test based on a single commit using Cypress -#' -#' @param commit A commit hash code or a branch's name -#' @param project_path The path to the project with all needed packages -#' installed -#' @param cypress_file The path to the .js file conteining cypress tests to -#' be recorded -#' @param debug Logical. TRUE to display all the system messages on runtime -#' -#' @importFrom utils read.table -#' @export -run_cypress_ptest <- function(commit, project_path, cypress_file, debug) { - files <- create_cypress_tests( - project_path = project_path, - cypress_file = cypress_file - ) - - js_file <- files$js_file - txt_file <- files$txt_file - - # checkout to the desired commit - checkout(branch = commit) - date <- get_commit_date(branch = commit) - message(glue("Switched to {commit}")) - - # run tests there - command <- glue( - "cd {project_path}; ", - "set -eu; exec yarn --cwd node performance-test" - ) - system(command, ignore.stdout = !debug, ignore.stderr = !debug) - - # read the file saved by cypress - perf_file <- read.table(file = txt_file, header = FALSE, sep = ";") - perf_file <- cbind.data.frame(date = date, perf_file) - colnames(perf_file) <- c("date", "test_name", "duration_ms") - - # removing temp files - unlink(x = c(js_file, txt_file)) - - # removing anything new in the github repo - checkout_files() - - # return times - return(perf_file) -} - -#' @title Run the performance test based on a single commit using shinytest2 -#' -#' @param commit A commit hash code or a branch's name -#' @param app_dir The path to the application root -#' @param project_path The path to the project with all needed -#' packages installed -#' @param debug Logical. TRUE to display all the system messages on runtime -#' -#' @importFrom testthat ListReporter -#' @importFrom shinytest2 test_app -#' @export -run_shinytest2_ptest <- function(commit, app_dir, project_path, debug) { - # checkout to the desired commit - checkout(branch = commit) - date <- get_commit_date(branch = commit) - message(glue("Switched to {commit}")) - - # run tests there - my_reporter <- ListReporter$new() - test_app( - app_dir = app_dir, - reporter = my_reporter, - stop_on_failure = FALSE, - stop_on_warning = FALSE - ) - - perf_file <- as.data.frame(my_reporter$get_results()) - perf_file <- perf_file[, c("test", "real")] - perf_file$test <- gsub( - x = perf_file$test, - pattern = "\\{shinytest2\\} recording: ", - replacement = "" - ) - - perf_file <- cbind.data.frame(date = date, perf_file) - colnames(perf_file) <- c("date", "test_name", "duration_ms") - - # removing anything new in the github repo - checkout_files() - - # return times - return(perf_file) -} diff --git a/R/tests_cypress.R b/R/tests_cypress.R new file mode 100644 index 0000000..c18a454 --- /dev/null +++ b/R/tests_cypress.R @@ -0,0 +1,97 @@ +#' @title Run the performance test based on multiple commits using Cypress +#' +#' @param commit_list A list of commit hash codes, branches' names or anything +#' else you can use with git checkout [...] +#' @param cypress_file The path to the .js file conteining cypress tests to +#' be recorded +#' @param app_dir The path to the application root +#' @param port Port to run the app +#' @param debug Logical. TRUE to display all the system messages on runtime +#' +#' @export +ptest_cypress <- function(commit_list, cypress_file, app_dir, port, debug) { + # creating the structure + project_path <- create_cypress_structure( + app_dir = app_dir, + port = port, + debug = debug + ) + + # apply the tests for each branch/commit + perf_list <- tryCatch( + expr = { + mapply( + commit_list, + cypress_file, + FUN = run_cypress_performance_test, + project_path = project_path, + debug = debug + ) + }, + error = function(e) { + message(e) + }, + finally = { + checkout(branch = current_branch) + message(glue("Switched back to {current_branch}")) + + # Cleaning the temporary directory + unlink( + x = c( + file.path(project_path, "node"), + file.path(project_path, "tests") + ), + recursive = TRUE + ) + } + ) + + return(perf_list) +} + +#' @title Run the performance test based on a single commit using Cypress +#' +#' @param commit A commit hash code or a branch's name +#' @param project_path The path to the project with all needed packages +#' installed +#' @param cypress_file The path to the .js file conteining cypress tests to +#' be recorded +#' @param debug Logical. TRUE to display all the system messages on runtime +#' +#' @importFrom utils read.table +#' @export +run_cypress_ptest <- function(commit, project_path, cypress_file, debug) { + files <- create_cypress_tests( + project_path = project_path, + cypress_file = cypress_file + ) + + js_file <- files$js_file + txt_file <- files$txt_file + + # checkout to the desired commit + checkout(branch = commit) + date <- get_commit_date(branch = commit) + message(glue("Switched to {commit}")) + + # run tests there + command <- glue( + "cd {project_path}; ", + "set -eu; exec yarn --cwd node performance-test" + ) + system(command, ignore.stdout = !debug, ignore.stderr = !debug) + + # read the file saved by cypress + perf_file <- read.table(file = txt_file, header = FALSE, sep = ";") + perf_file <- cbind.data.frame(date = date, perf_file) + colnames(perf_file) <- c("date", "test_name", "duration_ms") + + # removing temp files + unlink(x = c(js_file, txt_file)) + + # removing anything new in the github repo + checkout_files() + + # return times + return(perf_file) +} diff --git a/R/tests_shinytest2.R b/R/tests_shinytest2.R new file mode 100644 index 0000000..da88dd7 --- /dev/null +++ b/R/tests_shinytest2.R @@ -0,0 +1,83 @@ +#' @title Run the performance test based on a multiple commits using shinytest2 +#' +#' @param commit_list A list of commit hash codes, branches' names or anything +#' else you can use with git checkout [...] +#' @param shinytest2_dir The directory with tests recorded by shinytest2 +#' It can also be a vector of the same size of commit_list +#' @param app_dir The path to the application root +#' @param debug Logical. TRUE to display all the system messages on runtime +#' +#' @export +ptest_shinytest2 <- function(commit_list, shinytest2_dir, app_dir, debug) { + # creating the structure + project_path <- create_shinytest2_structure(shinytest2_dir = shinytest2_dir) + + # apply the tests for each branch/commit + perf_list <- tryCatch( + expr = { + lapply( + X = commit_list, + FUN = run_shinytest2_ptest, + app_dir = app_dir, + project_path = project_path, + debug = debug + ) + }, + error = function(e) { + message(e) + }, + finally = { + checkout(branch = current_branch) + message(glue("Switched back to {current_branch}")) + + # Cleaning the temporary directory + unlink(x = file.path(project_path, "tests"), recursive = TRUE) + } + ) + + return(perf_list) +} + +#' @title Run the performance test based on a single commit using shinytest2 +#' +#' @param commit A commit hash code or a branch's name +#' @param app_dir The path to the application root +#' @param project_path The path to the project with all needed +#' packages installed +#' @param debug Logical. TRUE to display all the system messages on runtime +#' +#' @importFrom testthat ListReporter +#' @importFrom shinytest2 test_app +#' @export +run_shinytest2_ptest <- function(commit, app_dir, project_path, debug) { + # checkout to the desired commit + checkout(branch = commit) + date <- get_commit_date(branch = commit) + message(glue("Switched to {commit}")) + + # run tests there + my_reporter <- ListReporter$new() + test_app( + app_dir = app_dir, + reporter = my_reporter, + stop_on_failure = FALSE, + stop_on_warning = FALSE + ) + + perf_file <- as.data.frame(my_reporter$get_results()) + perf_file <- perf_file[, c("test", "real")] + perf_file$test <- gsub( + x = perf_file$test, + pattern = "\\{shinytest2\\} recording: ", + replacement = "" + ) + + perf_file <- cbind.data.frame(date = date, perf_file) + colnames(perf_file) <- c("date", "test_name", "duration_ms") + + # removing anything new in the github repo + checkout_files() + + # return times + return(perf_file) +} diff --git a/man/ptest_cypress.Rd b/man/ptest_cypress.Rd new file mode 100644 index 0000000..b2bfbc7 --- /dev/null +++ b/man/ptest_cypress.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tests_cypress.R +\name{ptest_cypress} +\alias{ptest_cypress} +\title{Run the performance test based on multiple commits using Cypress} +\usage{ +ptest_cypress(commit_list, cypress_file, app_dir, port, debug) +} +\arguments{ +\item{commit_list}{A list of commit hash codes, branches' names or anything +else you can use with git checkout \link{...}} + +\item{cypress_file}{The path to the .js file conteining cypress tests to +be recorded} + +\item{app_dir}{The path to the application root} + +\item{port}{Port to run the app} + +\item{debug}{Logical. TRUE to display all the system messages on runtime} +} +\description{ +Run the performance test based on multiple commits using Cypress +} diff --git a/man/ptest_shinytest2.Rd b/man/ptest_shinytest2.Rd new file mode 100644 index 0000000..48e2f3a --- /dev/null +++ b/man/ptest_shinytest2.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tests_shinytest2.R +\name{ptest_shinytest2} +\alias{ptest_shinytest2} +\title{Run the performance test based on a multiple commits using shinytest2} +\usage{ +ptest_shinytest2(commit_list, shinytest2_dir, app_dir, debug) +} +\arguments{ +\item{commit_list}{A list of commit hash codes, branches' names or anything +else you can use with git checkout \link{...}} + +\item{shinytest2_dir}{The directory with tests recorded by shinytest2 +It can also be a vector of the same size of commit_list} + +\item{app_dir}{The path to the application root} + +\item{debug}{Logical. TRUE to display all the system messages on runtime} +} +\description{ +Run the performance test based on a multiple commits using shinytest2 +} diff --git a/man/run_cypress_ptest.Rd b/man/run_cypress_ptest.Rd index 4aace05..afce4cf 100644 --- a/man/run_cypress_ptest.Rd +++ b/man/run_cypress_ptest.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/performance_tests.R +% Please edit documentation in R/tests_cypress.R \name{run_cypress_ptest} \alias{run_cypress_ptest} \title{Run the performance test based on a single commit using Cypress} diff --git a/man/run_shinytest2_ptest.Rd b/man/run_shinytest2_ptest.Rd index c8a2300..5e73e48 100644 --- a/man/run_shinytest2_ptest.Rd +++ b/man/run_shinytest2_ptest.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/performance_tests.R +% Please edit documentation in R/tests_shinytest2.R \name{run_shinytest2_ptest} \alias{run_shinytest2_ptest} \title{Run the performance test based on a single commit using shinytest2} From ac92113dcfc58b65bc4878e37f032a319849dde9 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 20 Oct 2022 18:50:28 +0200 Subject: [PATCH 023/225] Creating app.R in the tmp folder and adding fucntion to move tests to tmp folder --- R/utils_shinytest2.R | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/R/utils_shinytest2.R b/R/utils_shinytest2.R index 25d200c..8aff733 100644 --- a/R/utils_shinytest2.R +++ b/R/utils_shinytest2.R @@ -1,14 +1,32 @@ #' @title Create a temporary directory to store everything needed by shinytest2 #' -#' @param shinytest2_dir The path to the shinytest2 tests -create_shinytest2_structure <- function(shinytest2_dir) { +#' @param app_dir +#' +#' @importFrom glue glue +create_shinytest2_structure <- function(app_dir) { # temp dir to run the tests dir_tests <- tempdir() - # copy everything to the temporary directory - system(glue("cp -r {shinytest2_dir} {dir_tests}")) + # shiny call + writeLines( + text = glue('shiny::runApp(appDir = "{app_dir}")'), + con = file.path(dir_tests, "app.R") + ) # returning the project folder message(glue("Structure created at {dir_tests}")) + return(dir_tests) } + +#' @title Move tests to a temporary folder +#' +#' @param project_path The path to the project +#' @param shinytest2_dir The directory with tests recorded by shinytest2 +move_shinytest2_tests <- function(project_path, shinytest2_dir) { + # copy everything to the temporary directory + system(glue("cp -r {shinytest2_dir} {project_path}")) + tests_dir <- file.path(project_path, "tests") + + return(tests_dir) +} From 308b7be0c03e410da3ab27d40de0fa45e4749236 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 20 Oct 2022 18:51:33 +0200 Subject: [PATCH 024/225] Using mapply correctly --- R/tests_cypress.R | 19 ++++++++++++------- R/tests_shinytest2.R | 24 ++++++++++++++++-------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/R/tests_cypress.R b/R/tests_cypress.R index c18a454..490cf46 100644 --- a/R/tests_cypress.R +++ b/R/tests_cypress.R @@ -17,15 +17,19 @@ ptest_cypress <- function(commit_list, cypress_file, app_dir, port, debug) { debug = debug ) + # getting the current branch + current_branch <- get_commit_hash() + # apply the tests for each branch/commit perf_list <- tryCatch( expr = { mapply( commit_list, cypress_file, - FUN = run_cypress_performance_test, + FUN = run_cypress_ptest, project_path = project_path, - debug = debug + debug = debug, + SIMPLIFY = FALSE ) }, error = function(e) { @@ -61,6 +65,12 @@ ptest_cypress <- function(commit_list, cypress_file, app_dir, port, debug) { #' @importFrom utils read.table #' @export run_cypress_ptest <- function(commit, project_path, cypress_file, debug) { + # checkout to the desired commit + checkout(branch = commit) + date <- get_commit_date(branch = commit) + message(glue("Switched to {commit}")) + + # get Cypress files files <- create_cypress_tests( project_path = project_path, cypress_file = cypress_file @@ -69,11 +79,6 @@ run_cypress_ptest <- function(commit, project_path, cypress_file, debug) { js_file <- files$js_file txt_file <- files$txt_file - # checkout to the desired commit - checkout(branch = commit) - date <- get_commit_date(branch = commit) - message(glue("Switched to {commit}")) - # run tests there command <- glue( "cd {project_path}; ", diff --git a/R/tests_shinytest2.R b/R/tests_shinytest2.R index da88dd7..3abc91c 100644 --- a/R/tests_shinytest2.R +++ b/R/tests_shinytest2.R @@ -10,17 +10,22 @@ #' @export ptest_shinytest2 <- function(commit_list, shinytest2_dir, app_dir, debug) { # creating the structure - project_path <- create_shinytest2_structure(shinytest2_dir = shinytest2_dir) + project_path <- create_shinytest2_structure(app_dir = app_dir) + + # getting the current branch + current_branch <- get_commit_hash() # apply the tests for each branch/commit perf_list <- tryCatch( expr = { - lapply( - X = commit_list, + mapply( + commit_list, + shinytest2_dir, FUN = run_shinytest2_ptest, app_dir = app_dir, project_path = project_path, - debug = debug + debug = debug, + SIMPLIFY = FALSE ) }, error = function(e) { @@ -42,23 +47,26 @@ ptest_shinytest2 <- function(commit_list, shinytest2_dir, app_dir, debug) { #' #' @param commit A commit hash code or a branch's name #' @param app_dir The path to the application root -#' @param project_path The path to the project with all needed -#' packages installed +#' @param project_path The path to the project +#' @param shinytest2_dir The directory with tests recorded by shinytest2 #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @importFrom testthat ListReporter #' @importFrom shinytest2 test_app #' @export -run_shinytest2_ptest <- function(commit, app_dir, project_path, debug) { +run_shinytest2_ptest <- function(commit, project_path, app_dir, shinytest2_dir, debug) { # checkout to the desired commit checkout(branch = commit) date <- get_commit_date(branch = commit) message(glue("Switched to {commit}")) + # move test files to the project folder + tests_dir <- move_shinytest2_tests(project_path = project_path, shinytest2_dir = shinytest2_dir) + # run tests there my_reporter <- ListReporter$new() test_app( - app_dir = app_dir, + app_dir = dirname(tests_dir), reporter = my_reporter, stop_on_failure = FALSE, stop_on_warning = FALSE From 1c8df0c5453b0c23957e591787ea03de1a1f3b4a Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 20 Oct 2022 18:52:27 +0200 Subject: [PATCH 025/225] Raising error when the length of input files is different than the commit list --- R/performance_tests.R | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/R/performance_tests.R b/R/performance_tests.R index 50cb8ba..596d8de 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -29,23 +29,17 @@ performance_tests <- function( if (!is.null(cypress_file) && !is.null(shinytest2_dir)) { message("Using the cypress file only") shinytest2_dir <- NULL - - if (length(cypress_file) == 1) - cypress_file <- rep(cypress_file, n_commits) - if (length(cypress_file) != n_commits) - stop("You must provide 1 or {n_commits} paths for cypress_file") - } else { - if (length(shinytest2_dir) == 1) - shinytest2_dir <- rep(shinytest2_dir, n_commits) - if (length(shinytest2_dir) != n_commits) - stop("You must provide 1 or {n_commits} paths for shinytest2_dir") } type <- ifelse(!is.null(cypress_file), "cypress", "shinytest2") + obj_name <- ifelse(type == "cypress", "cypress_file", "shinytest2_dir") - # getting the current branch - current_branch <- get_commit_hash() + if (length(get(obj_name)) == 1) + assign(obj_name, rep(cypress_file, n_commits)) + if (length(get(obj_name)) != n_commits) + stop("You must provide 1 or {n_commits} paths for {obj_name}") + # run tests if (type == "cypress") { perf_list <- ptest_cypress( commit_list = commit_list, From 592dd1e757c3a0443d0e79504789aadedd6d9eaa Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 20 Oct 2022 18:52:39 +0200 Subject: [PATCH 026/225] updating documentation --- man/create_shinytest2_structure.Rd | 4 ++-- man/move_shinytest2_tests.Rd | 16 ++++++++++++++++ man/run_shinytest2_ptest.Rd | 7 ++++--- man/test_app_custom.Rd | 16 ++++++++++++++++ 4 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 man/move_shinytest2_tests.Rd create mode 100644 man/test_app_custom.Rd diff --git a/man/create_shinytest2_structure.Rd b/man/create_shinytest2_structure.Rd index 82a25ba..d3787c6 100644 --- a/man/create_shinytest2_structure.Rd +++ b/man/create_shinytest2_structure.Rd @@ -4,10 +4,10 @@ \alias{create_shinytest2_structure} \title{Create a temporary directory to store everything needed by shinytest2} \usage{ -create_shinytest2_structure(shinytest2_dir) +create_shinytest2_structure(app_dir) } \arguments{ -\item{shinytest2_dir}{The path to the shinytest2 tests} +\item{app_dir}{} } \description{ Create a temporary directory to store everything needed by shinytest2 diff --git a/man/move_shinytest2_tests.Rd b/man/move_shinytest2_tests.Rd new file mode 100644 index 0000000..3aa5b3b --- /dev/null +++ b/man/move_shinytest2_tests.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils_shinytest2.R +\name{move_shinytest2_tests} +\alias{move_shinytest2_tests} +\title{Move tests to a temporary folder} +\usage{ +move_shinytest2_tests(project_path, shinytest2_dir) +} +\arguments{ +\item{project_path}{The path to the project} + +\item{shinytest2_dir}{The directory with tests recorded by shinytest2} +} +\description{ +Move tests to a temporary folder +} diff --git a/man/run_shinytest2_ptest.Rd b/man/run_shinytest2_ptest.Rd index 5e73e48..be4adad 100644 --- a/man/run_shinytest2_ptest.Rd +++ b/man/run_shinytest2_ptest.Rd @@ -4,15 +4,16 @@ \alias{run_shinytest2_ptest} \title{Run the performance test based on a single commit using shinytest2} \usage{ -run_shinytest2_ptest(commit, app_dir, project_path, debug) +run_shinytest2_ptest(commit, project_path, app_dir, shinytest2_dir, debug) } \arguments{ \item{commit}{A commit hash code or a branch's name} +\item{project_path}{The path to the project} + \item{app_dir}{The path to the application root} -\item{project_path}{The path to the project with all needed -packages installed} +\item{shinytest2_dir}{The directory with tests recorded by shinytest2} \item{debug}{Logical. TRUE to display all the system messages on runtime} } diff --git a/man/test_app_custom.Rd b/man/test_app_custom.Rd new file mode 100644 index 0000000..9641962 --- /dev/null +++ b/man/test_app_custom.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/test_app.R +\name{test_app_custom} +\alias{test_app_custom} +\title{Addapted from shinytest2::test_app} +\usage{ +test_app_custom( + app_dir = missing_arg(), + tests_dir = fs::path(app_dir, "tests", "testthat"), + ..., + check_setup = TRUE +) +} +\description{ +Addapted from shinytest2::test_app +} From 153da9e4520cf5699c504f21fa3b630557cb3046 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 20 Oct 2022 19:08:17 +0200 Subject: [PATCH 027/225] Fixing issues with linter and devtools::check --- R/utils_shinytest2.R | 2 +- man/create_shinytest2_structure.Rd | 2 +- man/test_app_custom.Rd | 16 ---------------- 3 files changed, 2 insertions(+), 18 deletions(-) delete mode 100644 man/test_app_custom.Rd diff --git a/R/utils_shinytest2.R b/R/utils_shinytest2.R index 8aff733..b9bf9ab 100644 --- a/R/utils_shinytest2.R +++ b/R/utils_shinytest2.R @@ -1,6 +1,6 @@ #' @title Create a temporary directory to store everything needed by shinytest2 #' -#' @param app_dir +#' @param app_dir The path to the application root #' #' @importFrom glue glue create_shinytest2_structure <- function(app_dir) { diff --git a/man/create_shinytest2_structure.Rd b/man/create_shinytest2_structure.Rd index d3787c6..965adf1 100644 --- a/man/create_shinytest2_structure.Rd +++ b/man/create_shinytest2_structure.Rd @@ -7,7 +7,7 @@ create_shinytest2_structure(app_dir) } \arguments{ -\item{app_dir}{} +\item{app_dir}{The path to the application root} } \description{ Create a temporary directory to store everything needed by shinytest2 diff --git a/man/test_app_custom.Rd b/man/test_app_custom.Rd deleted file mode 100644 index 9641962..0000000 --- a/man/test_app_custom.Rd +++ /dev/null @@ -1,16 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/test_app.R -\name{test_app_custom} -\alias{test_app_custom} -\title{Addapted from shinytest2::test_app} -\usage{ -test_app_custom( - app_dir = missing_arg(), - tests_dir = fs::path(app_dir, "tests", "testthat"), - ..., - check_setup = TRUE -) -} -\description{ -Addapted from shinytest2::test_app -} From de05befd196e63d2b0f3e92c289da52d93147e66 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 20 Oct 2022 21:13:10 +0200 Subject: [PATCH 028/225] Checking possibility to run tests using github actions --- .github/workflows/tests.yml | 43 +++++++++++++++++++ .../tests/app/fake_folder/tests/testthat.R | 1 + .../app/fake_folder/tests/testthat/setup.R | 2 + .../tests/testthat/test-shinytest2.R | 21 +++++++++ .github/workflows/tests/app/global.R | 2 + .github/workflows/tests/app/server.R | 39 +++++++++++++++++ .github/workflows/tests/app/tests/testthat.R | 1 + .../tests/app/tests/testthat/setup.R | 2 + .../app/tests/testthat/test-shinytest2.R | 21 +++++++++ .github/workflows/tests/app/ui.R | 26 +++++++++++ .github/workflows/tests/cypress_tests1.js | 30 +++++++++++++ .github/workflows/tests/cypress_tests2.js | 30 +++++++++++++ .github/workflows/tests/run_tests.R | 42 ++++++++++++++++++ .github/workflows/tests/setting_branhces.sh | 33 ++++++++++++++ 14 files changed, 293 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 .github/workflows/tests/app/fake_folder/tests/testthat.R create mode 100644 .github/workflows/tests/app/fake_folder/tests/testthat/setup.R create mode 100644 .github/workflows/tests/app/fake_folder/tests/testthat/test-shinytest2.R create mode 100644 .github/workflows/tests/app/global.R create mode 100644 .github/workflows/tests/app/server.R create mode 100644 .github/workflows/tests/app/tests/testthat.R create mode 100644 .github/workflows/tests/app/tests/testthat/setup.R create mode 100644 .github/workflows/tests/app/tests/testthat/test-shinytest2.R create mode 100644 .github/workflows/tests/app/ui.R create mode 100644 .github/workflows/tests/cypress_tests1.js create mode 100644 .github/workflows/tests/cypress_tests2.js create mode 100644 .github/workflows/tests/run_tests.R create mode 100644 .github/workflows/tests/setting_branhces.sh diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..bd26e44 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,43 @@ +on: push + +name: package-usage-tests + +jobs: + main: + name: ${{ matrix.config.os }} (${{ matrix.config.r }}) + + runs-on: ${{ matrix.config.os }} + + timeout-minutes: 30 + + strategy: + fail-fast: false + matrix: + config: + - {os: ubuntu-22.04, r: 'release'} + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install R + uses: r-lib/actions/setup-r@v2 + with: + r-version: ${{ matrix.config.r }} + + - name: Install R package dependencies + uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: local::. # Necessary to avoid object usage linter errors. + + - name: Create app structure + if: always() + shell: git status # test + + - name: Check basic functionality + if: always() + shell: Rscript tests/run_tests.R cypress master,develop app/tests/cypress_tests1.js + + - name: Check it again for fun + if: always() + shell: Rscript tests/run_tests.R cypress master,develop app/tests/cypress_tests1.js diff --git a/.github/workflows/tests/app/fake_folder/tests/testthat.R b/.github/workflows/tests/app/fake_folder/tests/testthat.R new file mode 100644 index 0000000..7d25b5b --- /dev/null +++ b/.github/workflows/tests/app/fake_folder/tests/testthat.R @@ -0,0 +1 @@ +shinytest2::test_app() diff --git a/.github/workflows/tests/app/fake_folder/tests/testthat/setup.R b/.github/workflows/tests/app/fake_folder/tests/testthat/setup.R new file mode 100644 index 0000000..be65b4f --- /dev/null +++ b/.github/workflows/tests/app/fake_folder/tests/testthat/setup.R @@ -0,0 +1,2 @@ +# Load application support files into testing environment +shinytest2::load_app_env() diff --git a/.github/workflows/tests/app/fake_folder/tests/testthat/test-shinytest2.R b/.github/workflows/tests/app/fake_folder/tests/testthat/test-shinytest2.R new file mode 100644 index 0000000..7c504e7 --- /dev/null +++ b/.github/workflows/tests/app/fake_folder/tests/testthat/test-shinytest2.R @@ -0,0 +1,21 @@ +library(shinytest2) + +test_that("{shinytest2} recording: test1", { + app <- AppDriver$new(name = "test1", height = 975, width = 1619) + app$click("run1") + app$expect_values(output = "out1") +}) + + +test_that("{shinytest2} recording: test2", { + app <- AppDriver$new(name = "test2", height = 975, width = 1619) + app$click("run2") + app$expect_values(output = "out2") +}) + + +test_that("{shinytest2} recording: test3", { + app <- AppDriver$new(name = "test3", height = 975, width = 1619) + app$click("run3") + app$expect_values(output = "out3") +}) diff --git a/.github/workflows/tests/app/global.R b/.github/workflows/tests/app/global.R new file mode 100644 index 0000000..6e470a6 --- /dev/null +++ b/.github/workflows/tests/app/global.R @@ -0,0 +1,2 @@ +library(shiny) +library(shinycssloaders) diff --git a/.github/workflows/tests/app/server.R b/.github/workflows/tests/app/server.R new file mode 100644 index 0000000..12428e6 --- /dev/null +++ b/.github/workflows/tests/app/server.R @@ -0,0 +1,39 @@ +function(input, output, session) { + # Sys.sleep + react1 <- eventReactive(input$run1, { + out <- system.time( + Sys.sleep(6) + ) + + return(out[3]) + }) + + react2 <- eventReactive(input$run2, { + out <- system.time( + Sys.sleep(3) + ) + + return(out[3]) + }) + + react3 <- eventReactive(input$run3, { + out <- system.time( + Sys.sleep(1) + ) + + return(out[1]) + }) + + # outputs + output$out1 <- renderUI({ + tags$span(round(react1()), style = "font-size: 500px;") + }) + + output$out2 <- renderUI({ + tags$span(round(react2()), style = "font-size: 500px;") + }) + + output$out3 <- renderUI({ + tags$span(round(react3()), style = "font-size: 500px;") + }) +} diff --git a/.github/workflows/tests/app/tests/testthat.R b/.github/workflows/tests/app/tests/testthat.R new file mode 100644 index 0000000..7d25b5b --- /dev/null +++ b/.github/workflows/tests/app/tests/testthat.R @@ -0,0 +1 @@ +shinytest2::test_app() diff --git a/.github/workflows/tests/app/tests/testthat/setup.R b/.github/workflows/tests/app/tests/testthat/setup.R new file mode 100644 index 0000000..be65b4f --- /dev/null +++ b/.github/workflows/tests/app/tests/testthat/setup.R @@ -0,0 +1,2 @@ +# Load application support files into testing environment +shinytest2::load_app_env() diff --git a/.github/workflows/tests/app/tests/testthat/test-shinytest2.R b/.github/workflows/tests/app/tests/testthat/test-shinytest2.R new file mode 100644 index 0000000..7c504e7 --- /dev/null +++ b/.github/workflows/tests/app/tests/testthat/test-shinytest2.R @@ -0,0 +1,21 @@ +library(shinytest2) + +test_that("{shinytest2} recording: test1", { + app <- AppDriver$new(name = "test1", height = 975, width = 1619) + app$click("run1") + app$expect_values(output = "out1") +}) + + +test_that("{shinytest2} recording: test2", { + app <- AppDriver$new(name = "test2", height = 975, width = 1619) + app$click("run2") + app$expect_values(output = "out2") +}) + + +test_that("{shinytest2} recording: test3", { + app <- AppDriver$new(name = "test3", height = 975, width = 1619) + app$click("run3") + app$expect_values(output = "out3") +}) diff --git a/.github/workflows/tests/app/ui.R b/.github/workflows/tests/app/ui.R new file mode 100644 index 0000000..2dd025d --- /dev/null +++ b/.github/workflows/tests/app/ui.R @@ -0,0 +1,26 @@ +function() { + bootstrapPage( + tags$h1("Measuring time in different commits"), + column( + width = 4, + actionButton(inputId = "run1", label = "Run 1"), + withSpinner( + uiOutput(outputId = "out1") + ) + ), + column( + width = 4, + actionButton(inputId = "run2", label = "Run 2"), + withSpinner( + uiOutput(outputId = "out2") + ) + ), + column( + width = 4, + actionButton(inputId = "run3", label = "Run 3"), + withSpinner( + uiOutput(outputId = "out3") + ) + ) + ) +} diff --git a/.github/workflows/tests/cypress_tests1.js b/.github/workflows/tests/cypress_tests1.js new file mode 100644 index 0000000..3943a97 --- /dev/null +++ b/.github/workflows/tests/cypress_tests1.js @@ -0,0 +1,30 @@ +describe('Cypress test', () => { + // Test that the app starts at all + // Also it is needed to start other tests + it('The app starts', () => { + cy.visit('/'); + }); + + // Test how long it takes to wait for out1 + it('Out1 time elapsed', () => { + cy.get('#run1').click(); + cy.get('#out1', {timeout: 10000}).should('be.visible'); + }); + + // Test how long it takes to wait for out2 + it('Out2 time elapsed', () => { + cy.get('#run2').click(); + cy.get('#out2', {timeout: 10000}).should('be.visible'); + }); + + // Test how long it takes to wait for out3 + it('Out3 time elapsed', () => { + cy.get('#run3').click(); + cy.get('#out3', {timeout: 10000}).should('be.visible'); + }); + + // Test if we have a title + it('App has a title', () => { + cy.contains('Measuring time in different commits').should('be.visible'); + }); +}); diff --git a/.github/workflows/tests/cypress_tests2.js b/.github/workflows/tests/cypress_tests2.js new file mode 100644 index 0000000..3943a97 --- /dev/null +++ b/.github/workflows/tests/cypress_tests2.js @@ -0,0 +1,30 @@ +describe('Cypress test', () => { + // Test that the app starts at all + // Also it is needed to start other tests + it('The app starts', () => { + cy.visit('/'); + }); + + // Test how long it takes to wait for out1 + it('Out1 time elapsed', () => { + cy.get('#run1').click(); + cy.get('#out1', {timeout: 10000}).should('be.visible'); + }); + + // Test how long it takes to wait for out2 + it('Out2 time elapsed', () => { + cy.get('#run2').click(); + cy.get('#out2', {timeout: 10000}).should('be.visible'); + }); + + // Test how long it takes to wait for out3 + it('Out3 time elapsed', () => { + cy.get('#run3').click(); + cy.get('#out3', {timeout: 10000}).should('be.visible'); + }); + + // Test if we have a title + it('App has a title', () => { + cy.contains('Measuring time in different commits').should('be.visible'); + }); +}); diff --git a/.github/workflows/tests/run_tests.R b/.github/workflows/tests/run_tests.R new file mode 100644 index 0000000..4049a78 --- /dev/null +++ b/.github/workflows/tests/run_tests.R @@ -0,0 +1,42 @@ +#!/usr/bin/env Rscript +args <- commandArgs(trailingOnly = TRUE) +args <- strsplit(args, ",") + +stopifnot(T == F) + +# packages +library(shiny) +library(testthat) +library(shiny.performance) + +# commits to compare +type <- args[[1]] +commit_list <- args[[2]] +tests <- args[[3]] +# use_renv <- args[[4]] + +if (type == "cypress") { + # run performance check using Cypress + out <- shiny.performance::performance_tests( + commit_list = commit_list, + cypress_file = tests, + app_dir = "./app/", + # use_renv = use_renv, + port = 3333, + debug = FALSE + ) +} else { + # run performance check using shinytest2 + out <- shiny.performance::performance_tests( + commit_list = commit_list, + shinytest2_dir = tests, + app_dir = "./app/", + # use_renv = use_renv, + port = 3333, + debug = FALSE + ) +} + +# checks +stopifnot(length(out) == length(commit_list)) +stopifnot(nrow(out[[1]]) == 0) diff --git a/.github/workflows/tests/setting_branhces.sh b/.github/workflows/tests/setting_branhces.sh new file mode 100644 index 0000000..1bd1ad0 --- /dev/null +++ b/.github/workflows/tests/setting_branhces.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +# starting +git init + +# STANDARD FUNCTIONALITIES +## master +git add . +git commit -m "first commit" + +## develop +git checkout -b develop +git commit --allow-empty -m "dummy commit to change hash" + +# RENV FUNCTIONALITIES +## No renv at all +git branch renv_missing master +git checkout renv_missing +git commit --allow-empty -m "dummy commit to change hash" + +## Creating renv +git branch renv_shiny1 master +git checkout renv_shiny1 +R -e 'renv::init()' +git add . +git commit -m "renv active" + +## Downgrading shiny +git checkout -b renv_shiny2 +R -e 'renv::install("shiny@1.7.0")' +R -e 'renv::snapshot()' +git add . +git commit -m "downgrading shiny" From e621bd6efc62d5536185964741d05b10e2edfc8d Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 10:07:59 +0200 Subject: [PATCH 029/225] Fixing run file and adding the real code to tests.yml --- .github/workflows/tests.yml | 9 ++++++--- .github/workflows/tests/run_tests.R | 2 -- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bd26e44..e8dea7f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,12 +32,15 @@ jobs: - name: Create app structure if: always() - shell: git status # test + run: | + bash tests/setting_branches.sh - name: Check basic functionality if: always() - shell: Rscript tests/run_tests.R cypress master,develop app/tests/cypress_tests1.js + run: | + Rscript tests/run_tests.R cypress master,develop app/tests/cypress_tests1.js - name: Check it again for fun if: always() - shell: Rscript tests/run_tests.R cypress master,develop app/tests/cypress_tests1.js + run: | + Rscript tests/run_tests.R cypress master,develop app/tests/cypress_tests1.js diff --git a/.github/workflows/tests/run_tests.R b/.github/workflows/tests/run_tests.R index 4049a78..c2895c3 100644 --- a/.github/workflows/tests/run_tests.R +++ b/.github/workflows/tests/run_tests.R @@ -2,8 +2,6 @@ args <- commandArgs(trailingOnly = TRUE) args <- strsplit(args, ",") -stopifnot(T == F) - # packages library(shiny) library(testthat) From 7b6358f1c686257ff30ab6a4b661d98204dc7283 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 10:11:27 +0200 Subject: [PATCH 030/225] Fixing working directory --- .github/workflows/tests.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e8dea7f..0bff498 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,6 +2,10 @@ on: push name: package-usage-tests +defaults: + run: + working-directory: ./.github/workflows/tests/app + jobs: main: name: ${{ matrix.config.os }} (${{ matrix.config.r }}) @@ -33,14 +37,14 @@ jobs: - name: Create app structure if: always() run: | - bash tests/setting_branches.sh + bash ../setting_branches.sh - name: Check basic functionality if: always() run: | - Rscript tests/run_tests.R cypress master,develop app/tests/cypress_tests1.js + Rscript ../run_tests.R cypress master,develop app/tests/cypress_tests1.js - name: Check it again for fun if: always() run: | - Rscript tests/run_tests.R cypress master,develop app/tests/cypress_tests1.js + Rscript ../run_tests.R cypress master,develop app/tests/cypress_tests1.js From d43beba18c7a8832b44cee0e8d7d883ef8640e4f Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 10:16:19 +0200 Subject: [PATCH 031/225] Trying to fix working directory entry --- .github/workflows/tests.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0bff498..ffb4825 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,10 +2,6 @@ on: push name: package-usage-tests -defaults: - run: - working-directory: ./.github/workflows/tests/app - jobs: main: name: ${{ matrix.config.os }} (${{ matrix.config.r }}) @@ -36,15 +32,18 @@ jobs: - name: Create app structure if: always() + working-directory: ./.github/workflows/tests/app run: | - bash ../setting_branches.sh + bash ./../setting_branches.sh - name: Check basic functionality if: always() + working-directory: ./.github/workflows/tests/app run: | - Rscript ../run_tests.R cypress master,develop app/tests/cypress_tests1.js + Rscript ./../run_tests.R cypress master,develop app/tests/cypress_tests1.js - name: Check it again for fun if: always() + working-directory: ./.github/workflows/tests/app run: | - Rscript ../run_tests.R cypress master,develop app/tests/cypress_tests1.js + Rscript ./../run_tests.R cypress master,develop app/tests/cypress_tests1.js From bddc87a2bae0d029f2d4e7c5c34fd4b0dcfe39f8 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 10:21:02 +0200 Subject: [PATCH 032/225] Trying to understand the path logic --- .github/workflows/tests.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ffb4825..d689a44 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,6 +30,12 @@ jobs: with: extra-packages: local::. # Necessary to avoid object usage linter errors. + - name: Check the WD + if: always() + run: | + pwd + ls + - name: Create app structure if: always() working-directory: ./.github/workflows/tests/app From d8bd29692d3d765ebc3e58b0065615ca7d2799ed Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 10:23:28 +0200 Subject: [PATCH 033/225] Showing hidden files as well --- .github/workflows/tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d689a44..dc1ebde 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,8 +33,7 @@ jobs: - name: Check the WD if: always() run: | - pwd - ls + ls -a - name: Create app structure if: always() From 49db10621e5f3a5e6d6144f2eae638f84dce2f5b Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 10:24:25 +0200 Subject: [PATCH 034/225] Trying to see if the working directory really changed --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dc1ebde..a0ad26e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,6 +39,7 @@ jobs: if: always() working-directory: ./.github/workflows/tests/app run: | + ls -a bash ./../setting_branches.sh - name: Check basic functionality From 6fef096b359ab95e03125ba6fa5ac6469bcad5f7 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 10:29:39 +0200 Subject: [PATCH 035/225] Checking content in the parent folder --- .github/workflows/tests.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a0ad26e..91f5ebc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,16 +30,11 @@ jobs: with: extra-packages: local::. # Necessary to avoid object usage linter errors. - - name: Check the WD - if: always() - run: | - ls -a - - name: Create app structure if: always() working-directory: ./.github/workflows/tests/app run: | - ls -a + ls -a .. bash ./../setting_branches.sh - name: Check basic functionality From 18b790987472c9ff6a9521348803543202ca1ced Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 10:38:56 +0200 Subject: [PATCH 036/225] Fixing file names and adding renv tests after merging with develop --- .github/workflows/tests.yml | 13 +++++++++---- .github/workflows/tests/run_tests.R | 6 +++--- .../{setting_branhces.sh => setting_branches.sh} | 0 3 files changed, 12 insertions(+), 7 deletions(-) rename .github/workflows/tests/{setting_branhces.sh => setting_branches.sh} (100%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 91f5ebc..dddede7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,17 +34,22 @@ jobs: if: always() working-directory: ./.github/workflows/tests/app run: | - ls -a .. bash ./../setting_branches.sh - name: Check basic functionality if: always() working-directory: ./.github/workflows/tests/app run: | - Rscript ./../run_tests.R cypress master,develop app/tests/cypress_tests1.js + Rscript ./../run_tests.R cypress master,develop app/tests/cypress_tests1.js FALSE - - name: Check it again for fun + - name: Check if it fails when renv not present if: always() working-directory: ./.github/workflows/tests/app run: | - Rscript ./../run_tests.R cypress master,develop app/tests/cypress_tests1.js + Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,shiny2 app/tests/cypress_tests1.js FALSE + + - name: Check if it can handle renv + if: always() + working-directory: ./.github/workflows/tests/app + run: | + Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,shiny2 app/tests/cypress_tests1.js TRUE diff --git a/.github/workflows/tests/run_tests.R b/.github/workflows/tests/run_tests.R index c2895c3..d3ccddc 100644 --- a/.github/workflows/tests/run_tests.R +++ b/.github/workflows/tests/run_tests.R @@ -11,7 +11,7 @@ library(shiny.performance) type <- args[[1]] commit_list <- args[[2]] tests <- args[[3]] -# use_renv <- args[[4]] +use_renv <- args[[4]] if (type == "cypress") { # run performance check using Cypress @@ -19,7 +19,7 @@ if (type == "cypress") { commit_list = commit_list, cypress_file = tests, app_dir = "./app/", - # use_renv = use_renv, + use_renv = use_renv, port = 3333, debug = FALSE ) @@ -29,7 +29,7 @@ if (type == "cypress") { commit_list = commit_list, shinytest2_dir = tests, app_dir = "./app/", - # use_renv = use_renv, + use_renv = use_renv, port = 3333, debug = FALSE ) diff --git a/.github/workflows/tests/setting_branhces.sh b/.github/workflows/tests/setting_branches.sh similarity index 100% rename from .github/workflows/tests/setting_branhces.sh rename to .github/workflows/tests/setting_branches.sh From 02708bf3e97d6454d8cc28449b759349385b7a8a Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 11:09:26 +0200 Subject: [PATCH 037/225] Github setup and installing shiny.performance --- .github/workflows/tests.yml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dddede7..f9dbdd3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,6 +2,10 @@ on: push name: package-usage-tests +defaults: + run: + working-directory: ./.github/workflows/tests/app + jobs: main: name: ${{ matrix.config.os }} (${{ matrix.config.r }}) @@ -31,25 +35,24 @@ jobs: extra-packages: local::. # Necessary to avoid object usage linter errors. - name: Create app structure - if: always() - working-directory: ./.github/workflows/tests/app run: | + git config --local user.name "$GITHUB_ACTOR" + git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" bash ./../setting_branches.sh + - name: Install shiny.performance + run: | + R -e "install.packages('remotes')" + R -e "remotes::install_github(repo = 'Appsilon/experimental.performance', ref = 'develop')" + - name: Check basic functionality - if: always() - working-directory: ./.github/workflows/tests/app run: | Rscript ./../run_tests.R cypress master,develop app/tests/cypress_tests1.js FALSE - name: Check if it fails when renv not present - if: always() - working-directory: ./.github/workflows/tests/app run: | Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,shiny2 app/tests/cypress_tests1.js FALSE - name: Check if it can handle renv - if: always() - working-directory: ./.github/workflows/tests/app run: | Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,shiny2 app/tests/cypress_tests1.js TRUE From fb48fb01e1031f58f4e1c374eb6f484e3fc356ac Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 11:12:31 +0200 Subject: [PATCH 038/225] Adding credentials to bash file --- .github/workflows/tests.yml | 2 -- .github/workflows/tests/setting_branches.sh | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f9dbdd3..6c4b3c8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,8 +36,6 @@ jobs: - name: Create app structure run: | - git config --local user.name "$GITHUB_ACTOR" - git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" bash ./../setting_branches.sh - name: Install shiny.performance diff --git a/.github/workflows/tests/setting_branches.sh b/.github/workflows/tests/setting_branches.sh index 1bd1ad0..04436c3 100644 --- a/.github/workflows/tests/setting_branches.sh +++ b/.github/workflows/tests/setting_branches.sh @@ -3,6 +3,10 @@ # starting git init +# credentials +git config --local user.name "$GITHUB_ACTOR" +git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" + # STANDARD FUNCTIONALITIES ## master git add . From 445c6d761b05cabaa9bac8247a082dc2cc64fdda Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 11:21:52 +0200 Subject: [PATCH 039/225] Trying to clone repo instead of installing using remotes --- .github/workflows/tests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6c4b3c8..976b11e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,8 +40,10 @@ jobs: - name: Install shiny.performance run: | - R -e "install.packages('remotes')" - R -e "remotes::install_github(repo = 'Appsilon/experimental.performance', ref = 'develop')" + git clone https://github.com/Appsilon/experimental.performance.git + cd experimental.performance + R -q --vanilla -e 'devtools::install_local(path = ".", upgrade = "never")' + cd .. && rm -rf experimental.performance - name: Check basic functionality run: | From e36ab3681fadf1a4c6c4c0f3292839d2bb4b013e Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 13:38:37 +0200 Subject: [PATCH 040/225] Using GITHUB_TOKEN --- .github/workflows/tests.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 976b11e..051e92e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,6 +20,9 @@ jobs: config: - {os: ubuntu-22.04, r: 'release'} + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + steps: - name: Checkout repository uses: actions/checkout@v2 @@ -39,8 +42,9 @@ jobs: bash ./../setting_branches.sh - name: Install shiny.performance + if: ${{ !env.ACT }} run: | - git clone https://github.com/Appsilon/experimental.performance.git + git clone git@github.com:Appsilon/experimental.performance.git cd experimental.performance R -q --vanilla -e 'devtools::install_local(path = ".", upgrade = "never")' cd .. && rm -rf experimental.performance From d4ca293a3b6eb0eee2a120d87c6cfc23b192f583 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 13:57:30 +0200 Subject: [PATCH 041/225] Trying to build instead of installing --- .github/workflows/tests.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 051e92e..18da371 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,12 +42,10 @@ jobs: bash ./../setting_branches.sh - name: Install shiny.performance + working-directory: ../../../../ if: ${{ !env.ACT }} run: | - git clone git@github.com:Appsilon/experimental.performance.git - cd experimental.performance - R -q --vanilla -e 'devtools::install_local(path = ".", upgrade = "never")' - cd .. && rm -rf experimental.performance + R CMD build . - name: Check basic functionality run: | From 7f7ac38ed60e7dfacb986e919d667e5f7bc457d5 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 14:13:07 +0200 Subject: [PATCH 042/225] Testing in a different way --- .github/workflows/tests.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 18da371..251171d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,10 +42,9 @@ jobs: bash ./../setting_branches.sh - name: Install shiny.performance - working-directory: ../../../../ - if: ${{ !env.ACT }} run: | - R CMD build . + ls + R CMD build ../../../../. - name: Check basic functionality run: | From 8db4f94c3e7164d6ab0d6d70a0245b73a2abfed6 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 14:18:24 +0200 Subject: [PATCH 043/225] Installing package --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 251171d..e80e9b5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,8 +43,8 @@ jobs: - name: Install shiny.performance run: | - ls R CMD build ../../../../. + R CMD INSTALL shiny.performance_0.1.1.tar.gz - name: Check basic functionality run: | From e1825d6ff2236594fac0488fb959df548879b924 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 17:30:06 +0200 Subject: [PATCH 044/225] Avoiding tmp dir in github action --- .github/workflows/tests/setting_branches.sh | 6 ++++++ R/utils.R | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests/setting_branches.sh b/.github/workflows/tests/setting_branches.sh index 04436c3..a0abd19 100644 --- a/.github/workflows/tests/setting_branches.sh +++ b/.github/workflows/tests/setting_branches.sh @@ -1,5 +1,8 @@ #!/bin/sh +# global varuable for testing +export shiny_pergformance_run_mode=github_actions + # starting git init @@ -35,3 +38,6 @@ R -e 'renv::install("shiny@1.7.0")' R -e 'renv::snapshot()' git add . git commit -m "downgrading shiny" + +## Switching back to master +git checkout master diff --git a/R/utils.R b/R/utils.R index 3f692fa..67a7dd1 100644 --- a/R/utils.R +++ b/R/utils.R @@ -3,7 +3,12 @@ #' @param shinytest2_dir The path to the shinytest2 tests create_shinytest2_structure <- function(shinytest2_dir) { # temp dir to run the tests - dir_tests <- tempdir() + if (Sys.getenv("shiny_pergformance_run_mode") == "github_actions") { + dir_tests <- tempdir() + } else { + dir.create("github_actions") + dir_tests <- "github_actions" + } # copy everything to the temporary directory system(glue("cp -r {shinytest2_dir} {dir_tests}")) @@ -22,7 +27,12 @@ create_shinytest2_structure <- function(shinytest2_dir) { #' @importFrom jsonlite write_json create_cypress_structure <- function(app_dir, port, debug) { # temp dir to run the tests - dir_tests <- tempdir() + if (Sys.getenv("shiny_pergformance_run_mode") == "github_actions") { + dir_tests <- tempdir() + } else { + dir.create("github_actions") + dir_tests <- "github_actions" + } # node path node_path <- file.path(dir_tests, "node") From 48f2f3040fd4a5a41b047d97cd94da05eb2a7744 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 17:47:53 +0200 Subject: [PATCH 045/225] Adding shinytest2 tests --- .github/workflows/tests.yml | 18 +++++++++++++++--- R/utils.R | 4 ++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e80e9b5..4082ca8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,14 +46,26 @@ jobs: R CMD build ../../../../. R CMD INSTALL shiny.performance_0.1.1.tar.gz - - name: Check basic functionality + - name: Check basic functionality - Cypress run: | Rscript ./../run_tests.R cypress master,develop app/tests/cypress_tests1.js FALSE - - name: Check if it fails when renv not present + - name: Check basic functionality - shinytest2 + run: | + Rscript ./../run_tests.R shinytest2 master,develop tests FALSE + + - name: Check if it fails when renv not present - Cypress run: | Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,shiny2 app/tests/cypress_tests1.js FALSE - - name: Check if it can handle renv + - name: Check if it fails when renv not present - shinytest2 + run: | + Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,shiny2 tests FALSE + + - name: Check if it can handle renv - Cypress run: | Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,shiny2 app/tests/cypress_tests1.js TRUE + + - name: Check if it can handle renv - shinytest2 + run: | + Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,shiny2 tests TRUE diff --git a/R/utils.R b/R/utils.R index 67a7dd1..a71a787 100644 --- a/R/utils.R +++ b/R/utils.R @@ -3,7 +3,7 @@ #' @param shinytest2_dir The path to the shinytest2 tests create_shinytest2_structure <- function(shinytest2_dir) { # temp dir to run the tests - if (Sys.getenv("shiny_pergformance_run_mode") == "github_actions") { + if (Sys.getenv("shiny_pergformance_run_mode") != "github_actions") { dir_tests <- tempdir() } else { dir.create("github_actions") @@ -27,7 +27,7 @@ create_shinytest2_structure <- function(shinytest2_dir) { #' @importFrom jsonlite write_json create_cypress_structure <- function(app_dir, port, debug) { # temp dir to run the tests - if (Sys.getenv("shiny_pergformance_run_mode") == "github_actions") { + if (Sys.getenv("shiny_pergformance_run_mode") != "github_actions") { dir_tests <- tempdir() } else { dir.create("github_actions") From e12076893d54d38e7968ccbc30d5d07db303c6cb Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 17:54:19 +0200 Subject: [PATCH 046/225] Testing if the global variable is correct --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4082ca8..48fa222 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,6 +40,7 @@ jobs: - name: Create app structure run: | bash ./../setting_branches.sh + echo $shiny_pergformance_run_mode - name: Install shiny.performance run: | From 3347f398196b3398d60e9575f6d0d4cf8e3b091d Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 17:59:13 +0200 Subject: [PATCH 047/225] Changing env position --- .github/workflows/tests.yml | 4 +++- .github/workflows/tests/setting_branches.sh | 3 --- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 48fa222..ffc250f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,6 +6,9 @@ defaults: run: working-directory: ./.github/workflows/tests/app +env: + shiny_pergformance_run_mode: github_actions + jobs: main: name: ${{ matrix.config.os }} (${{ matrix.config.r }}) @@ -40,7 +43,6 @@ jobs: - name: Create app structure run: | bash ./../setting_branches.sh - echo $shiny_pergformance_run_mode - name: Install shiny.performance run: | diff --git a/.github/workflows/tests/setting_branches.sh b/.github/workflows/tests/setting_branches.sh index a0abd19..ff344ed 100644 --- a/.github/workflows/tests/setting_branches.sh +++ b/.github/workflows/tests/setting_branches.sh @@ -1,8 +1,5 @@ #!/bin/sh -# global varuable for testing -export shiny_pergformance_run_mode=github_actions - # starting git init From aec1328be08ed5c569efcab78e88df7bc8b0a3ea Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 18:11:09 +0200 Subject: [PATCH 048/225] Just to read the warnings --- R/utils.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/utils.R b/R/utils.R index a71a787..f421c78 100644 --- a/R/utils.R +++ b/R/utils.R @@ -45,7 +45,7 @@ create_cypress_structure <- function(app_dir, port, debug) { plugins_path <- file.path(cypress_path, "plugins") # creating paths - dir.create(path = node_path, showWarnings = FALSE) + dir.create(path = node_path, showWarnings = TRUE) dir.create(path = tests_path, showWarnings = FALSE) dir.create(path = cypress_path, showWarnings = FALSE) dir.create(path = integration_path, showWarnings = FALSE) From 35c08c4d9dce099cec4e08c94578ea1926bf1a86 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 18:42:59 +0200 Subject: [PATCH 049/225] testing if path exists --- R/utils.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/utils.R b/R/utils.R index f421c78..146a81f 100644 --- a/R/utils.R +++ b/R/utils.R @@ -45,7 +45,8 @@ create_cypress_structure <- function(app_dir, port, debug) { plugins_path <- file.path(cypress_path, "plugins") # creating paths - dir.create(path = node_path, showWarnings = TRUE) + dir.create(path = node_path, showWarnings = FALSE) + file.exists(node_path) dir.create(path = tests_path, showWarnings = FALSE) dir.create(path = cypress_path, showWarnings = FALSE) dir.create(path = integration_path, showWarnings = FALSE) From 3ab3a7f505064593e6a6c33f8ce7ebda95242d1c Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 18:55:52 +0200 Subject: [PATCH 050/225] Trying shinytest2 first --- .github/workflows/tests.yml | 8 ++++---- R/utils.R | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ffc250f..2c85957 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,14 +49,14 @@ jobs: R CMD build ../../../../. R CMD INSTALL shiny.performance_0.1.1.tar.gz - - name: Check basic functionality - Cypress - run: | - Rscript ./../run_tests.R cypress master,develop app/tests/cypress_tests1.js FALSE - - name: Check basic functionality - shinytest2 run: | Rscript ./../run_tests.R shinytest2 master,develop tests FALSE + - name: Check basic functionality - Cypress + run: | + Rscript ./../run_tests.R cypress master,develop app/tests/cypress_tests1.js FALSE + - name: Check if it fails when renv not present - Cypress run: | Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,shiny2 app/tests/cypress_tests1.js FALSE diff --git a/R/utils.R b/R/utils.R index 146a81f..a71a787 100644 --- a/R/utils.R +++ b/R/utils.R @@ -46,7 +46,6 @@ create_cypress_structure <- function(app_dir, port, debug) { # creating paths dir.create(path = node_path, showWarnings = FALSE) - file.exists(node_path) dir.create(path = tests_path, showWarnings = FALSE) dir.create(path = cypress_path, showWarnings = FALSE) dir.create(path = integration_path, showWarnings = FALSE) From 52d7c7a615dc383adcfba6b10cb1904d6b6e117d Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 19:27:56 +0200 Subject: [PATCH 051/225] Bringing back the correct order --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2c85957..ffc250f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,14 +49,14 @@ jobs: R CMD build ../../../../. R CMD INSTALL shiny.performance_0.1.1.tar.gz - - name: Check basic functionality - shinytest2 - run: | - Rscript ./../run_tests.R shinytest2 master,develop tests FALSE - - name: Check basic functionality - Cypress run: | Rscript ./../run_tests.R cypress master,develop app/tests/cypress_tests1.js FALSE + - name: Check basic functionality - shinytest2 + run: | + Rscript ./../run_tests.R shinytest2 master,develop tests FALSE + - name: Check if it fails when renv not present - Cypress run: | Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,shiny2 app/tests/cypress_tests1.js FALSE From 684a1b64da65238696a257775866f4c25bbb978a Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 21:09:15 +0200 Subject: [PATCH 052/225] Missing parameters --- R/tests_cypress.R | 2 ++ R/tests_shinytest2.R | 2 ++ 2 files changed, 4 insertions(+) diff --git a/R/tests_cypress.R b/R/tests_cypress.R index be482b7..623c515 100644 --- a/R/tests_cypress.R +++ b/R/tests_cypress.R @@ -40,6 +40,8 @@ ptest_cypress <- function( cypress_file, FUN = run_cypress_ptest, project_path = project_path, + use_renv = use_renv, + renv_prompt = renv_prompt, debug = debug, SIMPLIFY = FALSE ) diff --git a/R/tests_shinytest2.R b/R/tests_shinytest2.R index b949da0..3b707f9 100644 --- a/R/tests_shinytest2.R +++ b/R/tests_shinytest2.R @@ -35,6 +35,8 @@ ptest_shinytest2 <- function( FUN = run_shinytest2_ptest, app_dir = app_dir, project_path = project_path, + use_renv = use_renv, + renv_prompt = renv_prompt, debug = debug, SIMPLIFY = FALSE ) From ada2f957623c80c6e5c49a97578896b99cb3c296 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 21:09:27 +0200 Subject: [PATCH 053/225] small issues with merge --- R/performance_tests.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/performance_tests.R b/R/performance_tests.R index 92e6d38..3ac008f 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -41,7 +41,7 @@ performance_tests <- function( obj_name <- ifelse(type == "cypress", "cypress_file", "shinytest2_dir") if (length(get(obj_name)) == 1) - assign(obj_name, rep(cypress_file, n_commits)) + assign(obj_name, rep(get(obj_name), n_commits)) if (length(get(obj_name)) != n_commits) stop("You must provide 1 or {n_commits} paths for {obj_name}") @@ -63,7 +63,7 @@ performance_tests <- function( app_dir, use_renv = use_renv, renv_prompt = renv_prompt, - debug + debug = debug ) } From a1dd201e4be9dd1d3ba8c9088ebf27d5711f7c21 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 21:17:46 +0200 Subject: [PATCH 054/225] Removing warning message --- .github/workflows/tests/setting_branches.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests/setting_branches.sh b/.github/workflows/tests/setting_branches.sh index ff344ed..e15d555 100644 --- a/.github/workflows/tests/setting_branches.sh +++ b/.github/workflows/tests/setting_branches.sh @@ -2,6 +2,7 @@ # starting git init +git config --global advice.detachedHead false # credentials git config --local user.name "$GITHUB_ACTOR" From 62a628dbe7f990972f2ff8b5eb680674e5549375 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 21 Oct 2022 21:37:53 +0200 Subject: [PATCH 055/225] Fresh tentative after testing locally --- .github/workflows/tests.yml | 12 ++++++------ .github/workflows/tests/run_tests.R | 7 ++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ffc250f..8a2a7d7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -51,24 +51,24 @@ jobs: - name: Check basic functionality - Cypress run: | - Rscript ./../run_tests.R cypress master,develop app/tests/cypress_tests1.js FALSE + Rscript ../run_tests.R cypress master,develop ../cypress_tests1.js FALSE - name: Check basic functionality - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 master,develop tests FALSE + Rscript ../run_tests.R shinytest2 master,develop tests/ FALSE - name: Check if it fails when renv not present - Cypress run: | - Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,shiny2 app/tests/cypress_tests1.js FALSE + Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 ../cypress_tests1.js FALSE - name: Check if it fails when renv not present - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,shiny2 tests FALSE + Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests FALSE - name: Check if it can handle renv - Cypress run: | - Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,shiny2 app/tests/cypress_tests1.js TRUE + Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 ../cypress_tests1.js TRUE - name: Check if it can handle renv - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,shiny2 tests TRUE + Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests TRUE diff --git a/.github/workflows/tests/run_tests.R b/.github/workflows/tests/run_tests.R index d3ccddc..31c798f 100644 --- a/.github/workflows/tests/run_tests.R +++ b/.github/workflows/tests/run_tests.R @@ -18,8 +18,9 @@ if (type == "cypress") { out <- shiny.performance::performance_tests( commit_list = commit_list, cypress_file = tests, - app_dir = "./app/", + app_dir = getwd(), use_renv = use_renv, + renv_prompt = FALSE, port = 3333, debug = FALSE ) @@ -28,8 +29,9 @@ if (type == "cypress") { out <- shiny.performance::performance_tests( commit_list = commit_list, shinytest2_dir = tests, - app_dir = "./app/", + app_dir = getwd(), use_renv = use_renv, + renv_prompt = FALSE, port = 3333, debug = FALSE ) @@ -37,4 +39,3 @@ if (type == "cypress") { # checks stopifnot(length(out) == length(commit_list)) -stopifnot(nrow(out[[1]]) == 0) From 0d0856d69db1ffd5ebe3cee9cc5252ea1df8096e Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 24 Oct 2022 09:38:49 +0200 Subject: [PATCH 056/225] Adding debug options in the checkout and checkout_files functions --- R/performance_tests.R | 14 ++++++------- R/utils.R | 49 +++++++++++++++++++++++++++---------------- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/R/performance_tests.R b/R/performance_tests.R index ebc1ee2..ccdb7ba 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -68,7 +68,7 @@ performance_tests <- function( }, finally = { # Restore initital setup - checkout(branch = current_branch) + checkout(branch = current_branch, debug = debug) message(glue("Switched back to {current_branch}")) if (use_renv) restore_env(branch = current_branch, renv_prompt = renv_prompt) @@ -104,7 +104,7 @@ performance_tests <- function( }, finally = { # Restore initital setup - checkout(branch = current_branch) + checkout(branch = current_branch, debug = debug) message(glue("Switched back to {current_branch}")) if (use_renv) restore_env(branch = current_branch, renv_prompt = renv_prompt) @@ -142,7 +142,7 @@ run_cypress_ptest <- function(commit, project_path, cypress_file, use_renv, renv txt_file <- files$txt_file # checkout to the desired commit - checkout(branch = commit) + checkout(branch = commit, debug = debug) date <- get_commit_date(branch = commit) message(glue("Switched to {commit}")) @@ -154,7 +154,7 @@ run_cypress_ptest <- function(commit, project_path, cypress_file, use_renv, renv "cd {project_path}; ", "set -eu; exec yarn --cwd node performance-test" ) - system(command, ignore.stdout = !debug, ignore.stderr = !debug) + system(command = command, ignore.stdout = !debug, ignore.stderr = !debug) # read the file saved by cypress perf_file <- read.table(file = txt_file, header = FALSE, sep = ";") @@ -165,7 +165,7 @@ run_cypress_ptest <- function(commit, project_path, cypress_file, use_renv, renv unlink(x = c(js_file, txt_file)) # removing anything new in the github repo - checkout_files() + checkout_files(debug = debug) # return times return(perf_file) @@ -188,7 +188,7 @@ run_cypress_ptest <- function(commit, project_path, cypress_file, use_renv, renv #' @export run_shinytest2_ptest <- function(commit, app_dir, project_path, use_renv, renv_prompt, debug) { # checkout to the desired commit - checkout(branch = commit) + checkout(branch = commit, debug = debug) date <- get_commit_date(branch = commit) message(glue("Switched to {commit}")) @@ -216,7 +216,7 @@ run_shinytest2_ptest <- function(commit, app_dir, project_path, use_renv, renv_p colnames(perf_file) <- c("date", "test_name", "duration_ms") # removing anything new in the github repo - checkout_files() + checkout_files(debug = debug) # return times return(perf_file) diff --git a/R/utils.R b/R/utils.R index 3f692fa..6f08375 100644 --- a/R/utils.R +++ b/R/utils.R @@ -6,7 +6,7 @@ create_shinytest2_structure <- function(shinytest2_dir) { dir_tests <- tempdir() # copy everything to the temporary directory - system(glue("cp -r {shinytest2_dir} {dir_tests}")) + system(command = glue("cp -r {shinytest2_dir} {dir_tests}")) # returning the project folder message(glue("Structure created at {dir_tests}")) @@ -43,7 +43,7 @@ create_cypress_structure <- function(app_dir, port, debug) { # create a path root linked to the main directory app symlink_cmd <- glue("cd {dir_tests}; ln -s {app_dir} {root_path}") - system(symlink_cmd) + system(command = symlink_cmd) # create the packages.json file json_txt <- create_node_list(tests_path = tests_path, port = port) @@ -52,7 +52,11 @@ create_cypress_structure <- function(app_dir, port, debug) { # install everything that is needed install_deps <- glue("yarn --cwd {node_path}") - system(install_deps, ignore.stdout = !debug, ignore.stderr = !debug) + system( + command = install_deps, + ignore.stdout = !debug, + ignore.stderr = !debug + ) # creating cypress plugin file js_txt <- create_cypress_plugins() @@ -198,7 +202,7 @@ add_sendtime2js <- function(js_file, txt_file) { #' @importFrom glue glue get_commit_date <- function(branch) { date <- system( - glue("git show -s --format=%ci {branch}"), + command = glue("git show -s --format=%ci {branch}"), intern = TRUE ) date <- as.POSIXct(date[1]) @@ -212,7 +216,7 @@ get_commit_date <- function(branch) { get_commit_hash <- function() { hash <- system("git show -s --format=%H", intern = TRUE)[1] branch <- system( - glue("git branch --contains {hash}"), + command = glue("git branch --contains {hash}"), intern = TRUE ) @@ -241,8 +245,28 @@ get_commit_hash <- function() { #' #' @description Checkout anything created by the app. It prevents errors when #' changing branches -checkout_files <- function() { - system("git checkout .") +#' +#' @param debug Logical. TRUE to display all the system messages on runtime +checkout_files <- function(debug) { + system( + command = "git checkout .", + ignore.stdout = !debug, + ignore.stderr = !debug + ) +} + +#' @title Checkout GitHub branch +#' +#' @description checkout and go to a different branch +#' +#' @param branch Commit hash code or branch name +#' @param debug Logical. TRUE to display all the system messages on runtime +checkout <- function(branch, debug) { + system( + command = glue("git checkout {branch}"), + ignore.stdout = !debug, + ignore.stderr = !debug + ) } #' @title Check and restore renv @@ -268,14 +292,3 @@ restore_env <- function(branch, renv_prompt) { } ) } - -#' @title Checkout GitHub branch -#' -#' @description checkout and go to a different branch -#' -#' @param branch Commit hash code or branch name -checkout <- function(branch) { - system( - glue("git checkout {branch}") - ) -} From b0b4c3b5b654be4eab2eee6ae06b3b01534eb3a7 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 24 Oct 2022 09:39:10 +0200 Subject: [PATCH 057/225] Updating documentation --- man/checkout.Rd | 4 +++- man/checkout_files.Rd | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/man/checkout.Rd b/man/checkout.Rd index 0c38c46..7e98bb5 100644 --- a/man/checkout.Rd +++ b/man/checkout.Rd @@ -4,10 +4,12 @@ \alias{checkout} \title{Checkout GitHub branch} \usage{ -checkout(branch) +checkout(branch, debug) } \arguments{ \item{branch}{Commit hash code or branch name} + +\item{debug}{Logical. TRUE to display all the system messages on runtime} } \description{ checkout and go to a different branch diff --git a/man/checkout_files.Rd b/man/checkout_files.Rd index 3811aa9..d427219 100644 --- a/man/checkout_files.Rd +++ b/man/checkout_files.Rd @@ -4,7 +4,10 @@ \alias{checkout_files} \title{Checkout GitHub files} \usage{ -checkout_files() +checkout_files(debug) +} +\arguments{ +\item{debug}{Logical. TRUE to display all the system messages on runtime} } \description{ Checkout anything created by the app. It prevents errors when From 56cbe59133ed3b14140b3ead29d366afbd485ede Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 24 Oct 2022 10:25:26 +0200 Subject: [PATCH 058/225] Check for uncommitted files --- R/performance_tests.R | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/R/performance_tests.R b/R/performance_tests.R index ebc1ee2..fd3f889 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -38,6 +38,9 @@ performance_tests <- function( # getting the current branch current_branch <- get_commit_hash() + # check if the repo is ready for running the checks + check_uncommitted_files() + if (type == "cypress") { # creating the structure project_path <- create_cypress_structure( @@ -221,3 +224,14 @@ run_shinytest2_ptest <- function(commit, app_dir, project_path, use_renv, renv_p # return times return(perf_file) } + +check_uncommitted_files <- function() { + changes <- system("git status --porcelain", intern = TRUE) + + if (length(changes) != 0) { + system("git status -uno") + stop("You have uncommitted files. Please resolve it before running the performance checks.") + } else { + return(TRUE) + } +} From 65bf4b8b27a743f4f9545120ace7fd3c60ad2b10 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 24 Oct 2022 10:26:49 +0200 Subject: [PATCH 059/225] Moving function to utils --- R/performance_tests.R | 11 ----------- R/utils.R | 12 ++++++++++++ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/R/performance_tests.R b/R/performance_tests.R index fd3f889..9708b9f 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -224,14 +224,3 @@ run_shinytest2_ptest <- function(commit, app_dir, project_path, use_renv, renv_p # return times return(perf_file) } - -check_uncommitted_files <- function() { - changes <- system("git status --porcelain", intern = TRUE) - - if (length(changes) != 0) { - system("git status -uno") - stop("You have uncommitted files. Please resolve it before running the performance checks.") - } else { - return(TRUE) - } -} diff --git a/R/utils.R b/R/utils.R index 3f692fa..8139dbd 100644 --- a/R/utils.R +++ b/R/utils.R @@ -279,3 +279,15 @@ checkout <- function(branch) { glue("git checkout {branch}") ) } + +#' @title Check for uncommitted files +check_uncommitted_files <- function() { + changes <- system("git status --porcelain", intern = TRUE) + + if (length(changes) != 0) { + system("git status -uno") + stop("You have uncommitted files. Please resolve it before running the performance checks.") + } else { + return(TRUE) + } +} From 16bc11a0cab59369fe1c5ad255c8ccdb0af26685 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 24 Oct 2022 11:31:12 +0200 Subject: [PATCH 060/225] Improving error message --- R/utils.R | 2 +- man/check_uncommitted_files.Rd | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 man/check_uncommitted_files.Rd diff --git a/R/utils.R b/R/utils.R index 30e937d..cfe7332 100644 --- a/R/utils.R +++ b/R/utils.R @@ -91,7 +91,7 @@ check_uncommitted_files <- function() { changes <- system("git status --porcelain", intern = TRUE) if (length(changes) != 0) { - system("git status -uno") + system("git status -u") stop("You have uncommitted files. Please resolve it before running the performance checks.") } else { return(TRUE) diff --git a/man/check_uncommitted_files.Rd b/man/check_uncommitted_files.Rd new file mode 100644 index 0000000..5ef9786 --- /dev/null +++ b/man/check_uncommitted_files.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{check_uncommitted_files} +\alias{check_uncommitted_files} +\title{Check for uncommitted files} +\usage{ +check_uncommitted_files() +} +\description{ +Check for uncommitted files +} From 4083bc3b735bf5e70b3777b42108a47cf72795e2 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 24 Oct 2022 11:31:58 +0200 Subject: [PATCH 061/225] Returning TRUE in an invisible way --- R/utils.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/utils.R b/R/utils.R index cfe7332..35946b2 100644 --- a/R/utils.R +++ b/R/utils.R @@ -94,6 +94,6 @@ check_uncommitted_files <- function() { system("git status -u") stop("You have uncommitted files. Please resolve it before running the performance checks.") } else { - return(TRUE) + return(invisible(TRUE)) } } From 3f5db7b3a7cafb51d7cc89e2c1facab83e68c326 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 24 Oct 2022 12:15:21 +0200 Subject: [PATCH 062/225] Adding tests for multiple files --- .github/workflows/tests.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8a2a7d7..44339af 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,3 +72,12 @@ jobs: - name: Check if it can handle renv - shinytest2 run: | Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests TRUE + + - name: Check if it can handle multiple files - Cypress + run: | + Rscript ./../run_tests.R cypress renv_shiny1,renv_shiny2 ../cypress_tests1.js,../cypress_tests2.js TRUE + + - name: Check if it can handle multiple files - shinytest2 + run: | + Rscript ./../run_tests.R shinytest2 renv_shiny1,renv_shiny2 tests,fake_folder/tests TRUE + From 6ad457bffc99480b675e78b6f11e449f169ce1f9 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 24 Oct 2022 12:26:46 +0200 Subject: [PATCH 063/225] Simple test --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 44339af..1fb1cd4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -51,11 +51,11 @@ jobs: - name: Check basic functionality - Cypress run: | - Rscript ../run_tests.R cypress master,develop ../cypress_tests1.js FALSE + Rscript ./../run_tests.R cypress master,develop ../cypress_tests1.js FALSE - name: Check basic functionality - shinytest2 run: | - Rscript ../run_tests.R shinytest2 master,develop tests/ FALSE + Rscript ./../run_tests.R shinytest2 master,develop tests/ FALSE - name: Check if it fails when renv not present - Cypress run: | From 8ab25c664a4506d71b6042a81fea639185cd24d9 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 24 Oct 2022 14:12:40 +0200 Subject: [PATCH 064/225] Removing environment variable as it is useless right now --- .github/workflows/tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1fb1cd4..5d75196 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,9 +6,6 @@ defaults: run: working-directory: ./.github/workflows/tests/app -env: - shiny_pergformance_run_mode: github_actions - jobs: main: name: ${{ matrix.config.os }} (${{ matrix.config.r }}) From 9ea73f50e9db13ea8ce232433ea2d7a257dccf41 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 24 Oct 2022 16:01:41 +0200 Subject: [PATCH 065/225] Adding progress in Imports --- DESCRIPTION | 1 + 1 file changed, 1 insertion(+) diff --git a/DESCRIPTION b/DESCRIPTION index 97da3f0..eaf2ff2 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -26,6 +26,7 @@ Depends: Imports: glue, jsonlite, + progress, renv, shinytest2, stringr, From 96e7827a670d9af40fab2a71dbfea21d940739d9 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 24 Oct 2022 16:02:28 +0200 Subject: [PATCH 066/225] Adding function to create a nice progress bar --- NAMESPACE | 1 + R/utils.R | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/NAMESPACE b/NAMESPACE index 0b92298..828c14d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -7,6 +7,7 @@ export(run_cypress_ptest) export(run_shinytest2_ptest) importFrom(glue,glue) importFrom(jsonlite,write_json) +importFrom(progress,progress_bar) importFrom(renv,activate) importFrom(renv,restore) importFrom(shinytest2,test_app) diff --git a/R/utils.R b/R/utils.R index b71e66c..1177963 100644 --- a/R/utils.R +++ b/R/utils.R @@ -85,3 +85,17 @@ checkout <- function(branch) { glue("git checkout {branch}") ) } + +#' @title Create a progress bar to follow the execution +#' +#' @param total Total number of replications +#' @importFrom progress progress_bar +create_progress_bar <- function(total = 100) { + pb <- progress_bar$new( + format = "Iteration :current/:total", + total = total, + clear = FALSE + ) + + return(pb) +} From c4f98cfa4aa9688124513191e1476c0db7ba458a Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 24 Oct 2022 16:03:02 +0200 Subject: [PATCH 067/225] Introducing n_rep argument and changing output to a list instead of a data.frame --- R/performance_tests.R | 8 ++++++++ R/tests_cypress.R | 39 +++++++++++++++++++++++------------- R/tests_shinytest2.R | 46 +++++++++++++++++++++++++++---------------- 3 files changed, 62 insertions(+), 31 deletions(-) diff --git a/R/performance_tests.R b/R/performance_tests.R index 3ac008f..b4999a7 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -12,6 +12,7 @@ #' renv::restore() in all branches. Otherwise, the current loaded list of #' packages will be used in all branches. #' @param renv_prompt Prompt the user before taking any action? +#' @param n_rep Number of replications desired #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @export @@ -23,6 +24,7 @@ performance_tests <- function( port = 3333, use_renv = TRUE, renv_prompt = TRUE, + n_rep = 1, debug = FALSE ) { # Number of commits to test @@ -45,6 +47,10 @@ performance_tests <- function( if (length(get(obj_name)) != n_commits) stop("You must provide 1 or {n_commits} paths for {obj_name}") + n_rep <- as.integer(n_rep) + if (n_rep < 1) + stop("You must provide an integer greater than 1 for n_rep") + # run tests if (type == "cypress") { perf_list <- ptest_cypress( @@ -54,6 +60,7 @@ performance_tests <- function( port = port, use_renv = use_renv, renv_prompt = renv_prompt, + n_rep = n_rep, debug = debug ) } else { @@ -63,6 +70,7 @@ performance_tests <- function( app_dir, use_renv = use_renv, renv_prompt = renv_prompt, + n_rep = n_rep, debug = debug ) } diff --git a/R/tests_cypress.R b/R/tests_cypress.R index 623c515..5d9f4dc 100644 --- a/R/tests_cypress.R +++ b/R/tests_cypress.R @@ -10,6 +10,7 @@ #' renv::restore() in all branches. Otherwise, the current loaded list of #' packages will be used in all branches. #' @param renv_prompt Prompt the user before taking any action? +#' @param n_rep Number of replications desired #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @export @@ -20,6 +21,7 @@ ptest_cypress <- function( port, use_renv, renv_prompt, + n_rep, debug ) { # creating the structure @@ -42,6 +44,7 @@ ptest_cypress <- function( project_path = project_path, use_renv = use_renv, renv_prompt = renv_prompt, + n_rep = n_rep, debug = debug, SIMPLIFY = FALSE ) @@ -83,6 +86,7 @@ ptest_cypress <- function( #' renv::restore() in all branches. Otherwise, the current loaded list of #' packages will be used in all branches. #' @param renv_prompt Prompt the user before taking any action? +#' @param n_rep Number of replications desired #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @importFrom utils read.table @@ -93,6 +97,7 @@ run_cypress_ptest <- function( cypress_file, use_renv, renv_prompt, + n_rep, debug ) { # checkout to the desired commit @@ -106,24 +111,30 @@ run_cypress_ptest <- function( project_path = project_path, cypress_file = cypress_file ) - - js_file <- files$js_file txt_file <- files$txt_file - # run tests there - command <- glue( - "cd {project_path}; ", - "set -eu; exec yarn --cwd node performance-test" - ) - system(command, ignore.stdout = !debug, ignore.stderr = !debug) + # replicate tests + perf_file <- list() + pb <- create_progress_bar(total = n_rep) + for (i in 1:n_rep) { + # increment progress bar + pb$tick() + + # run tests there + command <- glue( + "cd {project_path}; ", + "set -eu; exec yarn --cwd node performance-test" + ) + system(command, ignore.stdout = !debug, ignore.stderr = !debug) - # read the file saved by cypress - perf_file <- read.table(file = txt_file, header = FALSE, sep = ";") - perf_file <- cbind.data.frame(date = date, perf_file) - colnames(perf_file) <- c("date", "test_name", "duration_ms") + # read the file saved by cypress + perf_file[[i]] <- read.table(file = txt_file, header = FALSE, sep = ";") + perf_file[[i]] <- cbind.data.frame(date = date, rep_id = i, perf_file[[i]]) + colnames(perf_file[[i]]) <- c("date", "rep_id", "test_name", "duration_ms") - # removing temp files - unlink(x = c(js_file, txt_file)) + # removing temp files + unlink(x = txt_file) + } # removing anything new in the github repo checkout_files() diff --git a/R/tests_shinytest2.R b/R/tests_shinytest2.R index 3b707f9..fc18f53 100644 --- a/R/tests_shinytest2.R +++ b/R/tests_shinytest2.R @@ -9,6 +9,7 @@ #' renv::restore() in all branches. Otherwise, the current loaded list of #' packages will be used in all branches. #' @param renv_prompt Prompt the user before taking any action? +#' @param n_rep Number of replications desired #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @export @@ -18,6 +19,7 @@ ptest_shinytest2 <- function( app_dir, use_renv, renv_prompt, + n_rep, debug ) { # creating the structure @@ -37,6 +39,7 @@ ptest_shinytest2 <- function( project_path = project_path, use_renv = use_renv, renv_prompt = renv_prompt, + n_rep = n_rep, debug = debug, SIMPLIFY = FALSE ) @@ -71,6 +74,7 @@ ptest_shinytest2 <- function( #' renv::restore() in all branches. Otherwise, the current loaded list of #' packages will be used in all branches. #' @param renv_prompt Prompt the user before taking any action? +#' @param n_rep Number of replications desired #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @importFrom testthat ListReporter @@ -83,6 +87,7 @@ run_shinytest2_ptest <- function( shinytest2_dir, use_renv, renv_prompt, + n_rep, debug ) { # checkout to the desired commit @@ -94,25 +99,32 @@ run_shinytest2_ptest <- function( # move test files to the project folder tests_dir <- move_shinytest2_tests(project_path = project_path, shinytest2_dir = shinytest2_dir) - # run tests there - my_reporter <- ListReporter$new() - test_app( - app_dir = dirname(tests_dir), - reporter = my_reporter, - stop_on_failure = FALSE, - stop_on_warning = FALSE - ) + perf_file <- list() + pb <- create_progress_bar(total = n_rep) + for (i in 1:n_rep) { + # increment progress bar + pb$tick() - perf_file <- as.data.frame(my_reporter$get_results()) - perf_file <- perf_file[, c("test", "real")] - perf_file$test <- gsub( - x = perf_file$test, - pattern = "\\{shinytest2\\} recording: ", - replacement = "" - ) + # run tests there + my_reporter <- ListReporter$new() + test_app( + app_dir = dirname(tests_dir), + reporter = my_reporter, + stop_on_failure = FALSE, + stop_on_warning = FALSE + ) + + perf_file[[i]] <- as.data.frame(my_reporter$get_results()) + perf_file[[i]] <- perf_file[[i]][, c("test", "real")] + perf_file[[i]]$test <- gsub( + x = perf_file[[i]]$test, + pattern = "\\{shinytest2\\} recording: ", + replacement = "" + ) - perf_file <- cbind.data.frame(date = date, perf_file) - colnames(perf_file) <- c("date", "test_name", "duration_ms") + perf_file[[i]] <- cbind.data.frame(date = date, rep_id = i, perf_file[[i]]) + colnames(perf_file[[i]]) <- c("date", "rep_id", "test_name", "duration_ms") + } # removing anything new in the github repo checkout_files() From 75f9c85a7c3bc6a58bd5e661ae1fb6b3bc80201a Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 24 Oct 2022 16:03:15 +0200 Subject: [PATCH 068/225] Updating documentation --- man/create_progress_bar.Rd | 14 ++++++++++++++ man/performance_tests.Rd | 3 +++ man/ptest_cypress.Rd | 3 +++ man/ptest_shinytest2.Rd | 3 +++ man/run_cypress_ptest.Rd | 3 +++ man/run_shinytest2_ptest.Rd | 3 +++ 6 files changed, 29 insertions(+) create mode 100644 man/create_progress_bar.Rd diff --git a/man/create_progress_bar.Rd b/man/create_progress_bar.Rd new file mode 100644 index 0000000..551c970 --- /dev/null +++ b/man/create_progress_bar.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{create_progress_bar} +\alias{create_progress_bar} +\title{Create a progress bar to follow the execution} +\usage{ +create_progress_bar(total = 100) +} +\arguments{ +\item{total}{Total number of replications} +} +\description{ +Create a progress bar to follow the execution +} diff --git a/man/performance_tests.Rd b/man/performance_tests.Rd index e50ff13..acc055a 100644 --- a/man/performance_tests.Rd +++ b/man/performance_tests.Rd @@ -12,6 +12,7 @@ performance_tests( port = 3333, use_renv = TRUE, renv_prompt = TRUE, + n_rep = 1, debug = FALSE ) } @@ -35,6 +36,8 @@ packages will be used in all branches.} \item{renv_prompt}{Prompt the user before taking any action?} +\item{n_rep}{Number of replications desired} + \item{debug}{Logical. TRUE to display all the system messages on runtime} } \description{ diff --git a/man/ptest_cypress.Rd b/man/ptest_cypress.Rd index 1c4bbf6..835d3e4 100644 --- a/man/ptest_cypress.Rd +++ b/man/ptest_cypress.Rd @@ -11,6 +11,7 @@ ptest_cypress( port, use_renv, renv_prompt, + n_rep, debug ) } @@ -31,6 +32,8 @@ packages will be used in all branches.} \item{renv_prompt}{Prompt the user before taking any action?} +\item{n_rep}{Number of replications desired} + \item{debug}{Logical. TRUE to display all the system messages on runtime} } \description{ diff --git a/man/ptest_shinytest2.Rd b/man/ptest_shinytest2.Rd index 31449a1..a93085d 100644 --- a/man/ptest_shinytest2.Rd +++ b/man/ptest_shinytest2.Rd @@ -10,6 +10,7 @@ ptest_shinytest2( app_dir, use_renv, renv_prompt, + n_rep, debug ) } @@ -28,6 +29,8 @@ packages will be used in all branches.} \item{renv_prompt}{Prompt the user before taking any action?} +\item{n_rep}{Number of replications desired} + \item{debug}{Logical. TRUE to display all the system messages on runtime} } \description{ diff --git a/man/run_cypress_ptest.Rd b/man/run_cypress_ptest.Rd index 5eebf75..942f16e 100644 --- a/man/run_cypress_ptest.Rd +++ b/man/run_cypress_ptest.Rd @@ -10,6 +10,7 @@ run_cypress_ptest( cypress_file, use_renv, renv_prompt, + n_rep, debug ) } @@ -28,6 +29,8 @@ packages will be used in all branches.} \item{renv_prompt}{Prompt the user before taking any action?} +\item{n_rep}{Number of replications desired} + \item{debug}{Logical. TRUE to display all the system messages on runtime} } \description{ diff --git a/man/run_shinytest2_ptest.Rd b/man/run_shinytest2_ptest.Rd index e72d9b9..992b01b 100644 --- a/man/run_shinytest2_ptest.Rd +++ b/man/run_shinytest2_ptest.Rd @@ -11,6 +11,7 @@ run_shinytest2_ptest( shinytest2_dir, use_renv, renv_prompt, + n_rep, debug ) } @@ -29,6 +30,8 @@ packages will be used in all branches.} \item{renv_prompt}{Prompt the user before taking any action?} +\item{n_rep}{Number of replications desired} + \item{debug}{Logical. TRUE to display all the system messages on runtime} } \description{ From cc9c5a6c6068432aa3e9be43dca77119bda9114f Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 24 Oct 2022 16:54:12 +0200 Subject: [PATCH 069/225] Do not clean test name --- R/tests_shinytest2.R | 5 ----- 1 file changed, 5 deletions(-) diff --git a/R/tests_shinytest2.R b/R/tests_shinytest2.R index 3b707f9..2314a60 100644 --- a/R/tests_shinytest2.R +++ b/R/tests_shinytest2.R @@ -105,11 +105,6 @@ run_shinytest2_ptest <- function( perf_file <- as.data.frame(my_reporter$get_results()) perf_file <- perf_file[, c("test", "real")] - perf_file$test <- gsub( - x = perf_file$test, - pattern = "\\{shinytest2\\} recording: ", - replacement = "" - ) perf_file <- cbind.data.frame(date = date, perf_file) colnames(perf_file) <- c("date", "test_name", "duration_ms") From 5a42114f3503cf94d89d6bc7e5e29cddc802f342 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 24 Oct 2022 17:49:10 +0200 Subject: [PATCH 070/225] After is not working. Adding a dummy test at the end to force Cypress to collect times from all tests --- R/utils_cypress.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/utils_cypress.R b/R/utils_cypress.R index ff4a611..d4a72cb 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -144,6 +144,8 @@ create_cypress_tests <- function(project_path, cypress_file) { add_sendtime2js <- function(js_file, txt_file) { lines_to_add <- glue( " + describe('Finalizing tests', () => {it('Ending tests', () => {})}) + // Returning the time for each test // https://www.cypress.io/blog/2020/05/22/where-does-the-test-spend-its-time/ let commands = [] @@ -169,7 +171,6 @@ add_sendtime2js <- function(js_file, txt_file) { } // Calling the sendTestTimings function beforeEach(sendTestTimings) - after(sendTestTimings) ", .open = "{{", .close = "}}" ) From e680a26de7daabad6a0c63ab2a1e860d80222872 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Tue, 25 Oct 2022 14:09:18 +0200 Subject: [PATCH 071/225] Adding the ability to select tests to run based on the file pattern --- R/performance_tests.R | 29 ++++++++++++++++++++--------- R/tests_cypress.R | 24 ++++++++++++++++-------- R/tests_shinytest2.R | 11 ++++++++++- R/utils_cypress.R | 21 +++++++++++++++++---- man/create_cypress_tests.Rd | 8 +++++--- man/performance_tests.Rd | 11 ++++++++--- man/ptest_cypress.Rd | 11 ++++++++--- man/ptest_shinytest2.Rd | 5 +++++ man/run_cypress_ptest.Rd | 9 ++++++--- man/run_shinytest2_ptest.Rd | 4 ++++ 10 files changed, 99 insertions(+), 34 deletions(-) diff --git a/R/performance_tests.R b/R/performance_tests.R index 3ac008f..f7e0028 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -2,10 +2,13 @@ #' #' @param commit_list A list of commit hash codes, branches' names or anything #' else you can use with git checkout [...] -#' @param cypress_file The path to the .js file containing cypress tests -#' to be recorded. It can also be a vector of the same size of commit_list +#' @param cypress_dir The directory with tests recorded by Cypress. +#' It can also be a vector of the same size of commit_list #' @param shinytest2_dir The directory with tests recorded by shinytest2 #' It can also be a vector of the same size of commit_list +#' @param tests_pattern Cypress/shinytest2 files pattern. E.g. 'performance' +#' It can also be a vector of the same size of commit_list. If it is NULL, +#' all the content in cypress_dir/shinytest2_dir will be used #' @param app_dir The path to the application root #' @param port Port to run the app #' @param use_renv In case it is set as TRUE, package will try to apply @@ -17,8 +20,9 @@ #' @export performance_tests <- function( commit_list, - cypress_file = NULL, + cypress_dir = NULL, shinytest2_dir = NULL, + tests_pattern = NULL, app_dir = getwd(), port = 3333, use_renv = TRUE, @@ -29,27 +33,33 @@ performance_tests <- function( n_commits <- length(commit_list) # Test whether we have everything we need - if (is.null(cypress_file) && is.null(shinytest2_dir)) - stop("You must provide a cypress_file or the shinytest2_dir") + if (is.null(cypress_dir) && is.null(shinytest2_dir)) + stop("You must provide a cypress_dir or the shinytest2_dir") - if (!is.null(cypress_file) && !is.null(shinytest2_dir)) { + if (!is.null(cypress_dir) && !is.null(shinytest2_dir)) { message("Using the cypress file only") shinytest2_dir <- NULL } - type <- ifelse(!is.null(cypress_file), "cypress", "shinytest2") - obj_name <- ifelse(type == "cypress", "cypress_file", "shinytest2_dir") + type <- ifelse(!is.null(cypress_dir), "cypress", "shinytest2") + obj_name <- ifelse(type == "cypress", "cypress_dir", "shinytest2_dir") if (length(get(obj_name)) == 1) assign(obj_name, rep(get(obj_name), n_commits)) if (length(get(obj_name)) != n_commits) stop("You must provide 1 or {n_commits} paths for {obj_name}") + if (is.null(tests_pattern)) + tests_pattern <- vector(mode = "list", length = n_commits) + if (length(tests_pattern) == 1) + tests_pattern <- as.list(rep(tests_pattern, n_commits)) + # run tests if (type == "cypress") { perf_list <- ptest_cypress( commit_list = commit_list, - cypress_file = cypress_file, + cypress_dir = cypress_dir, + tests_pattern = tests_pattern, app_dir = app_dir, port = port, use_renv = use_renv, @@ -60,6 +70,7 @@ performance_tests <- function( perf_list <- ptest_shinytest2( commit_list, shinytest2_dir, + tests_pattern = tests_pattern, app_dir, use_renv = use_renv, renv_prompt = renv_prompt, diff --git a/R/tests_cypress.R b/R/tests_cypress.R index 623c515..66a5e02 100644 --- a/R/tests_cypress.R +++ b/R/tests_cypress.R @@ -2,8 +2,11 @@ #' #' @param commit_list A list of commit hash codes, branches' names or anything #' else you can use with git checkout [...] -#' @param cypress_file The path to the .js file conteining cypress tests to -#' be recorded +#' @param cypress_dir The directory with tests recorded by Cypress. +#' It can also be a vector of the same size of commit_list +#' @param tests_pattern Cypress/shinytest2 files pattern. E.g. 'shinytest2' +#' It can also be a vector of the same size of commit_list. If it is NULL, +#' all the content in cypress_dir/shinytest2_dir will be used #' @param app_dir The path to the application root #' @param port Port to run the app #' @param use_renv In case it is set as TRUE, package will try to apply @@ -15,7 +18,8 @@ #' @export ptest_cypress <- function( commit_list, - cypress_file, + cypress_dir, + tests_pattern, app_dir, port, use_renv, @@ -37,7 +41,8 @@ ptest_cypress <- function( expr = { mapply( commit_list, - cypress_file, + cypress_dir, + tests_pattern, FUN = run_cypress_ptest, project_path = project_path, use_renv = use_renv, @@ -77,8 +82,9 @@ ptest_cypress <- function( #' @param commit A commit hash code or a branch's name #' @param project_path The path to the project with all needed packages #' installed -#' @param cypress_file The path to the .js file conteining cypress tests to -#' be recorded +#' @param cypress_dir The directory with tests recorded by Cypress +#' @param tests_pattern Cypress files pattern. E.g. 'performance'. If it is NULL, +#' all the content will be used #' @param use_renv In case it is set as TRUE, package will try to apply #' renv::restore() in all branches. Otherwise, the current loaded list of #' packages will be used in all branches. @@ -90,7 +96,8 @@ ptest_cypress <- function( run_cypress_ptest <- function( commit, project_path, - cypress_file, + cypress_dir, + tests_pattern, use_renv, renv_prompt, debug @@ -104,7 +111,8 @@ run_cypress_ptest <- function( # get Cypress files files <- create_cypress_tests( project_path = project_path, - cypress_file = cypress_file + cypress_dir = cypress_dir, + tests_pattern = tests_pattern ) js_file <- files$js_file diff --git a/R/tests_shinytest2.R b/R/tests_shinytest2.R index 3b707f9..1d94c1b 100644 --- a/R/tests_shinytest2.R +++ b/R/tests_shinytest2.R @@ -4,6 +4,9 @@ #' else you can use with git checkout [...] #' @param shinytest2_dir The directory with tests recorded by shinytest2 #' It can also be a vector of the same size of commit_list +#' @param tests_pattern shinytest2 files pattern. E.g. 'performance' +#' It can also be a vector of the same size of commit_list. If it is NULL, +#' all the content in cypress_dir/shinytest2_dir will be used #' @param app_dir The path to the application root #' @param use_renv In case it is set as TRUE, package will try to apply #' renv::restore() in all branches. Otherwise, the current loaded list of @@ -15,6 +18,7 @@ ptest_shinytest2 <- function( commit_list, shinytest2_dir, + tests_pattern, app_dir, use_renv, renv_prompt, @@ -32,6 +36,7 @@ ptest_shinytest2 <- function( mapply( commit_list, shinytest2_dir, + tests_pattern, FUN = run_shinytest2_ptest, app_dir = app_dir, project_path = project_path, @@ -67,6 +72,8 @@ ptest_shinytest2 <- function( #' @param app_dir The path to the application root #' @param project_path The path to the project #' @param shinytest2_dir The directory with tests recorded by shinytest2 +#' @param tests_pattern shinytest2 files pattern. E.g. 'performance'. If it is NULL, +#' all the content will be used #' @param use_renv In case it is set as TRUE, package will try to apply #' renv::restore() in all branches. Otherwise, the current loaded list of #' packages will be used in all branches. @@ -81,6 +88,7 @@ run_shinytest2_ptest <- function( project_path, app_dir, shinytest2_dir, + tests_pattern, use_renv, renv_prompt, debug @@ -100,7 +108,8 @@ run_shinytest2_ptest <- function( app_dir = dirname(tests_dir), reporter = my_reporter, stop_on_failure = FALSE, - stop_on_warning = FALSE + stop_on_warning = FALSE, + filter = tests_pattern ) perf_file <- as.data.frame(my_reporter$get_results()) diff --git a/R/utils_cypress.R b/R/utils_cypress.R index ff4a611..f3796b1 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -115,9 +115,19 @@ create_cypress_plugins <- function() { #' #' @param project_path The path to the project with all needed packages #' installed -#' @param cypress_file The path to the .js file conteining cypress tests to -#' be recorded -create_cypress_tests <- function(project_path, cypress_file) { +#' @param cypress_dir The directory with tests recorded by Cypress +#' @param tests_pattern Cypress files pattern. E.g. 'performance'. If it is NULL, +#' all the content will be used +create_cypress_tests <- function(project_path, cypress_dir, tests_pattern) { + # locate files + cypress_files <- list.files( + path = cypress_dir, + pattern = tests_pattern, + full.names = TRUE, + recursive = TRUE + ) + cypress_files <- grep(x = cypress_files, pattern = "\\.js$", value = TRUE) + # creating a copy to be able to edit the js file js_file <- file.path( project_path, @@ -127,7 +137,10 @@ create_cypress_tests <- function(project_path, cypress_file) { "app.spec.js" ) - file.copy(from = cypress_file, to = js_file, overwrite = TRUE) + # combine all files into one + cypress_files_string <- paste0(cypress_files, collapse = " ") # nolint + command <- glue("cat {cypress_files_string} > {js_file}") + system(command = command, intern = TRUE) # file to store the times txt_file <- file.path(project_path, "tests", "cypress", "performance.txt") diff --git a/man/create_cypress_tests.Rd b/man/create_cypress_tests.Rd index f0f66a1..924ceb6 100644 --- a/man/create_cypress_tests.Rd +++ b/man/create_cypress_tests.Rd @@ -4,14 +4,16 @@ \alias{create_cypress_tests} \title{Create the cypress files under project directory} \usage{ -create_cypress_tests(project_path, cypress_file) +create_cypress_tests(project_path, cypress_dir, tests_pattern) } \arguments{ \item{project_path}{The path to the project with all needed packages installed} -\item{cypress_file}{The path to the .js file conteining cypress tests to -be recorded} +\item{cypress_dir}{The directory with tests recorded by Cypress} + +\item{tests_pattern}{Cypress files pattern. E.g. 'performance'. If it is NULL, +all the content will be used} } \description{ Create the cypress files under project directory diff --git a/man/performance_tests.Rd b/man/performance_tests.Rd index e50ff13..9bfd4b8 100644 --- a/man/performance_tests.Rd +++ b/man/performance_tests.Rd @@ -6,8 +6,9 @@ \usage{ performance_tests( commit_list, - cypress_file = NULL, + cypress_dir = NULL, shinytest2_dir = NULL, + tests_pattern = NULL, app_dir = getwd(), port = 3333, use_renv = TRUE, @@ -19,12 +20,16 @@ performance_tests( \item{commit_list}{A list of commit hash codes, branches' names or anything else you can use with git checkout \link{...}} -\item{cypress_file}{The path to the .js file containing cypress tests -to be recorded. It can also be a vector of the same size of commit_list} +\item{cypress_dir}{The directory with tests recorded by Cypress. +It can also be a vector of the same size of commit_list} \item{shinytest2_dir}{The directory with tests recorded by shinytest2 It can also be a vector of the same size of commit_list} +\item{tests_pattern}{Cypress/shinytest2 files pattern. E.g. 'performance' +It can also be a vector of the same size of commit_list. If it is NULL, +all the content in cypress_dir/shinytest2_dir will be used} + \item{app_dir}{The path to the application root} \item{port}{Port to run the app} diff --git a/man/ptest_cypress.Rd b/man/ptest_cypress.Rd index 1c4bbf6..de4ec84 100644 --- a/man/ptest_cypress.Rd +++ b/man/ptest_cypress.Rd @@ -6,7 +6,8 @@ \usage{ ptest_cypress( commit_list, - cypress_file, + cypress_dir, + tests_pattern, app_dir, port, use_renv, @@ -18,8 +19,12 @@ ptest_cypress( \item{commit_list}{A list of commit hash codes, branches' names or anything else you can use with git checkout \link{...}} -\item{cypress_file}{The path to the .js file conteining cypress tests to -be recorded} +\item{cypress_dir}{The directory with tests recorded by Cypress. +It can also be a vector of the same size of commit_list} + +\item{tests_pattern}{Cypress/shinytest2 files pattern. E.g. 'shinytest2' +It can also be a vector of the same size of commit_list. If it is NULL, +all the content in cypress_dir/shinytest2_dir will be used} \item{app_dir}{The path to the application root} diff --git a/man/ptest_shinytest2.Rd b/man/ptest_shinytest2.Rd index 31449a1..cd6920d 100644 --- a/man/ptest_shinytest2.Rd +++ b/man/ptest_shinytest2.Rd @@ -7,6 +7,7 @@ ptest_shinytest2( commit_list, shinytest2_dir, + tests_pattern, app_dir, use_renv, renv_prompt, @@ -20,6 +21,10 @@ else you can use with git checkout \link{...}} \item{shinytest2_dir}{The directory with tests recorded by shinytest2 It can also be a vector of the same size of commit_list} +\item{tests_pattern}{shinytest2 files pattern. E.g. 'performance' +It can also be a vector of the same size of commit_list. If it is NULL, +all the content in cypress_dir/shinytest2_dir will be used} + \item{app_dir}{The path to the application root} \item{use_renv}{In case it is set as TRUE, package will try to apply diff --git a/man/run_cypress_ptest.Rd b/man/run_cypress_ptest.Rd index 5eebf75..2960c02 100644 --- a/man/run_cypress_ptest.Rd +++ b/man/run_cypress_ptest.Rd @@ -7,7 +7,8 @@ run_cypress_ptest( commit, project_path, - cypress_file, + cypress_dir, + tests_pattern, use_renv, renv_prompt, debug @@ -19,8 +20,10 @@ run_cypress_ptest( \item{project_path}{The path to the project with all needed packages installed} -\item{cypress_file}{The path to the .js file conteining cypress tests to -be recorded} +\item{cypress_dir}{The directory with tests recorded by Cypress} + +\item{tests_pattern}{Cypress files pattern. E.g. 'performance'. If it is NULL, +all the content will be used} \item{use_renv}{In case it is set as TRUE, package will try to apply renv::restore() in all branches. Otherwise, the current loaded list of diff --git a/man/run_shinytest2_ptest.Rd b/man/run_shinytest2_ptest.Rd index e72d9b9..81f41fe 100644 --- a/man/run_shinytest2_ptest.Rd +++ b/man/run_shinytest2_ptest.Rd @@ -9,6 +9,7 @@ run_shinytest2_ptest( project_path, app_dir, shinytest2_dir, + tests_pattern, use_renv, renv_prompt, debug @@ -23,6 +24,9 @@ run_shinytest2_ptest( \item{shinytest2_dir}{The directory with tests recorded by shinytest2} +\item{tests_pattern}{shinytest2 files pattern. E.g. 'performance'. If it is NULL, +all the content will be used} + \item{use_renv}{In case it is set as TRUE, package will try to apply renv::restore() in all branches. Otherwise, the current loaded list of packages will be used in all branches.} From 4d15845674b93a676d5e6ab0439dafdae1399c15 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 08:59:31 +0200 Subject: [PATCH 072/225] listing files in the tmp dir --- R/utils_cypress.R | 2 ++ 1 file changed, 2 insertions(+) diff --git a/R/utils_cypress.R b/R/utils_cypress.R index ff4a611..1bff73f 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -127,7 +127,9 @@ create_cypress_tests <- function(project_path, cypress_file) { "app.spec.js" ) + list.files(path = project_path) file.copy(from = cypress_file, to = js_file, overwrite = TRUE) + list.files(path = project_path) # file to store the times txt_file <- file.path(project_path, "tests", "cypress", "performance.txt") From 71800ef7dab938a2717867fe7857bad8491af276 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 09:05:01 +0200 Subject: [PATCH 073/225] ignoring renv folder --- .github/workflows/tests/setting_branches.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests/setting_branches.sh b/.github/workflows/tests/setting_branches.sh index e15d555..376d518 100644 --- a/.github/workflows/tests/setting_branches.sh +++ b/.github/workflows/tests/setting_branches.sh @@ -10,6 +10,7 @@ git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" # STANDARD FUNCTIONALITIES ## master +echo renv/* > .gitignore git add . git commit -m "first commit" From 81e1f0267fecb800377d3c6f4f2faa93dea775f1 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 09:08:48 +0200 Subject: [PATCH 074/225] remove shiny.performance_0.1.1.tar.gz --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5d75196..5b178f5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -45,6 +45,7 @@ jobs: run: | R CMD build ../../../../. R CMD INSTALL shiny.performance_0.1.1.tar.gz + rm shiny.performance_0.1.1.tar.gz - name: Check basic functionality - Cypress run: | From 2b438ef16c2784b989793ffa2fe6df93f0ea8928 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 09:39:42 +0200 Subject: [PATCH 075/225] Trying to use PAT --- .github/workflows/tests.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5b178f5..9df5bbf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,9 +43,8 @@ jobs: - name: Install shiny.performance run: | - R CMD build ../../../../. - R CMD INSTALL shiny.performance_0.1.1.tar.gz - rm shiny.performance_0.1.1.tar.gz + R -e "install.packages('remotes')" + remotes::install_github(repo = "Appsilon/experimental.performance", auth_token = Sys.getenv("GITHUB_PAT"), ref = "develop", quiet = TRUE) - name: Check basic functionality - Cypress run: | From eb7ac823e45a5d72cb5c7e314e4f3d970a8ff743 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 09:43:00 +0200 Subject: [PATCH 076/225] Personal PAT --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9df5bbf..d2a33e6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,7 @@ jobs: - {os: ubuntu-22.04, r: 'release'} env: - GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PAT: ${{ secrets.PAT }} steps: - name: Checkout repository From 73c658870bf03576aaa4beb878c847566083c7f0 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 09:54:33 +0200 Subject: [PATCH 077/225] Forgot R -e --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d2a33e6..263bee1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,7 @@ jobs: - {os: ubuntu-22.04, r: 'release'} env: - GITHUB_PAT: ${{ secrets.PAT }} + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout repository @@ -44,7 +44,7 @@ jobs: - name: Install shiny.performance run: | R -e "install.packages('remotes')" - remotes::install_github(repo = "Appsilon/experimental.performance", auth_token = Sys.getenv("GITHUB_PAT"), ref = "develop", quiet = TRUE) + R -e "remotes::install_github(repo = 'Appsilon/experimental.performance', auth_token = Sys.getenv('GITHUB_PAT'), ref = "develop", quiet = TRUE)"" - name: Check basic functionality - Cypress run: | From f48cb579eb2ea114999df5dd6a7fb99d55b91734 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 09:58:06 +0200 Subject: [PATCH 078/225] quotes issue --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 263bee1..406f30c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,7 +44,7 @@ jobs: - name: Install shiny.performance run: | R -e "install.packages('remotes')" - R -e "remotes::install_github(repo = 'Appsilon/experimental.performance', auth_token = Sys.getenv('GITHUB_PAT'), ref = "develop", quiet = TRUE)"" + R -e "remotes::install_github(repo = 'Appsilon/experimental.performance', auth_token = Sys.getenv('GITHUB_PAT'), ref = 'develop', quiet = TRUE)"" - name: Check basic functionality - Cypress run: | From aeac25653412f0a8c8033ada2bc9df4e94a384eb Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 10:03:15 +0200 Subject: [PATCH 079/225] Another quote issue --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 406f30c..02b5ae6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,7 +44,7 @@ jobs: - name: Install shiny.performance run: | R -e "install.packages('remotes')" - R -e "remotes::install_github(repo = 'Appsilon/experimental.performance', auth_token = Sys.getenv('GITHUB_PAT'), ref = 'develop', quiet = TRUE)"" + R -e "remotes::install_github(repo = 'Appsilon/experimental.performance', auth_token = Sys.getenv('GITHUB_PAT'), ref = 'develop', quiet = TRUE)" - name: Check basic functionality - Cypress run: | From 543b10131480c5a31ca89aa4a90cc017c0fe929f Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 12:56:00 +0200 Subject: [PATCH 080/225] Removing duplicated function --- R/utils.R | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/R/utils.R b/R/utils.R index 6b31780..b5a9c2c 100644 --- a/R/utils.R +++ b/R/utils.R @@ -85,16 +85,6 @@ check_uncommitted_files <- function() { } } -#' @title Checkout GitHub branch -#' @description checkout and go to a different branch -#' -#' @param branch Commit hash code or branch name -checkout <- function(branch) { - system( - glue("git checkout {branch}") - ) -} - #' @title Check and restore renv #' #' @description Check whether renv is in use in the current branch. Raise error From f33454a7ffd31f8bc24a8db0a44ddf64e14099a8 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 13:09:27 +0200 Subject: [PATCH 081/225] Adding messages to identify the error --- .github/workflows/tests.yml | 16 ++++++++-------- R/tests_cypress.R | 2 ++ R/utils_cypress.R | 2 -- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 02b5ae6..3b276e6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -48,33 +48,33 @@ jobs: - name: Check basic functionality - Cypress run: | - Rscript ./../run_tests.R cypress master,develop ../cypress_tests1.js FALSE + Rscript ../run_tests.R cypress master,develop ../cypress_tests1.js FALSE - name: Check basic functionality - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 master,develop tests/ FALSE + Rscript ../run_tests.R shinytest2 master,develop tests/ FALSE - name: Check if it fails when renv not present - Cypress run: | - Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 ../cypress_tests1.js FALSE + Rscript ../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 ../cypress_tests1.js FALSE - name: Check if it fails when renv not present - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests FALSE + Rscript ../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests FALSE - name: Check if it can handle renv - Cypress run: | - Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 ../cypress_tests1.js TRUE + Rscript ../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 ../cypress_tests1.js TRUE - name: Check if it can handle renv - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests TRUE + Rscript ../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests TRUE - name: Check if it can handle multiple files - Cypress run: | - Rscript ./../run_tests.R cypress renv_shiny1,renv_shiny2 ../cypress_tests1.js,../cypress_tests2.js TRUE + Rscript ../run_tests.R cypress renv_shiny1,renv_shiny2 ../cypress_tests1.js,../cypress_tests2.js TRUE - name: Check if it can handle multiple files - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 renv_shiny1,renv_shiny2 tests,fake_folder/tests TRUE + Rscript ../run_tests.R shinytest2 renv_shiny1,renv_shiny2 tests,fake_folder/tests TRUE diff --git a/R/tests_cypress.R b/R/tests_cypress.R index 453f36a..41c13f2 100644 --- a/R/tests_cypress.R +++ b/R/tests_cypress.R @@ -102,10 +102,12 @@ run_cypress_ptest <- function( if (use_renv) restore_env(branch = commit, renv_prompt = renv_prompt) # get Cypress files + message(list.files(project_path)) files <- create_cypress_tests( project_path = project_path, cypress_file = cypress_file ) + message(list.files(project_path)) js_file <- files$js_file txt_file <- files$txt_file diff --git a/R/utils_cypress.R b/R/utils_cypress.R index 1bff73f..ff4a611 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -127,9 +127,7 @@ create_cypress_tests <- function(project_path, cypress_file) { "app.spec.js" ) - list.files(path = project_path) file.copy(from = cypress_file, to = js_file, overwrite = TRUE) - list.files(path = project_path) # file to store the times txt_file <- file.path(project_path, "tests", "cypress", "performance.txt") From 58c86f392245c24b3a4a5a79645e783518c173d5 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 13:24:06 +0200 Subject: [PATCH 082/225] More messages --- R/tests_cypress.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/tests_cypress.R b/R/tests_cypress.R index 41c13f2..f2eacf0 100644 --- a/R/tests_cypress.R +++ b/R/tests_cypress.R @@ -102,12 +102,12 @@ run_cypress_ptest <- function( if (use_renv) restore_env(branch = commit, renv_prompt = renv_prompt) # get Cypress files - message(list.files(project_path)) + message("Create files") files <- create_cypress_tests( project_path = project_path, cypress_file = cypress_file ) - message(list.files(project_path)) + message("Files created") js_file <- files$js_file txt_file <- files$txt_file From ddeb64a2166242eeaea97247defac508935d55f6 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 13:30:42 +0200 Subject: [PATCH 083/225] master -> main --- .github/workflows/tests.yml | 4 ++-- .github/workflows/tests/setting_branches.sh | 10 +++++----- R/tests_cypress.R | 2 -- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3b276e6..86ccd38 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -48,11 +48,11 @@ jobs: - name: Check basic functionality - Cypress run: | - Rscript ../run_tests.R cypress master,develop ../cypress_tests1.js FALSE + Rscript ../run_tests.R cypress main,develop ../cypress_tests1.js FALSE - name: Check basic functionality - shinytest2 run: | - Rscript ../run_tests.R shinytest2 master,develop tests/ FALSE + Rscript ../run_tests.R shinytest2 main,develop tests/ FALSE - name: Check if it fails when renv not present - Cypress run: | diff --git a/.github/workflows/tests/setting_branches.sh b/.github/workflows/tests/setting_branches.sh index 376d518..771e4ed 100644 --- a/.github/workflows/tests/setting_branches.sh +++ b/.github/workflows/tests/setting_branches.sh @@ -9,7 +9,7 @@ git config --local user.name "$GITHUB_ACTOR" git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" # STANDARD FUNCTIONALITIES -## master +## main echo renv/* > .gitignore git add . git commit -m "first commit" @@ -20,12 +20,12 @@ git commit --allow-empty -m "dummy commit to change hash" # RENV FUNCTIONALITIES ## No renv at all -git branch renv_missing master +git branch renv_missing main git checkout renv_missing git commit --allow-empty -m "dummy commit to change hash" ## Creating renv -git branch renv_shiny1 master +git branch renv_shiny1 main git checkout renv_shiny1 R -e 'renv::init()' git add . @@ -38,5 +38,5 @@ R -e 'renv::snapshot()' git add . git commit -m "downgrading shiny" -## Switching back to master -git checkout master +## Switching back to main +git checkout main diff --git a/R/tests_cypress.R b/R/tests_cypress.R index f2eacf0..453f36a 100644 --- a/R/tests_cypress.R +++ b/R/tests_cypress.R @@ -102,12 +102,10 @@ run_cypress_ptest <- function( if (use_renv) restore_env(branch = commit, renv_prompt = renv_prompt) # get Cypress files - message("Create files") files <- create_cypress_tests( project_path = project_path, cypress_file = cypress_file ) - message("Files created") js_file <- files$js_file txt_file <- files$txt_file From 6e98f46ebf91beda478be70a603d01f487c829f7 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 13:34:03 +0200 Subject: [PATCH 084/225] main branch as default --- .github/workflows/tests/setting_branches.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests/setting_branches.sh b/.github/workflows/tests/setting_branches.sh index 771e4ed..c2d61ce 100644 --- a/.github/workflows/tests/setting_branches.sh +++ b/.github/workflows/tests/setting_branches.sh @@ -2,6 +2,7 @@ # starting git init +git config --global init.defaultBranch main git config --global advice.detachedHead false # credentials From 82295431620e292070bc3b2700ee00588e76a924 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 13:40:00 +0200 Subject: [PATCH 085/225] main -> master --- .github/workflows/tests.yml | 4 ++-- .github/workflows/tests/setting_branches.sh | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 86ccd38..3b276e6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -48,11 +48,11 @@ jobs: - name: Check basic functionality - Cypress run: | - Rscript ../run_tests.R cypress main,develop ../cypress_tests1.js FALSE + Rscript ../run_tests.R cypress master,develop ../cypress_tests1.js FALSE - name: Check basic functionality - shinytest2 run: | - Rscript ../run_tests.R shinytest2 main,develop tests/ FALSE + Rscript ../run_tests.R shinytest2 master,develop tests/ FALSE - name: Check if it fails when renv not present - Cypress run: | diff --git a/.github/workflows/tests/setting_branches.sh b/.github/workflows/tests/setting_branches.sh index c2d61ce..376d518 100644 --- a/.github/workflows/tests/setting_branches.sh +++ b/.github/workflows/tests/setting_branches.sh @@ -2,7 +2,6 @@ # starting git init -git config --global init.defaultBranch main git config --global advice.detachedHead false # credentials @@ -10,7 +9,7 @@ git config --local user.name "$GITHUB_ACTOR" git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" # STANDARD FUNCTIONALITIES -## main +## master echo renv/* > .gitignore git add . git commit -m "first commit" @@ -21,12 +20,12 @@ git commit --allow-empty -m "dummy commit to change hash" # RENV FUNCTIONALITIES ## No renv at all -git branch renv_missing main +git branch renv_missing master git checkout renv_missing git commit --allow-empty -m "dummy commit to change hash" ## Creating renv -git branch renv_shiny1 main +git branch renv_shiny1 master git checkout renv_shiny1 R -e 'renv::init()' git add . @@ -39,5 +38,5 @@ R -e 'renv::snapshot()' git add . git commit -m "downgrading shiny" -## Switching back to main -git checkout main +## Switching back to master +git checkout master From 7a973809a244594d4871e5bee1a4886e3c012dfd Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 13:43:44 +0200 Subject: [PATCH 086/225] Testing --- R/tests_cypress.R | 1 + 1 file changed, 1 insertion(+) diff --git a/R/tests_cypress.R b/R/tests_cypress.R index 453f36a..a7722cd 100644 --- a/R/tests_cypress.R +++ b/R/tests_cypress.R @@ -99,6 +99,7 @@ run_cypress_ptest <- function( checkout(branch = commit, debug = debug) date <- get_commit_date(branch = commit) message(glue("Switched to {commit}")) + message("Test") if (use_renv) restore_env(branch = commit, renv_prompt = renv_prompt) # get Cypress files From bbfd10ea024cddcc8c5bd66734e47b183dd0f8fc Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 14:05:01 +0200 Subject: [PATCH 087/225] Installing package from the current branch --- .github/workflows/tests.yml | 2 +- R/tests_cypress.R | 1 - R/utils_cypress.R | 2 ++ 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3b276e6..fba1e46 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,7 +44,7 @@ jobs: - name: Install shiny.performance run: | R -e "install.packages('remotes')" - R -e "remotes::install_github(repo = 'Appsilon/experimental.performance', auth_token = Sys.getenv('GITHUB_PAT'), ref = 'develop', quiet = TRUE)" + R -e "remotes::install_github(repo = 'Appsilon/experimental.performance', auth_token = Sys.getenv('GITHUB_PAT'), ref = 'douglas-31-create-test-environment', quiet = TRUE)" - name: Check basic functionality - Cypress run: | diff --git a/R/tests_cypress.R b/R/tests_cypress.R index a7722cd..453f36a 100644 --- a/R/tests_cypress.R +++ b/R/tests_cypress.R @@ -99,7 +99,6 @@ run_cypress_ptest <- function( checkout(branch = commit, debug = debug) date <- get_commit_date(branch = commit) message(glue("Switched to {commit}")) - message("Test") if (use_renv) restore_env(branch = commit, renv_prompt = renv_prompt) # get Cypress files diff --git a/R/utils_cypress.R b/R/utils_cypress.R index ff4a611..b626f81 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -127,7 +127,9 @@ create_cypress_tests <- function(project_path, cypress_file) { "app.spec.js" ) + message(list.files(path = project_path, recursive = T)) file.copy(from = cypress_file, to = js_file, overwrite = TRUE) + message(list.files(path = project_path, recursive = T)) # file to store the times txt_file <- file.path(project_path, "tests", "cypress", "performance.txt") From 553ba0a97124e43052bd0132ded27afff784b278 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 14:10:17 +0200 Subject: [PATCH 088/225] Listing based on a pattern --- R/tests_cypress.R | 1 + R/utils_cypress.R | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/R/tests_cypress.R b/R/tests_cypress.R index 453f36a..bbf1abd 100644 --- a/R/tests_cypress.R +++ b/R/tests_cypress.R @@ -107,6 +107,7 @@ run_cypress_ptest <- function( cypress_file = cypress_file ) + message(list.files(path = project_path, recursive = T, pattern = "performance")) js_file <- files$js_file txt_file <- files$txt_file diff --git a/R/utils_cypress.R b/R/utils_cypress.R index b626f81..ff4a611 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -127,9 +127,7 @@ create_cypress_tests <- function(project_path, cypress_file) { "app.spec.js" ) - message(list.files(path = project_path, recursive = T)) file.copy(from = cypress_file, to = js_file, overwrite = TRUE) - message(list.files(path = project_path, recursive = T)) # file to store the times txt_file <- file.path(project_path, "tests", "cypress", "performance.txt") From 81a1fd50947a341a4e4a5ab3361672d646c9bd04 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 14:12:55 +0200 Subject: [PATCH 089/225] lint issue --- R/tests_cypress.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/tests_cypress.R b/R/tests_cypress.R index bbf1abd..0457ce0 100644 --- a/R/tests_cypress.R +++ b/R/tests_cypress.R @@ -107,7 +107,7 @@ run_cypress_ptest <- function( cypress_file = cypress_file ) - message(list.files(path = project_path, recursive = T, pattern = "performance")) + message(list.files(path = project_path, recursive = TRUE, pattern = "performance")) js_file <- files$js_file txt_file <- files$txt_file From 758fd004d379e5c8ece7dcc68cb08d9edb82b995 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 14:26:11 +0200 Subject: [PATCH 090/225] forcing debug --- R/tests_cypress.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/R/tests_cypress.R b/R/tests_cypress.R index 0457ce0..492db9d 100644 --- a/R/tests_cypress.R +++ b/R/tests_cypress.R @@ -107,7 +107,6 @@ run_cypress_ptest <- function( cypress_file = cypress_file ) - message(list.files(path = project_path, recursive = TRUE, pattern = "performance")) js_file <- files$js_file txt_file <- files$txt_file @@ -116,7 +115,7 @@ run_cypress_ptest <- function( "cd {project_path}; ", "set -eu; exec yarn --cwd node performance-test" ) - system(command, ignore.stdout = !debug, ignore.stderr = !debug) + system(command, ignore.stdout = debug, ignore.stderr = debug) # read the file saved by cypress perf_file <- read.table(file = txt_file, header = FALSE, sep = ";") From 3a2fe93a41abd9e0f5e70f3e2a6c0ca14a181716 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 14:30:38 +0200 Subject: [PATCH 091/225] shinycssloaders is not necessary here --- .github/workflows/tests/app/global.R | 1 - .github/workflows/tests/app/ui.R | 12 +++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests/app/global.R b/.github/workflows/tests/app/global.R index 6e470a6..5a0cab8 100644 --- a/.github/workflows/tests/app/global.R +++ b/.github/workflows/tests/app/global.R @@ -1,2 +1 @@ library(shiny) -library(shinycssloaders) diff --git a/.github/workflows/tests/app/ui.R b/.github/workflows/tests/app/ui.R index 2dd025d..8d3615a 100644 --- a/.github/workflows/tests/app/ui.R +++ b/.github/workflows/tests/app/ui.R @@ -4,23 +4,17 @@ function() { column( width = 4, actionButton(inputId = "run1", label = "Run 1"), - withSpinner( - uiOutput(outputId = "out1") - ) + uiOutput(outputId = "out1") ), column( width = 4, actionButton(inputId = "run2", label = "Run 2"), - withSpinner( - uiOutput(outputId = "out2") - ) + uiOutput(outputId = "out2") ), column( width = 4, actionButton(inputId = "run3", label = "Run 3"), - withSpinner( - uiOutput(outputId = "out3") - ) + uiOutput(outputId = "out3") ) ) } From ea5ada693b253abbb3f9c7ffd198ba35e674b1c0 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 14:38:02 +0200 Subject: [PATCH 092/225] Reverting system debug to the correct state --- R/tests_cypress.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/tests_cypress.R b/R/tests_cypress.R index 492db9d..453f36a 100644 --- a/R/tests_cypress.R +++ b/R/tests_cypress.R @@ -115,7 +115,7 @@ run_cypress_ptest <- function( "cd {project_path}; ", "set -eu; exec yarn --cwd node performance-test" ) - system(command, ignore.stdout = debug, ignore.stderr = debug) + system(command, ignore.stdout = !debug, ignore.stderr = !debug) # read the file saved by cypress perf_file <- read.table(file = txt_file, header = FALSE, sep = ";") From 5e9244d2104618bc272189696cee3dac4d5e7156 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 14:40:02 +0200 Subject: [PATCH 093/225] Changing execution order --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fba1e46..f13c038 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,15 +37,15 @@ jobs: with: extra-packages: local::. # Necessary to avoid object usage linter errors. - - name: Create app structure - run: | - bash ./../setting_branches.sh - - name: Install shiny.performance run: | R -e "install.packages('remotes')" R -e "remotes::install_github(repo = 'Appsilon/experimental.performance', auth_token = Sys.getenv('GITHUB_PAT'), ref = 'douglas-31-create-test-environment', quiet = TRUE)" + - name: Create app structure + run: | + bash ./../setting_branches.sh + - name: Check basic functionality - Cypress run: | Rscript ../run_tests.R cypress master,develop ../cypress_tests1.js FALSE From 02db24f84322faaaf7c62ef2f74554cc34755e19 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 15:04:33 +0200 Subject: [PATCH 094/225] Installing shiny.performance after using renv --- .github/workflows/tests/setting_branches.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests/setting_branches.sh b/.github/workflows/tests/setting_branches.sh index 376d518..e6965a7 100644 --- a/.github/workflows/tests/setting_branches.sh +++ b/.github/workflows/tests/setting_branches.sh @@ -28,6 +28,8 @@ git commit --allow-empty -m "dummy commit to change hash" git branch renv_shiny1 master git checkout renv_shiny1 R -e 'renv::init()' +R -e "remotes::install_github(repo = 'Appsilon/experimental.performance', auth_token = Sys.getenv('GITHUB_PAT'), ref = 'douglas-31-create-test-environment', quiet = TRUE)" +R -e 'renv::snapshot()' git add . git commit -m "renv active" From dba836cf8600902eeded4f3896ad301abf5846ff Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 15:15:13 +0200 Subject: [PATCH 095/225] Installing shiny.performance if it is not installed --- .github/workflows/tests/run_tests.R | 10 +++++++++- .github/workflows/tests/setting_branches.sh | 2 -- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests/run_tests.R b/.github/workflows/tests/run_tests.R index 31c798f..59a3d58 100644 --- a/.github/workflows/tests/run_tests.R +++ b/.github/workflows/tests/run_tests.R @@ -5,7 +5,15 @@ args <- strsplit(args, ",") # packages library(shiny) library(testthat) -library(shiny.performance) + +if (!require(shiny.performance)) { + remotes::install_github( + repo = 'Appsilon/experimental.performance', + auth_token = Sys.getenv('GITHUB_PAT'), + ref = 'douglas-31-create-test-environment', + quiet = TRUE + ) +} # commits to compare type <- args[[1]] diff --git a/.github/workflows/tests/setting_branches.sh b/.github/workflows/tests/setting_branches.sh index e6965a7..376d518 100644 --- a/.github/workflows/tests/setting_branches.sh +++ b/.github/workflows/tests/setting_branches.sh @@ -28,8 +28,6 @@ git commit --allow-empty -m "dummy commit to change hash" git branch renv_shiny1 master git checkout renv_shiny1 R -e 'renv::init()' -R -e "remotes::install_github(repo = 'Appsilon/experimental.performance', auth_token = Sys.getenv('GITHUB_PAT'), ref = 'douglas-31-create-test-environment', quiet = TRUE)" -R -e 'renv::snapshot()' git add . git commit -m "renv active" From 757574e9e91ec3861ec5f8a52d762d42b01cadd9 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 15:26:32 +0200 Subject: [PATCH 096/225] Using branch name from github and using renv::deactivate at the end of the code --- .github/workflows/tests.yml | 3 ++- .github/workflows/tests/run_tests.R | 11 ++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f13c038..f29af49 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,7 @@ jobs: env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} steps: - name: Checkout repository @@ -40,7 +41,7 @@ jobs: - name: Install shiny.performance run: | R -e "install.packages('remotes')" - R -e "remotes::install_github(repo = 'Appsilon/experimental.performance', auth_token = Sys.getenv('GITHUB_PAT'), ref = 'douglas-31-create-test-environment', quiet = TRUE)" + R -e "remotes::install_github(repo = 'Appsilon/experimental.performance', auth_token = Sys.getenv('GITHUB_PAT'), ref = Sys.getenv('BRANCH_NAME'), quiet = TRUE)" - name: Create app structure run: | diff --git a/.github/workflows/tests/run_tests.R b/.github/workflows/tests/run_tests.R index 59a3d58..50a846b 100644 --- a/.github/workflows/tests/run_tests.R +++ b/.github/workflows/tests/run_tests.R @@ -5,15 +5,7 @@ args <- strsplit(args, ",") # packages library(shiny) library(testthat) - -if (!require(shiny.performance)) { - remotes::install_github( - repo = 'Appsilon/experimental.performance', - auth_token = Sys.getenv('GITHUB_PAT'), - ref = 'douglas-31-create-test-environment', - quiet = TRUE - ) -} +library(shiny.performance) # commits to compare type <- args[[1]] @@ -47,3 +39,4 @@ if (type == "cypress") { # checks stopifnot(length(out) == length(commit_list)) +renv::deactivate() From da05a0432e6f6937b6376f98fdf68b49a8c17252 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 16:40:20 +0200 Subject: [PATCH 097/225] Introducing testhat --- DESCRIPTION | 1 + tests/testthat.R | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 tests/testthat.R diff --git a/DESCRIPTION b/DESCRIPTION index 97da3f0..4594e07 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -30,3 +30,4 @@ Imports: shinytest2, stringr, testthat +Config/testthat/edition: 3 diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..716c09b --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,4 @@ +library(testthat) +library(shiny.performance) + +test_check("shiny.performance") From 8bbc7f754e749c444fbba7b85c747538a93559db Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 16:40:44 +0200 Subject: [PATCH 098/225] testing performance_test function --- tests/testthat/test-performance_tests.R | 42 +++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/testthat/test-performance_tests.R diff --git a/tests/testthat/test-performance_tests.R b/tests/testthat/test-performance_tests.R new file mode 100644 index 0000000..07ce8f0 --- /dev/null +++ b/tests/testthat/test-performance_tests.R @@ -0,0 +1,42 @@ +test_that("Function fails in case of missing cypress_file or shinytest2dir", { + expect_error( + performance_tests( + commit_list = list("commit_1", "commit_2"), + cypress_file = NULL, + shinytest2_dir = NULL, + app_dir = getwd(), + port = 3333, + use_renv = TRUE, + renv_prompt = TRUE, + debug = FALSE + ) + ) +}) + +test_that("Function fails in case of divergences between commit_list and files length", { + expect_error( + performance_tests( + commit_list = list("commit_1", "commit_2"), + cypress_file = c("file_1", "file_2", "file_3"), + shinytest2_dir = NULL, + app_dir = getwd(), + port = 3333, + use_renv = TRUE, + renv_prompt = TRUE, + debug = FALSE + ) + ) + + expect_error( + performance_tests( + commit_list = list("commit_1", "commit_2"), + cypress_file = NULL, + shinytest2_dir = c("file_1", "file_2", "file_3"), + app_dir = getwd(), + port = 3333, + use_renv = TRUE, + renv_prompt = TRUE, + debug = FALSE + ) + ) +}) From 38351f847a6567fb73092db2cc7e3d0e55e7b3e9 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 16:40:58 +0200 Subject: [PATCH 099/225] Tests for utils_cypress --- tests/testthat/test-utils_cypress.R | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/testthat/test-utils_cypress.R diff --git a/tests/testthat/test-utils_cypress.R b/tests/testthat/test-utils_cypress.R new file mode 100644 index 0000000..7a3333b --- /dev/null +++ b/tests/testthat/test-utils_cypress.R @@ -0,0 +1,37 @@ +test_that("Check if we are able to add Cypress code to a txt file", { + tmp_dir <- tempdir() + add_sendtime2js( + js_file = file.path(tmp_dir, "test.js"), + txt_file = "test.txt" + ) + + expect_true(file.exists(file.path(tmp_dir, "test.js"))) +}) + +test_that("Check if we are able to copy file content from a file to another", { + tmp_dir <- tempdir() + tmp_file <- tempfile(tmpdir = tmp_dir, fileext = ".txt") + content_before <- "TEST" + writeLines(text = content_before, con = tmp_file) + + files <- create_cypress_tests(project_path = tmp_dir, cypress_file = tmp_file) + content_after <- readLines(con = files$js_file, n = 1) + + expect_true(content_after == content_before) +}) + +test_that("Check whether we have are able to create Cypress structure correctly or not", { + tmp_dir <- create_cypress_structure( + app_dir = getwd(), + port = 3333, + debug = FALSE + ) + + expect_true(file.exists(file.path(tmp_dir, "node"))) + expect_true(file.exists(file.path(tmp_dir, "node", "root"))) + expect_true(file.exists(file.path(tmp_dir, "node", "root", "DESCRIPTION"))) + expect_true(file.exists(file.path(tmp_dir, "tests"))) + expect_true(file.exists(file.path(tmp_dir, "tests", "cypress", "plugins"))) +}) + + From 920525071e04f98f2308e308eff6a5b5fd496ad3 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 16:41:23 +0200 Subject: [PATCH 100/225] blank files for utils and utils_shinytest2 --- tests/testthat/test-utils.R | 0 tests/testthat/test-utils_shinytest2.R | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/testthat/test-utils.R create mode 100644 tests/testthat/test-utils_shinytest2.R diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R new file mode 100644 index 0000000..e69de29 diff --git a/tests/testthat/test-utils_shinytest2.R b/tests/testthat/test-utils_shinytest2.R new file mode 100644 index 0000000..e69de29 From 5437fea42f825e7acf13e2aa518250df0292e4de Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 16:47:19 +0200 Subject: [PATCH 101/225] Moving tests from .github to tests/end2end --- .Rbuildignore | 1 + .github/workflows/tests.yml | 3 +-- .../tests => tests/end2end}/app/fake_folder/tests/testthat.R | 0 .../end2end}/app/fake_folder/tests/testthat/setup.R | 0 .../end2end}/app/fake_folder/tests/testthat/test-shinytest2.R | 0 {.github/workflows/tests => tests/end2end}/app/global.R | 0 {.github/workflows/tests => tests/end2end}/app/server.R | 0 .../workflows/tests => tests/end2end}/app/tests/testthat.R | 0 .../tests => tests/end2end}/app/tests/testthat/setup.R | 0 .../end2end}/app/tests/testthat/test-shinytest2.R | 0 {.github/workflows/tests => tests/end2end}/app/ui.R | 0 {.github/workflows/tests => tests/end2end}/cypress_tests1.js | 0 {.github/workflows/tests => tests/end2end}/cypress_tests2.js | 0 {.github/workflows/tests => tests/end2end}/run_tests.R | 0 {.github/workflows/tests => tests/end2end}/setting_branches.sh | 0 15 files changed, 2 insertions(+), 2 deletions(-) rename {.github/workflows/tests => tests/end2end}/app/fake_folder/tests/testthat.R (100%) rename {.github/workflows/tests => tests/end2end}/app/fake_folder/tests/testthat/setup.R (100%) rename {.github/workflows/tests => tests/end2end}/app/fake_folder/tests/testthat/test-shinytest2.R (100%) rename {.github/workflows/tests => tests/end2end}/app/global.R (100%) rename {.github/workflows/tests => tests/end2end}/app/server.R (100%) rename {.github/workflows/tests => tests/end2end}/app/tests/testthat.R (100%) rename {.github/workflows/tests => tests/end2end}/app/tests/testthat/setup.R (100%) rename {.github/workflows/tests => tests/end2end}/app/tests/testthat/test-shinytest2.R (100%) rename {.github/workflows/tests => tests/end2end}/app/ui.R (100%) rename {.github/workflows/tests => tests/end2end}/cypress_tests1.js (100%) rename {.github/workflows/tests => tests/end2end}/cypress_tests2.js (100%) rename {.github/workflows/tests => tests/end2end}/run_tests.R (100%) rename {.github/workflows/tests => tests/end2end}/setting_branches.sh (100%) diff --git a/.Rbuildignore b/.Rbuildignore index 78dd63f..c7eaab0 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -2,3 +2,4 @@ ^\.Rproj\.user$ .github .lintr +tests/end2end diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f29af49..d3de322 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,7 +4,7 @@ name: package-usage-tests defaults: run: - working-directory: ./.github/workflows/tests/app + working-directory: ./tests/end2end/app jobs: main: @@ -78,4 +78,3 @@ jobs: - name: Check if it can handle multiple files - shinytest2 run: | Rscript ../run_tests.R shinytest2 renv_shiny1,renv_shiny2 tests,fake_folder/tests TRUE - diff --git a/.github/workflows/tests/app/fake_folder/tests/testthat.R b/tests/end2end/app/fake_folder/tests/testthat.R similarity index 100% rename from .github/workflows/tests/app/fake_folder/tests/testthat.R rename to tests/end2end/app/fake_folder/tests/testthat.R diff --git a/.github/workflows/tests/app/fake_folder/tests/testthat/setup.R b/tests/end2end/app/fake_folder/tests/testthat/setup.R similarity index 100% rename from .github/workflows/tests/app/fake_folder/tests/testthat/setup.R rename to tests/end2end/app/fake_folder/tests/testthat/setup.R diff --git a/.github/workflows/tests/app/fake_folder/tests/testthat/test-shinytest2.R b/tests/end2end/app/fake_folder/tests/testthat/test-shinytest2.R similarity index 100% rename from .github/workflows/tests/app/fake_folder/tests/testthat/test-shinytest2.R rename to tests/end2end/app/fake_folder/tests/testthat/test-shinytest2.R diff --git a/.github/workflows/tests/app/global.R b/tests/end2end/app/global.R similarity index 100% rename from .github/workflows/tests/app/global.R rename to tests/end2end/app/global.R diff --git a/.github/workflows/tests/app/server.R b/tests/end2end/app/server.R similarity index 100% rename from .github/workflows/tests/app/server.R rename to tests/end2end/app/server.R diff --git a/.github/workflows/tests/app/tests/testthat.R b/tests/end2end/app/tests/testthat.R similarity index 100% rename from .github/workflows/tests/app/tests/testthat.R rename to tests/end2end/app/tests/testthat.R diff --git a/.github/workflows/tests/app/tests/testthat/setup.R b/tests/end2end/app/tests/testthat/setup.R similarity index 100% rename from .github/workflows/tests/app/tests/testthat/setup.R rename to tests/end2end/app/tests/testthat/setup.R diff --git a/.github/workflows/tests/app/tests/testthat/test-shinytest2.R b/tests/end2end/app/tests/testthat/test-shinytest2.R similarity index 100% rename from .github/workflows/tests/app/tests/testthat/test-shinytest2.R rename to tests/end2end/app/tests/testthat/test-shinytest2.R diff --git a/.github/workflows/tests/app/ui.R b/tests/end2end/app/ui.R similarity index 100% rename from .github/workflows/tests/app/ui.R rename to tests/end2end/app/ui.R diff --git a/.github/workflows/tests/cypress_tests1.js b/tests/end2end/cypress_tests1.js similarity index 100% rename from .github/workflows/tests/cypress_tests1.js rename to tests/end2end/cypress_tests1.js diff --git a/.github/workflows/tests/cypress_tests2.js b/tests/end2end/cypress_tests2.js similarity index 100% rename from .github/workflows/tests/cypress_tests2.js rename to tests/end2end/cypress_tests2.js diff --git a/.github/workflows/tests/run_tests.R b/tests/end2end/run_tests.R similarity index 100% rename from .github/workflows/tests/run_tests.R rename to tests/end2end/run_tests.R diff --git a/.github/workflows/tests/setting_branches.sh b/tests/end2end/setting_branches.sh similarity index 100% rename from .github/workflows/tests/setting_branches.sh rename to tests/end2end/setting_branches.sh From df451ab2019022b269ee8e7a2925bb1c7e87d9bf Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 16:58:21 +0200 Subject: [PATCH 102/225] Updating tests based on the new arguments of the package --- .github/workflows/tests.yml | 32 ++++++++++++++++++++++++-------- tests/end2end/run_tests.R | 17 +++++++++++++---- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d3de322..2fe1e67 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,32 +49,48 @@ jobs: - name: Check basic functionality - Cypress run: | - Rscript ../run_tests.R cypress master,develop ../cypress_tests1.js FALSE + Rscript ../run_tests.R cypress master,develop ../cypress_tests1.js NULL FALSE 1 - name: Check basic functionality - shinytest2 run: | - Rscript ../run_tests.R shinytest2 master,develop tests/ FALSE + Rscript ../run_tests.R shinytest2 master,develop tests/ FALSE 1 - name: Check if it fails when renv not present - Cypress run: | - Rscript ../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 ../cypress_tests1.js FALSE + Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 ../cypress_tests1.js NULL FALSE 1 - name: Check if it fails when renv not present - shinytest2 run: | - Rscript ../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests FALSE + Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests NULL FALSE 1 - name: Check if it can handle renv - Cypress run: | - Rscript ../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 ../cypress_tests1.js TRUE + Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 ../cypress_tests1.js NULL TRUE 1 - name: Check if it can handle renv - shinytest2 run: | - Rscript ../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests TRUE + Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests NULL TRUE 1 - name: Check if it can handle multiple files - Cypress run: | - Rscript ../run_tests.R cypress renv_shiny1,renv_shiny2 ../cypress_tests1.js,../cypress_tests2.js TRUE + Rscript ./../run_tests.R cypress renv_shiny1,renv_shiny2 ../cypress_tests1.js,../cypress_tests2.js NULL TRUE 1 - name: Check if it can handle multiple files - shinytest2 run: | - Rscript ../run_tests.R shinytest2 renv_shiny1,renv_shiny2 tests,fake_folder/tests TRUE + Rscript ./../run_tests.R shinytest2 renv_shiny1,renv_shiny2 tests,fake_folder/tests NULL TRUE 1 + + - name: Check if we can replicate tests - Cypress + run | + Rscript ./../run_tests.R cypress master,develop ../cypress_tests1.js,../cypress_tests2.js0 NULL FALSE 2 + + - name: Check if we can replicate tests - shinytest2 + run | + Rscript ./../run_tests.R shinytest2 master,develop tests,fake_folder/tests NULL FALSE 2 + + - name: Check if we can run tests based on file patterns - Cypress + run | + Rscript ./../run_tests.R cypress master,develop ../cypress_tests1.js,../cypress_tests2.js0 use_this_one[0-9] FALSE 2 + + - name: Check if we can run tests based on file patterns - shinytest2 + run | + Rscript ./../run_tests.R shinytest2 master,develop tests,fake_folder/tests this_one FALSE 2 diff --git a/tests/end2end/run_tests.R b/tests/end2end/run_tests.R index 50a846b..f6fb740 100644 --- a/tests/end2end/run_tests.R +++ b/tests/end2end/run_tests.R @@ -10,33 +10,42 @@ library(shiny.performance) # commits to compare type <- args[[1]] commit_list <- args[[2]] -tests <- args[[3]] -use_renv <- args[[4]] +dir <- args[[3]] +pattern <- args[[4]] +use_renv <- args[[5]] +n_rep <- args[[6]] if (type == "cypress") { # run performance check using Cypress out <- shiny.performance::performance_tests( commit_list = commit_list, - cypress_file = tests, + cypress_dir = dir, + tests_pattern = pattern, app_dir = getwd(), use_renv = use_renv, renv_prompt = FALSE, port = 3333, + n_rep = n_rep, debug = FALSE ) } else { # run performance check using shinytest2 out <- shiny.performance::performance_tests( commit_list = commit_list, - shinytest2_dir = tests, + shinytest2_dir = dir, + tests_pattern = pattern, app_dir = getwd(), use_renv = use_renv, renv_prompt = FALSE, port = 3333, + n_rep = n_rep, debug = FALSE ) } # checks stopifnot(length(out) == length(commit_list)) +stopifnot(length(out[[1]]) >= n_rep) + +# deactivate renv renv::deactivate() From 10ee824af5abdda32d9866aa007a45cb5771ef98 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 17:08:13 +0200 Subject: [PATCH 103/225] Running also on pull requests --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2fe1e67..9db8850 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -on: push +on: [push, pull_request] name: package-usage-tests From ef9d4034b28f1b2e87d7ffd2c9c20a781ce7991b Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 17:34:09 +0200 Subject: [PATCH 104/225] Adding missing column --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9db8850..9956f4f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -80,17 +80,17 @@ jobs: Rscript ./../run_tests.R shinytest2 renv_shiny1,renv_shiny2 tests,fake_folder/tests NULL TRUE 1 - name: Check if we can replicate tests - Cypress - run | + run: | Rscript ./../run_tests.R cypress master,develop ../cypress_tests1.js,../cypress_tests2.js0 NULL FALSE 2 - name: Check if we can replicate tests - shinytest2 - run | + run: | Rscript ./../run_tests.R shinytest2 master,develop tests,fake_folder/tests NULL FALSE 2 - name: Check if we can run tests based on file patterns - Cypress - run | + run: | Rscript ./../run_tests.R cypress master,develop ../cypress_tests1.js,../cypress_tests2.js0 use_this_one[0-9] FALSE 2 - name: Check if we can run tests based on file patterns - shinytest2 - run | + run: | Rscript ./../run_tests.R shinytest2 master,develop tests,fake_folder/tests this_one FALSE 2 From f4c03a0619b655f22bcc9a73c1f1dba29c75dadb Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 17:35:04 +0200 Subject: [PATCH 105/225] Do not trigger on PR --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9956f4f..df412fe 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -on: [push, pull_request] +on: push name: package-usage-tests From 883a7f14347faff8da741cab6b78fe2b687f5ee5 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 17:50:25 +0200 Subject: [PATCH 106/225] debug messages --- tests/end2end/run_tests.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/end2end/run_tests.R b/tests/end2end/run_tests.R index f6fb740..84a5bc6 100644 --- a/tests/end2end/run_tests.R +++ b/tests/end2end/run_tests.R @@ -26,7 +26,7 @@ if (type == "cypress") { renv_prompt = FALSE, port = 3333, n_rep = n_rep, - debug = FALSE + debug = TRUE ) } else { # run performance check using shinytest2 From 27870c42d1ffa76746083ec0c31f02ca22f1f968 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 19:32:44 +0200 Subject: [PATCH 107/225] Updating tests after merge with develop --- .github/workflows/tests.yml | 24 +++++++-------- tests/end2end/app/.gitignore | 49 +++++++++++++++++++++++++++++++ tests/end2end/run_tests.R | 6 ++-- tests/end2end/setting_branches.sh | 1 - 4 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 tests/end2end/app/.gitignore diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index df412fe..3a3bf46 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,48 +49,48 @@ jobs: - name: Check basic functionality - Cypress run: | - Rscript ../run_tests.R cypress master,develop ../cypress_tests1.js NULL FALSE 1 + Rscript ../run_tests.R cypress master,develop tests/cypress use_this_one_1 FALSE 1 - name: Check basic functionality - shinytest2 run: | - Rscript ../run_tests.R shinytest2 master,develop tests/ FALSE 1 + Rscript ../run_tests.R shinytest2 master,develop tests use_this_one_1 FALSE 1 - name: Check if it fails when renv not present - Cypress run: | - Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 ../cypress_tests1.js NULL FALSE 1 + Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 tests/cypress use_this_one_1 FALSE 1 - name: Check if it fails when renv not present - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests NULL FALSE 1 + Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests use_this_one_1 FALSE 1 - name: Check if it can handle renv - Cypress run: | - Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 ../cypress_tests1.js NULL TRUE 1 + Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 tests/cypress use_this_one_1 TRUE 1 - name: Check if it can handle renv - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests NULL TRUE 1 + Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests use_this_one_1 TRUE 1 - name: Check if it can handle multiple files - Cypress run: | - Rscript ./../run_tests.R cypress renv_shiny1,renv_shiny2 ../cypress_tests1.js,../cypress_tests2.js NULL TRUE 1 + Rscript ./../run_tests.R cypress renv_shiny1,renv_shiny2 tests/cypress,fake_folder/tests/cypress use_this_one_1 TRUE 1 - name: Check if it can handle multiple files - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 renv_shiny1,renv_shiny2 tests,fake_folder/tests NULL TRUE 1 + Rscript ./../run_tests.R shinytest2 renv_shiny1,renv_shiny2 tests,fake_folder/tests use_this_one_1 TRUE 1 - name: Check if we can replicate tests - Cypress run: | - Rscript ./../run_tests.R cypress master,develop ../cypress_tests1.js,../cypress_tests2.js0 NULL FALSE 2 + Rscript ./../run_tests.R cypress master,develop tests/cypress use_this_one_1 FALSE 2 - name: Check if we can replicate tests - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 master,develop tests,fake_folder/tests NULL FALSE 2 + Rscript ./../run_tests.R shinytest2 master,develop tests use_this_one_1 FALSE 2 - name: Check if we can run tests based on file patterns - Cypress run: | - Rscript ./../run_tests.R cypress master,develop ../cypress_tests1.js,../cypress_tests2.js0 use_this_one[0-9] FALSE 2 + Rscript ./../run_tests.R cypress master,develop tests/cypress use_this_one_[0-9] FALSE 1 - name: Check if we can run tests based on file patterns - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 master,develop tests,fake_folder/tests this_one FALSE 2 + Rscript ./../run_tests.R shinytest2 master,develop tests use_this_one_[0-9] FALSE 1 diff --git a/tests/end2end/app/.gitignore b/tests/end2end/app/.gitignore new file mode 100644 index 0000000..47f4f74 --- /dev/null +++ b/tests/end2end/app/.gitignore @@ -0,0 +1,49 @@ +# History files +.Rhistory +.Rapp.history + +# Session Data files +.RData + +# User-specific files +.Ruserdata + +# Example code in package build process +*-Ex.R + +# Output files from R CMD build +/*.tar.gz + +# Output files from R CMD check +/*.Rcheck/ + + # RStudio files + .Rproj.user/ + + # produced vignettes + vignettes/*.html +vignettes/*.pdf + +# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 +.httr-oauth + +# knitr and R markdown default cache directories +*_cache/ + /cache/ + + # Temporary files created by R markdown + *.utf8.md +*.knit.md + +# R Environment Variables +.Renviron +__pycache__ +.Rproj.user +TODO.txt + +# csv opened files +.~lock* + + renv/ + renv.lock +.Rprofile diff --git a/tests/end2end/run_tests.R b/tests/end2end/run_tests.R index 84a5bc6..c14601e 100644 --- a/tests/end2end/run_tests.R +++ b/tests/end2end/run_tests.R @@ -12,8 +12,8 @@ type <- args[[1]] commit_list <- args[[2]] dir <- args[[3]] pattern <- args[[4]] -use_renv <- args[[5]] -n_rep <- args[[6]] +use_renv <- as.logical(args[[5]]) +n_rep <- as.integer(args[[6]]) if (type == "cypress") { # run performance check using Cypress @@ -26,7 +26,7 @@ if (type == "cypress") { renv_prompt = FALSE, port = 3333, n_rep = n_rep, - debug = TRUE + debug = FALSE ) } else { # run performance check using shinytest2 diff --git a/tests/end2end/setting_branches.sh b/tests/end2end/setting_branches.sh index 376d518..e15d555 100644 --- a/tests/end2end/setting_branches.sh +++ b/tests/end2end/setting_branches.sh @@ -10,7 +10,6 @@ git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" # STANDARD FUNCTIONALITIES ## master -echo renv/* > .gitignore git add . git commit -m "first commit" From 47d74ba3185217e08a765cbea26f6ac3acc7810d Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 19:37:46 +0200 Subject: [PATCH 108/225] Debug mode --- tests/end2end/run_tests.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/end2end/run_tests.R b/tests/end2end/run_tests.R index c14601e..1c6891a 100644 --- a/tests/end2end/run_tests.R +++ b/tests/end2end/run_tests.R @@ -26,7 +26,7 @@ if (type == "cypress") { renv_prompt = FALSE, port = 3333, n_rep = n_rep, - debug = FALSE + debug = TRUE ) } else { # run performance check using shinytest2 From b0465a8fb301744f0f93635fd548a7fa19f84a48 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 19:38:43 +0200 Subject: [PATCH 109/225] Decreasing time for testing --- tests/end2end/app/server.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/end2end/app/server.R b/tests/end2end/app/server.R index 12428e6..84b0505 100644 --- a/tests/end2end/app/server.R +++ b/tests/end2end/app/server.R @@ -2,7 +2,7 @@ function(input, output, session) { # Sys.sleep react1 <- eventReactive(input$run1, { out <- system.time( - Sys.sleep(6) + Sys.sleep(0.1) ) return(out[3]) @@ -10,7 +10,7 @@ function(input, output, session) { react2 <- eventReactive(input$run2, { out <- system.time( - Sys.sleep(3) + Sys.sleep(0.1) ) return(out[3]) @@ -18,7 +18,7 @@ function(input, output, session) { react3 <- eventReactive(input$run3, { out <- system.time( - Sys.sleep(1) + Sys.sleep(0.1) ) return(out[1]) From c6e770cdd8a7cf9d8060d290c0cf8878f5f57d0e Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 19:50:44 +0200 Subject: [PATCH 110/225] Updating structure and renaming files --- .../tests/cypress/cypress_use_this_one_1.js} | 0 .../tests/cypress/cypress_use_this_one_2.js} | 0 ...est-shinytest2.R => test-use_this_one_1.R} | 0 .../tests/testthat/test-use_this_one_2.R} | 0 .../app/tests/cypress/cypress_use_this_one_1 | 30 +++++++++++++++++++ .../app/tests/cypress/cypress_use_this_one_2 | 30 +++++++++++++++++++ .../app/tests/testthat/test-use_this_one_1.R | 21 +++++++++++++ .../app/tests/testthat/test-use_this_one_2.R | 21 +++++++++++++ tests/end2end/run_tests.R | 2 +- 9 files changed, 103 insertions(+), 1 deletion(-) rename tests/end2end/{cypress_tests1.js => app/fake_folder/tests/cypress/cypress_use_this_one_1.js} (100%) rename tests/end2end/{cypress_tests2.js => app/fake_folder/tests/cypress/cypress_use_this_one_2.js} (100%) rename tests/end2end/app/fake_folder/tests/testthat/{test-shinytest2.R => test-use_this_one_1.R} (100%) rename tests/end2end/app/{tests/testthat/test-shinytest2.R => fake_folder/tests/testthat/test-use_this_one_2.R} (100%) create mode 100644 tests/end2end/app/tests/cypress/cypress_use_this_one_1 create mode 100644 tests/end2end/app/tests/cypress/cypress_use_this_one_2 create mode 100644 tests/end2end/app/tests/testthat/test-use_this_one_1.R create mode 100644 tests/end2end/app/tests/testthat/test-use_this_one_2.R diff --git a/tests/end2end/cypress_tests1.js b/tests/end2end/app/fake_folder/tests/cypress/cypress_use_this_one_1.js similarity index 100% rename from tests/end2end/cypress_tests1.js rename to tests/end2end/app/fake_folder/tests/cypress/cypress_use_this_one_1.js diff --git a/tests/end2end/cypress_tests2.js b/tests/end2end/app/fake_folder/tests/cypress/cypress_use_this_one_2.js similarity index 100% rename from tests/end2end/cypress_tests2.js rename to tests/end2end/app/fake_folder/tests/cypress/cypress_use_this_one_2.js diff --git a/tests/end2end/app/fake_folder/tests/testthat/test-shinytest2.R b/tests/end2end/app/fake_folder/tests/testthat/test-use_this_one_1.R similarity index 100% rename from tests/end2end/app/fake_folder/tests/testthat/test-shinytest2.R rename to tests/end2end/app/fake_folder/tests/testthat/test-use_this_one_1.R diff --git a/tests/end2end/app/tests/testthat/test-shinytest2.R b/tests/end2end/app/fake_folder/tests/testthat/test-use_this_one_2.R similarity index 100% rename from tests/end2end/app/tests/testthat/test-shinytest2.R rename to tests/end2end/app/fake_folder/tests/testthat/test-use_this_one_2.R diff --git a/tests/end2end/app/tests/cypress/cypress_use_this_one_1 b/tests/end2end/app/tests/cypress/cypress_use_this_one_1 new file mode 100644 index 0000000..3943a97 --- /dev/null +++ b/tests/end2end/app/tests/cypress/cypress_use_this_one_1 @@ -0,0 +1,30 @@ +describe('Cypress test', () => { + // Test that the app starts at all + // Also it is needed to start other tests + it('The app starts', () => { + cy.visit('/'); + }); + + // Test how long it takes to wait for out1 + it('Out1 time elapsed', () => { + cy.get('#run1').click(); + cy.get('#out1', {timeout: 10000}).should('be.visible'); + }); + + // Test how long it takes to wait for out2 + it('Out2 time elapsed', () => { + cy.get('#run2').click(); + cy.get('#out2', {timeout: 10000}).should('be.visible'); + }); + + // Test how long it takes to wait for out3 + it('Out3 time elapsed', () => { + cy.get('#run3').click(); + cy.get('#out3', {timeout: 10000}).should('be.visible'); + }); + + // Test if we have a title + it('App has a title', () => { + cy.contains('Measuring time in different commits').should('be.visible'); + }); +}); diff --git a/tests/end2end/app/tests/cypress/cypress_use_this_one_2 b/tests/end2end/app/tests/cypress/cypress_use_this_one_2 new file mode 100644 index 0000000..3943a97 --- /dev/null +++ b/tests/end2end/app/tests/cypress/cypress_use_this_one_2 @@ -0,0 +1,30 @@ +describe('Cypress test', () => { + // Test that the app starts at all + // Also it is needed to start other tests + it('The app starts', () => { + cy.visit('/'); + }); + + // Test how long it takes to wait for out1 + it('Out1 time elapsed', () => { + cy.get('#run1').click(); + cy.get('#out1', {timeout: 10000}).should('be.visible'); + }); + + // Test how long it takes to wait for out2 + it('Out2 time elapsed', () => { + cy.get('#run2').click(); + cy.get('#out2', {timeout: 10000}).should('be.visible'); + }); + + // Test how long it takes to wait for out3 + it('Out3 time elapsed', () => { + cy.get('#run3').click(); + cy.get('#out3', {timeout: 10000}).should('be.visible'); + }); + + // Test if we have a title + it('App has a title', () => { + cy.contains('Measuring time in different commits').should('be.visible'); + }); +}); diff --git a/tests/end2end/app/tests/testthat/test-use_this_one_1.R b/tests/end2end/app/tests/testthat/test-use_this_one_1.R new file mode 100644 index 0000000..7c504e7 --- /dev/null +++ b/tests/end2end/app/tests/testthat/test-use_this_one_1.R @@ -0,0 +1,21 @@ +library(shinytest2) + +test_that("{shinytest2} recording: test1", { + app <- AppDriver$new(name = "test1", height = 975, width = 1619) + app$click("run1") + app$expect_values(output = "out1") +}) + + +test_that("{shinytest2} recording: test2", { + app <- AppDriver$new(name = "test2", height = 975, width = 1619) + app$click("run2") + app$expect_values(output = "out2") +}) + + +test_that("{shinytest2} recording: test3", { + app <- AppDriver$new(name = "test3", height = 975, width = 1619) + app$click("run3") + app$expect_values(output = "out3") +}) diff --git a/tests/end2end/app/tests/testthat/test-use_this_one_2.R b/tests/end2end/app/tests/testthat/test-use_this_one_2.R new file mode 100644 index 0000000..7c504e7 --- /dev/null +++ b/tests/end2end/app/tests/testthat/test-use_this_one_2.R @@ -0,0 +1,21 @@ +library(shinytest2) + +test_that("{shinytest2} recording: test1", { + app <- AppDriver$new(name = "test1", height = 975, width = 1619) + app$click("run1") + app$expect_values(output = "out1") +}) + + +test_that("{shinytest2} recording: test2", { + app <- AppDriver$new(name = "test2", height = 975, width = 1619) + app$click("run2") + app$expect_values(output = "out2") +}) + + +test_that("{shinytest2} recording: test3", { + app <- AppDriver$new(name = "test3", height = 975, width = 1619) + app$click("run3") + app$expect_values(output = "out3") +}) diff --git a/tests/end2end/run_tests.R b/tests/end2end/run_tests.R index 1c6891a..c14601e 100644 --- a/tests/end2end/run_tests.R +++ b/tests/end2end/run_tests.R @@ -26,7 +26,7 @@ if (type == "cypress") { renv_prompt = FALSE, port = 3333, n_rep = n_rep, - debug = TRUE + debug = FALSE ) } else { # run performance check using shinytest2 From a27c23cb7dd441df59ec6419f0416f1cb702adac Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 19:57:17 +0200 Subject: [PATCH 111/225] Testing --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3a3bf46..cd9b265 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,7 +49,7 @@ jobs: - name: Check basic functionality - Cypress run: | - Rscript ../run_tests.R cypress master,develop tests/cypress use_this_one_1 FALSE 1 + Rscript ./../run_tests.R cypress master,develop tests/cypress use_this_one_1 FALSE 1 - name: Check basic functionality - shinytest2 run: | From b13bfa4bb9d7b6ca3780154043e7d6cf6ef3b3fa Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 19:57:32 +0200 Subject: [PATCH 112/225] Testing --- tests/end2end/run_tests.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/end2end/run_tests.R b/tests/end2end/run_tests.R index c14601e..1c6891a 100644 --- a/tests/end2end/run_tests.R +++ b/tests/end2end/run_tests.R @@ -26,7 +26,7 @@ if (type == "cypress") { renv_prompt = FALSE, port = 3333, n_rep = n_rep, - debug = FALSE + debug = TRUE ) } else { # run performance check using shinytest2 From 3c03a275e7a4220ef6ec07dd8730b980611743d6 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 20:06:02 +0200 Subject: [PATCH 113/225] Testing --- .github/workflows/tests.yml | 24 ++++++++++++------------ tests/end2end/run_tests.R | 4 +++- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cd9b265..c8aa2f1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,48 +49,48 @@ jobs: - name: Check basic functionality - Cypress run: | - Rscript ./../run_tests.R cypress master,develop tests/cypress use_this_one_1 FALSE 1 + Rscript ../run_tests.R cypress master,develop tests/cypress/ use_this_one_1 FALSE 1 - name: Check basic functionality - shinytest2 run: | - Rscript ../run_tests.R shinytest2 master,develop tests use_this_one_1 FALSE 1 + Rscript ../run_tests.R shinytest2 master,develop tests/ use_this_one_1 FALSE 1 - name: Check if it fails when renv not present - Cypress run: | - Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 tests/cypress use_this_one_1 FALSE 1 + Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 tests/cypress/ use_this_one_1 FALSE 1 - name: Check if it fails when renv not present - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests use_this_one_1 FALSE 1 + Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests/ use_this_one_1 FALSE 1 - name: Check if it can handle renv - Cypress run: | - Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 tests/cypress use_this_one_1 TRUE 1 + Rscript ./../run_tests.R cypress renv_missing,renv_shiny1,renv_shiny2 tests/cypress/ use_this_one_1 TRUE 1 - name: Check if it can handle renv - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests use_this_one_1 TRUE 1 + Rscript ./../run_tests.R shinytest2 renv_missing,renv_shiny1,renv_shiny2 tests/ use_this_one_1 TRUE 1 - name: Check if it can handle multiple files - Cypress run: | - Rscript ./../run_tests.R cypress renv_shiny1,renv_shiny2 tests/cypress,fake_folder/tests/cypress use_this_one_1 TRUE 1 + Rscript ./../run_tests.R cypress renv_shiny1,renv_shiny2 tests/cypress/,fake_folder/tests/cypress/ use_this_one_1 TRUE 1 - name: Check if it can handle multiple files - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 renv_shiny1,renv_shiny2 tests,fake_folder/tests use_this_one_1 TRUE 1 + Rscript ./../run_tests.R shinytest2 renv_shiny1,renv_shiny2 tests/,fake_folder/tests/ use_this_one_1 TRUE 1 - name: Check if we can replicate tests - Cypress run: | - Rscript ./../run_tests.R cypress master,develop tests/cypress use_this_one_1 FALSE 2 + Rscript ./../run_tests.R cypress master,develop tests/cypress/ use_this_one_1 FALSE 2 - name: Check if we can replicate tests - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 master,develop tests use_this_one_1 FALSE 2 + Rscript ./../run_tests.R shinytest2 master,develop tests/ use_this_one_1 FALSE 2 - name: Check if we can run tests based on file patterns - Cypress run: | - Rscript ./../run_tests.R cypress master,develop tests/cypress use_this_one_[0-9] FALSE 1 + Rscript ./../run_tests.R cypress master,develop tests/cypress/ use_this_one_[0-9] FALSE 1 - name: Check if we can run tests based on file patterns - shinytest2 run: | - Rscript ./../run_tests.R shinytest2 master,develop tests use_this_one_[0-9] FALSE 1 + Rscript ./../run_tests.R shinytest2 master,develop tests/ use_this_one_[0-9] FALSE 1 diff --git a/tests/end2end/run_tests.R b/tests/end2end/run_tests.R index 1c6891a..109f814 100644 --- a/tests/end2end/run_tests.R +++ b/tests/end2end/run_tests.R @@ -2,6 +2,8 @@ args <- commandArgs(trailingOnly = TRUE) args <- strsplit(args, ",") +args + # packages library(shiny) library(testthat) @@ -26,7 +28,7 @@ if (type == "cypress") { renv_prompt = FALSE, port = 3333, n_rep = n_rep, - debug = TRUE + debug = FALSE ) } else { # run performance check using shinytest2 From 74ccad78dd4514f75e0509a155f35e77530c31c9 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 20:10:24 +0200 Subject: [PATCH 114/225] Print files on the parent folder --- .github/workflows/tests.yml | 3 ++- tests/end2end/run_tests.R | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c8aa2f1..88a72c8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,7 +4,7 @@ name: package-usage-tests defaults: run: - working-directory: ./tests/end2end/app + working-directory: ./tests/end2end/app/ jobs: main: @@ -46,6 +46,7 @@ jobs: - name: Create app structure run: | bash ./../setting_branches.sh + ls ../ - name: Check basic functionality - Cypress run: | diff --git a/tests/end2end/run_tests.R b/tests/end2end/run_tests.R index 109f814..c14601e 100644 --- a/tests/end2end/run_tests.R +++ b/tests/end2end/run_tests.R @@ -2,8 +2,6 @@ args <- commandArgs(trailingOnly = TRUE) args <- strsplit(args, ",") -args - # packages library(shiny) library(testthat) From b9c72addd44740a22b079c6273e10457e0b5fe4d Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Wed, 26 Oct 2022 20:13:41 +0200 Subject: [PATCH 115/225] ignoring only renv/ folder --- .github/workflows/tests.yml | 1 - tests/end2end/app/.gitignore | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 88a72c8..3cfafb8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,7 +46,6 @@ jobs: - name: Create app structure run: | bash ./../setting_branches.sh - ls ../ - name: Check basic functionality - Cypress run: | diff --git a/tests/end2end/app/.gitignore b/tests/end2end/app/.gitignore index 47f4f74..c131308 100644 --- a/tests/end2end/app/.gitignore +++ b/tests/end2end/app/.gitignore @@ -44,6 +44,4 @@ TODO.txt # csv opened files .~lock* - renv/ - renv.lock -.Rprofile +renv/ From 8d42520167362988e0ecbc32d50071bbf59e0df0 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 08:41:06 +0200 Subject: [PATCH 116/225] Allowing debug --- tests/end2end/run_tests.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/end2end/run_tests.R b/tests/end2end/run_tests.R index c14601e..1c6891a 100644 --- a/tests/end2end/run_tests.R +++ b/tests/end2end/run_tests.R @@ -26,7 +26,7 @@ if (type == "cypress") { renv_prompt = FALSE, port = 3333, n_rep = n_rep, - debug = FALSE + debug = TRUE ) } else { # run performance check using shinytest2 From ef9e7d6074f45b5b09cd7169c548c1bcbfbe0e70 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 08:59:47 +0200 Subject: [PATCH 117/225] Adding .js to cypress files --- .../cypress/{cypress_use_this_one_1 => cypress_use_this_one_1.js} | 0 .../cypress/{cypress_use_this_one_2 => cypress_use_this_one_2.js} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/end2end/app/tests/cypress/{cypress_use_this_one_1 => cypress_use_this_one_1.js} (100%) rename tests/end2end/app/tests/cypress/{cypress_use_this_one_2 => cypress_use_this_one_2.js} (100%) diff --git a/tests/end2end/app/tests/cypress/cypress_use_this_one_1 b/tests/end2end/app/tests/cypress/cypress_use_this_one_1.js similarity index 100% rename from tests/end2end/app/tests/cypress/cypress_use_this_one_1 rename to tests/end2end/app/tests/cypress/cypress_use_this_one_1.js diff --git a/tests/end2end/app/tests/cypress/cypress_use_this_one_2 b/tests/end2end/app/tests/cypress/cypress_use_this_one_2.js similarity index 100% rename from tests/end2end/app/tests/cypress/cypress_use_this_one_2 rename to tests/end2end/app/tests/cypress/cypress_use_this_one_2.js From b55d6775bc181c530dc11b4a8265637a7577ede4 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 09:00:00 +0200 Subject: [PATCH 118/225] Remove debug --- tests/end2end/run_tests.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/end2end/run_tests.R b/tests/end2end/run_tests.R index 1c6891a..c14601e 100644 --- a/tests/end2end/run_tests.R +++ b/tests/end2end/run_tests.R @@ -26,7 +26,7 @@ if (type == "cypress") { renv_prompt = FALSE, port = 3333, n_rep = n_rep, - debug = TRUE + debug = FALSE ) } else { # run performance check using shinytest2 From ac8b4000892ab9fc709441e6cf4dff687e7444a6 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 11:27:58 +0200 Subject: [PATCH 119/225] Adding some tests for utils and utils_shinytest2 --- tests/testthat/test-utils.R | 9 +++++++++ tests/testthat/test-utils_shinytest2.R | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index e69de29..7cfa014 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -0,0 +1,9 @@ +test_that("Check if commit date is in fact a date", { + commit_date <- get_commit_date(branch = "main") + expect_s3_class(object = commit_date, class = "POSIXct") +}) + +test_that("Check if we are able to get the commit hash", { + commit_hash <- shiny.performance:::get_commit_hash() + expect_true(is.character(commit_hash)) +}) diff --git a/tests/testthat/test-utils_shinytest2.R b/tests/testthat/test-utils_shinytest2.R index e69de29..410bd3e 100644 --- a/tests/testthat/test-utils_shinytest2.R +++ b/tests/testthat/test-utils_shinytest2.R @@ -0,0 +1,21 @@ +test_that("Check if we are able to move files properly", { + tmp_dir <- tempdir() + + tmp_dir1 <- file.path(tmp_dir, "folder1") + dir.create(tmp_dir1, showWarnings = FALSE) + tmp_dir2 <- file.path(tmp_dir, "folder2") + dir.create(tmp_dir2, showWarnings = FALSE) + + shinytest2_dir <- file.path(tmp_dir1, "tst") + shinytest2_dir_copy <- file.path(tmp_dir2, "tst") + dir.create(path = shinytest2_dir, showWarnings = FALSE) + + move_shinytest2_tests(project_path = tmp_dir2, shinytest2_dir = shinytest2_dir) + + expect_true(file.exists(shinytest2_dir_copy)) +}) + +test_that("Check if we are able to create shinytest2 structure", { + tmp_dir <- create_shinytest2_structure(app_dir = ".") + expect_true(file.exists(file.path(tmp_dir, "app.R"))) +}) From 3b689df7354537832fe3d6fb4aac7858c055f055 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 12:33:19 +0200 Subject: [PATCH 120/225] Adding some tests for utils and cypress --- DESCRIPTION | 1 - tests/testthat/test-utils.R | 14 ++++++++++++-- tests/testthat/test-utils_cypress.R | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 4594e07..97da3f0 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -30,4 +30,3 @@ Imports: shinytest2, stringr, testthat -Config/testthat/edition: 3 diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 7cfa014..09576cf 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -1,9 +1,19 @@ +tmp_dir <- tempdir() +command <- paste0("git init ", tmp_dir) +system(command = command) +command <- paste0("cd ", tmp_dir, "; git add .; git commit --allow-empty -n -m 'Initial commit'") +system(command = command) + +wd <- getwd() +setwd(tmp_dir) +on.exit(setwd(wd)) + test_that("Check if commit date is in fact a date", { - commit_date <- get_commit_date(branch = "main") + commit_date <- get_commit_date(branch = "master") expect_s3_class(object = commit_date, class = "POSIXct") }) test_that("Check if we are able to get the commit hash", { - commit_hash <- shiny.performance:::get_commit_hash() + commit_hash <- get_commit_hash() expect_true(is.character(commit_hash)) }) diff --git a/tests/testthat/test-utils_cypress.R b/tests/testthat/test-utils_cypress.R index 7a3333b..423f945 100644 --- a/tests/testthat/test-utils_cypress.R +++ b/tests/testthat/test-utils_cypress.R @@ -14,6 +14,7 @@ test_that("Check if we are able to copy file content from a file to another", { content_before <- "TEST" writeLines(text = content_before, con = tmp_file) + dir.create(file.path(tmp_dir, "tests", "cypress", "integration"), showWarnings = FALSE, recursive = TRUE) files <- create_cypress_tests(project_path = tmp_dir, cypress_file = tmp_file) content_after <- readLines(con = files$js_file, n = 1) @@ -29,7 +30,6 @@ test_that("Check whether we have are able to create Cypress structure correctly expect_true(file.exists(file.path(tmp_dir, "node"))) expect_true(file.exists(file.path(tmp_dir, "node", "root"))) - expect_true(file.exists(file.path(tmp_dir, "node", "root", "DESCRIPTION"))) expect_true(file.exists(file.path(tmp_dir, "tests"))) expect_true(file.exists(file.path(tmp_dir, "tests", "cypress", "plugins"))) }) From bb78ed2c08bba14281bbf5e42987595a29c38453 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 13:25:17 +0200 Subject: [PATCH 121/225] Improving cypress tests (lintr) --- tests/testthat/test-utils_cypress.R | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/tests/testthat/test-utils_cypress.R b/tests/testthat/test-utils_cypress.R index 423f945..faaa6f9 100644 --- a/tests/testthat/test-utils_cypress.R +++ b/tests/testthat/test-utils_cypress.R @@ -10,28 +10,18 @@ test_that("Check if we are able to add Cypress code to a txt file", { test_that("Check if we are able to copy file content from a file to another", { tmp_dir <- tempdir() - tmp_file <- tempfile(tmpdir = tmp_dir, fileext = ".txt") + tmp_file <- tempfile(tmpdir = tmp_dir, fileext = ".js") content_before <- "TEST" writeLines(text = content_before, con = tmp_file) - dir.create(file.path(tmp_dir, "tests", "cypress", "integration"), showWarnings = FALSE, recursive = TRUE) - files <- create_cypress_tests(project_path = tmp_dir, cypress_file = tmp_file) + integration_dir <- file.path(tmp_dir, "tests", "cypress", "integration") + dir.create(integration_dir, showWarnings = FALSE, recursive = TRUE) + files <- create_cypress_tests( + project_path = tmp_dir, + cypress_dir = tmp_dir, + tests_pattern = ".js" + ) content_after <- readLines(con = files$js_file, n = 1) expect_true(content_after == content_before) }) - -test_that("Check whether we have are able to create Cypress structure correctly or not", { - tmp_dir <- create_cypress_structure( - app_dir = getwd(), - port = 3333, - debug = FALSE - ) - - expect_true(file.exists(file.path(tmp_dir, "node"))) - expect_true(file.exists(file.path(tmp_dir, "node", "root"))) - expect_true(file.exists(file.path(tmp_dir, "tests"))) - expect_true(file.exists(file.path(tmp_dir, "tests", "cypress", "plugins"))) -}) - - From 2635107f345bb03794976d31e333aa00f654cc36 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 13:30:58 +0200 Subject: [PATCH 122/225] Avoiding tests that need git setup --- tests/testthat/test-utils.R | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 tests/testthat/test-utils.R diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R deleted file mode 100644 index 09576cf..0000000 --- a/tests/testthat/test-utils.R +++ /dev/null @@ -1,19 +0,0 @@ -tmp_dir <- tempdir() -command <- paste0("git init ", tmp_dir) -system(command = command) -command <- paste0("cd ", tmp_dir, "; git add .; git commit --allow-empty -n -m 'Initial commit'") -system(command = command) - -wd <- getwd() -setwd(tmp_dir) -on.exit(setwd(wd)) - -test_that("Check if commit date is in fact a date", { - commit_date <- get_commit_date(branch = "master") - expect_s3_class(object = commit_date, class = "POSIXct") -}) - -test_that("Check if we are able to get the commit hash", { - commit_hash <- get_commit_hash() - expect_true(is.character(commit_hash)) -}) From 36170265af9f441a5475054f5f027582e5e9bdb8 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 13:39:26 +0200 Subject: [PATCH 123/225] Fixing error message --- R/performance_tests.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/performance_tests.R b/R/performance_tests.R index 6fffe23..19c4364 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -18,6 +18,7 @@ #' @param n_rep Number of replications desired #' @param debug Logical. TRUE to display all the system messages on runtime #' +#' @importFrom glue glue #' @export performance_tests <- function( commit_list, @@ -49,7 +50,7 @@ performance_tests <- function( if (length(get(obj_name)) == 1) assign(obj_name, rep(get(obj_name), n_commits)) if (length(get(obj_name)) != n_commits) - stop("You must provide 1 or {n_commits} paths for {obj_name}") + stop(glue("You must provide 1 or {n_commits} paths for {obj_name}")) if (is.null(tests_pattern)) tests_pattern <- vector(mode = "list", length = n_commits) From 6f5b06c72d672f740b16d06695575dffa32a150f Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 13:46:10 +0200 Subject: [PATCH 124/225] shiny.performance to shiny.benchmark --- DESCRIPTION | 4 ++-- README.md | 2 +- shiny.performance.Rproj => shiny.benchmark.Rproj | 0 tests/end2end/run_tests.R | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) rename shiny.performance.Rproj => shiny.benchmark.Rproj (100%) diff --git a/DESCRIPTION b/DESCRIPTION index eaf2ff2..44b5444 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,4 +1,4 @@ -Package: shiny.performance +Package: shiny.benchmark Title: Compare performance of several versions of a shiny app Version: 0.1.1 Authors@R: @@ -10,7 +10,7 @@ Authors@R: ) Description: Compare performance of several versions of a shiny app based on commit hashs. License: LGPL-3 -URL: https://github.com/Appsilon/shiny.performance +URL: https://github.com/Appsilon/shiny.benchmark SystemRequirements: yarn 1.22.17 or higher, cypress 9.4.1 or higher, xvfb Encoding: UTF-8 LazyData: true diff --git a/README.md b/README.md index 4009b6c..842b2b3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# shiny.performance +# shiny.benchmark Tools to measure performance improvements in shiny apps diff --git a/shiny.performance.Rproj b/shiny.benchmark.Rproj similarity index 100% rename from shiny.performance.Rproj rename to shiny.benchmark.Rproj diff --git a/tests/end2end/run_tests.R b/tests/end2end/run_tests.R index c14601e..6652876 100644 --- a/tests/end2end/run_tests.R +++ b/tests/end2end/run_tests.R @@ -5,7 +5,7 @@ args <- strsplit(args, ",") # packages library(shiny) library(testthat) -library(shiny.performance) +library(shiny.benchmark) # commits to compare type <- args[[1]] @@ -17,7 +17,7 @@ n_rep <- as.integer(args[[6]]) if (type == "cypress") { # run performance check using Cypress - out <- shiny.performance::performance_tests( + out <- performance_tests( commit_list = commit_list, cypress_dir = dir, tests_pattern = pattern, @@ -30,7 +30,7 @@ if (type == "cypress") { ) } else { # run performance check using shinytest2 - out <- shiny.performance::performance_tests( + out <- performance_tests( commit_list = commit_list, shinytest2_dir = dir, tests_pattern = pattern, From 88de416b79536530afc1be1bbb281577ccc51ded Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 14:38:44 +0200 Subject: [PATCH 125/225] replacing performance by benchmark --- NAMESPACE | 6 +++--- R/{performance_tests.R => benchmark.R} | 6 +++--- R/{tests_cypress.R => benchmark_cypress.R} | 2 +- R/{tests_shinytest2.R => benchmark_shinytest2.R} | 2 +- man/{performance_tests.Rd => benchmark.Rd} | 8 ++++---- man/{ptest_cypress.Rd => benchmark_cypress.Rd} | 8 ++++---- man/{ptest_shinytest2.Rd => benchmark_shinytest2.Rd} | 8 ++++---- man/run_cypress_ptest.Rd | 2 +- man/run_shinytest2_ptest.Rd | 2 +- tests/end2end/run_tests.R | 4 ++-- 10 files changed, 24 insertions(+), 24 deletions(-) rename R/{performance_tests.R => benchmark.R} (96%) rename R/{tests_cypress.R => benchmark_cypress.R} (99%) rename R/{tests_shinytest2.R => benchmark_shinytest2.R} (99%) rename man/{performance_tests.Rd => benchmark.Rd} (91%) rename man/{ptest_cypress.Rd => benchmark_cypress.Rd} (90%) rename man/{ptest_shinytest2.Rd => benchmark_shinytest2.Rd} (89%) diff --git a/NAMESPACE b/NAMESPACE index 828c14d..fb2ff5c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,8 +1,8 @@ # Generated by roxygen2: do not edit by hand -export(performance_tests) -export(ptest_cypress) -export(ptest_shinytest2) +export(benchmark) +export(benchmark_cypress) +export(benchmark_shinytest2) export(run_cypress_ptest) export(run_shinytest2_ptest) importFrom(glue,glue) diff --git a/R/performance_tests.R b/R/benchmark.R similarity index 96% rename from R/performance_tests.R rename to R/benchmark.R index 6fffe23..6b93a00 100644 --- a/R/performance_tests.R +++ b/R/benchmark.R @@ -19,7 +19,7 @@ #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @export -performance_tests <- function( +benchmark <- function( commit_list, cypress_dir = NULL, shinytest2_dir = NULL, @@ -65,7 +65,7 @@ performance_tests <- function( # run tests if (type == "cypress") { - perf_list <- ptest_cypress( + perf_list <- benchmark_cypress( commit_list = commit_list, cypress_dir = cypress_dir, tests_pattern = tests_pattern, @@ -77,7 +77,7 @@ performance_tests <- function( debug = debug ) } else { - perf_list <- ptest_shinytest2( + perf_list <- benchmark_shinytest2( commit_list, shinytest2_dir, tests_pattern = tests_pattern, diff --git a/R/tests_cypress.R b/R/benchmark_cypress.R similarity index 99% rename from R/tests_cypress.R rename to R/benchmark_cypress.R index b9882f4..737b422 100644 --- a/R/tests_cypress.R +++ b/R/benchmark_cypress.R @@ -17,7 +17,7 @@ #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @export -ptest_cypress <- function( +benchmark_cypress <- function( commit_list, cypress_dir, tests_pattern, diff --git a/R/tests_shinytest2.R b/R/benchmark_shinytest2.R similarity index 99% rename from R/tests_shinytest2.R rename to R/benchmark_shinytest2.R index 01e7886..e3fd9af 100644 --- a/R/tests_shinytest2.R +++ b/R/benchmark_shinytest2.R @@ -16,7 +16,7 @@ #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @export -ptest_shinytest2 <- function( +benchmark_shinytest2 <- function( commit_list, shinytest2_dir, tests_pattern, diff --git a/man/performance_tests.Rd b/man/benchmark.Rd similarity index 91% rename from man/performance_tests.Rd rename to man/benchmark.Rd index ec8efe0..b21b421 100644 --- a/man/performance_tests.Rd +++ b/man/benchmark.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/performance_tests.R -\name{performance_tests} -\alias{performance_tests} +% Please edit documentation in R/benchmark.R +\name{benchmark} +\alias{benchmark} \title{Execute performance tests for a list of commits} \usage{ -performance_tests( +benchmark( commit_list, cypress_dir = NULL, shinytest2_dir = NULL, diff --git a/man/ptest_cypress.Rd b/man/benchmark_cypress.Rd similarity index 90% rename from man/ptest_cypress.Rd rename to man/benchmark_cypress.Rd index 049520a..123e346 100644 --- a/man/ptest_cypress.Rd +++ b/man/benchmark_cypress.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/tests_cypress.R -\name{ptest_cypress} -\alias{ptest_cypress} +% Please edit documentation in R/benchmark_cypress.R +\name{benchmark_cypress} +\alias{benchmark_cypress} \title{Run the performance test based on multiple commits using Cypress} \usage{ -ptest_cypress( +benchmark_cypress( commit_list, cypress_dir, tests_pattern, diff --git a/man/ptest_shinytest2.Rd b/man/benchmark_shinytest2.Rd similarity index 89% rename from man/ptest_shinytest2.Rd rename to man/benchmark_shinytest2.Rd index 0dc390e..952e632 100644 --- a/man/ptest_shinytest2.Rd +++ b/man/benchmark_shinytest2.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/tests_shinytest2.R -\name{ptest_shinytest2} -\alias{ptest_shinytest2} +% Please edit documentation in R/benchmark_shinytest2.R +\name{benchmark_shinytest2} +\alias{benchmark_shinytest2} \title{Run the performance test based on a multiple commits using shinytest2} \usage{ -ptest_shinytest2( +benchmark_shinytest2( commit_list, shinytest2_dir, tests_pattern, diff --git a/man/run_cypress_ptest.Rd b/man/run_cypress_ptest.Rd index 3547405..fc978db 100644 --- a/man/run_cypress_ptest.Rd +++ b/man/run_cypress_ptest.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/tests_cypress.R +% Please edit documentation in R/benchmark_cypress.R \name{run_cypress_ptest} \alias{run_cypress_ptest} \title{Run the performance test based on a single commit using Cypress} diff --git a/man/run_shinytest2_ptest.Rd b/man/run_shinytest2_ptest.Rd index fa7a520..a1e6662 100644 --- a/man/run_shinytest2_ptest.Rd +++ b/man/run_shinytest2_ptest.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/tests_shinytest2.R +% Please edit documentation in R/benchmark_shinytest2.R \name{run_shinytest2_ptest} \alias{run_shinytest2_ptest} \title{Run the performance test based on a single commit using shinytest2} diff --git a/tests/end2end/run_tests.R b/tests/end2end/run_tests.R index c14601e..bdfa453 100644 --- a/tests/end2end/run_tests.R +++ b/tests/end2end/run_tests.R @@ -17,7 +17,7 @@ n_rep <- as.integer(args[[6]]) if (type == "cypress") { # run performance check using Cypress - out <- shiny.performance::performance_tests( + out <- benchmark( commit_list = commit_list, cypress_dir = dir, tests_pattern = pattern, @@ -30,7 +30,7 @@ if (type == "cypress") { ) } else { # run performance check using shinytest2 - out <- shiny.performance::performance_tests( + out <- benchmark( commit_list = commit_list, shinytest2_dir = dir, tests_pattern = pattern, From 6218c409b81cd6719f995bf64309b3c358593a5b Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 14:58:31 +0200 Subject: [PATCH 126/225] Creating a class and a basic print method --- NAMESPACE | 2 ++ R/performance_tests.R | 62 +++++++++++++++++++++--------------- R/print.R | 20 ++++++++++++ R/shiny_benchmark-class.R | 18 +++++++++++ man/shiny_benchmark-class.Rd | 20 ++++++++++++ 5 files changed, 97 insertions(+), 25 deletions(-) create mode 100644 R/print.R create mode 100644 R/shiny_benchmark-class.R create mode 100644 man/shiny_benchmark-class.Rd diff --git a/NAMESPACE b/NAMESPACE index 828c14d..11f3a44 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -5,8 +5,10 @@ export(ptest_cypress) export(ptest_shinytest2) export(run_cypress_ptest) export(run_shinytest2_ptest) +exportClasses(shiny_benchmark) importFrom(glue,glue) importFrom(jsonlite,write_json) +importFrom(methods,new) importFrom(progress,progress_bar) importFrom(renv,activate) importFrom(renv,restore) diff --git a/R/performance_tests.R b/R/performance_tests.R index 6fffe23..ff67dc0 100644 --- a/R/performance_tests.R +++ b/R/performance_tests.R @@ -31,6 +31,9 @@ performance_tests <- function( n_rep = 1, debug = FALSE ) { + # Get the call parameters + call_benchmark <- match.call() + # Number of commits to test n_commits <- length(commit_list) @@ -64,30 +67,39 @@ performance_tests <- function( check_uncommitted_files() # run tests - if (type == "cypress") { - perf_list <- ptest_cypress( - commit_list = commit_list, - cypress_dir = cypress_dir, - tests_pattern = tests_pattern, - app_dir = app_dir, - port = port, - use_renv = use_renv, - renv_prompt = renv_prompt, - n_rep = n_rep, - debug = debug - ) - } else { - perf_list <- ptest_shinytest2( - commit_list, - shinytest2_dir, - tests_pattern = tests_pattern, - app_dir, - use_renv = use_renv, - renv_prompt = renv_prompt, - n_rep = n_rep, - debug = debug - ) - } + total_time <- system.time( + if (type == "cypress") { + perf_list <- ptest_cypress( + commit_list = commit_list, + cypress_dir = cypress_dir, + tests_pattern = tests_pattern, + app_dir = app_dir, + port = port, + use_renv = use_renv, + renv_prompt = renv_prompt, + n_rep = n_rep, + debug = debug + ) + } else { + perf_list <- ptest_shinytest2( + commit_list, + shinytest2_dir, + tests_pattern = tests_pattern, + app_dir, + use_renv = use_renv, + renv_prompt = renv_prompt, + n_rep = n_rep, + debug = debug + ) + } + ) + + out <- list( + call = call_benchmark, + time = total_time, + performance = perf_list + ) + class(out) <- "shiny_benchmark" - return(perf_list) + return(out) } diff --git a/R/print.R b/R/print.R new file mode 100644 index 0000000..56ed87c --- /dev/null +++ b/R/print.R @@ -0,0 +1,20 @@ +#' Print for shiny_benchmark class +#' +#' @param object shiny_benchmark object to print +#' +#' @method print shiny_benchmark +#' @export +print.shiny_benchmark <- function(object){ + cat('Shiny benchmark: \n') + cat('\n') + cat('Call:') + cat('\n') + print(object$call) + cat('\n') + cat('Total time ellapsed:') + cat('\n') + print(object$time[["elapsed"]]) + cat('\n') + cat('Fit measures: \n') + print(object$performance) +} diff --git a/R/shiny_benchmark-class.R b/R/shiny_benchmark-class.R new file mode 100644 index 0000000..e98cccb --- /dev/null +++ b/R/shiny_benchmark-class.R @@ -0,0 +1,18 @@ +#' @title An object of 'shiny_benchmark' class +#' +#' @slot call Function call +#' @slot time Time elapsed +#' @slot performance List of measuraments (one entry for each commit) +#' +#' @importFrom methods new +#' +#' @export + +shiny_benchmark_class <- setClass( + Class = 'shiny_benchmark', + representation( + call = 'call', + time = 'proc_time', + performance = 'list' + ) +) diff --git a/man/shiny_benchmark-class.Rd b/man/shiny_benchmark-class.Rd new file mode 100644 index 0000000..60483a8 --- /dev/null +++ b/man/shiny_benchmark-class.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/shiny_benchmark-class.R +\docType{class} +\name{shiny_benchmark-class} +\alias{shiny_benchmark-class} +\alias{shiny_benchmark_class} +\title{An object of 'shiny_benchmark' class} +\description{ +An object of 'shiny_benchmark' class +} +\section{Slots}{ + +\describe{ +\item{\code{call}}{Function call} + +\item{\code{time}}{Time elapsed} + +\item{\code{performance}}{List of measuraments (one entry for each commit)} +}} + From 11fdf28378f25d7d1e5218fad7d56171a2ddaf35 Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Wed, 26 Oct 2022 16:41:03 +0200 Subject: [PATCH 127/225] docs: Basic readme. --- README.md | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 842b2b3..e292636 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,42 @@ # shiny.benchmark -Tools to measure performance improvements in shiny apps + +> _Tools to measure performance improvements in Shiny apps._ + + +![R-CMD-check](https://github.com/Appsilon/shiny.benchmark/workflows/R-CMD-check/badge.svg) + +How to install? +--------------- + +```r +remotes::install_github("Appsilon/shiny.benchmark") +``` + +Dependencies +------------ + +TODO + +How to use it? +-------------- + +TODO + +How to contribute? +------------------ + +If you want to contribute to this project please submit a regular PR, once you're done with new feature or bug fix. + +Reporting a bug is also helpful - please use [GitHub issues](https://github.com/Appsilon/shiny.benchmark/issues) and describe your problem as detailed as possible. + +Appsilon +======== + + + +Appsilon is the **Full Service Certified RStudio Partner**. Learn more +at [appsilon.com](https://appsilon.com). + +Get in touch [opensource@appsilon.com](opensource@appsilon.com) + +We are hiring! From bb8242ecac7f7768559f6c786fc602305753be59 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 16:00:16 +0200 Subject: [PATCH 128/225] Updating repo name --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3cfafb8..9c56f25 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,7 +41,7 @@ jobs: - name: Install shiny.performance run: | R -e "install.packages('remotes')" - R -e "remotes::install_github(repo = 'Appsilon/experimental.performance', auth_token = Sys.getenv('GITHUB_PAT'), ref = Sys.getenv('BRANCH_NAME'), quiet = TRUE)" + R -e "remotes::install_github(repo = 'Appsilon/shiny.benchmark', auth_token = Sys.getenv('GITHUB_PAT'), ref = Sys.getenv('BRANCH_NAME'), quiet = TRUE)" - name: Create app structure run: | From 8eeeafad6d8cd3389a8216f5a246189187d46e63 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 16:10:07 +0200 Subject: [PATCH 129/225] Adding plot, summary and print methods --- NAMESPACE | 3 +++ R/plot.R | 9 +++++++++ R/print.R | 2 +- R/summary.R | 9 +++++++++ man/plot.shiny_benchmark.Rd | 14 ++++++++++++++ man/print.shiny_benchmark.Rd | 14 ++++++++++++++ man/summary.shiny_benchmark.Rd | 14 ++++++++++++++ 7 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 R/plot.R create mode 100644 R/summary.R create mode 100644 man/plot.shiny_benchmark.Rd create mode 100644 man/print.shiny_benchmark.Rd create mode 100644 man/summary.shiny_benchmark.Rd diff --git a/NAMESPACE b/NAMESPACE index 11f3a44..7ebe85d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,8 @@ # Generated by roxygen2: do not edit by hand +S3method(plot,shiny_benchmark) +S3method(print,shiny_benchmark) +S3method(summary,shiny_benchmark) export(performance_tests) export(ptest_cypress) export(ptest_shinytest2) diff --git a/R/plot.R b/R/plot.R new file mode 100644 index 0000000..1a0aa57 --- /dev/null +++ b/R/plot.R @@ -0,0 +1,9 @@ +#' Plot for shiny_benchmark class +#' +#' @param object shiny_benchmark object +#' +#' @method plot shiny_benchmark +#' @export +plot.shiny_benchmark <- function(object){ + +} diff --git a/R/print.R b/R/print.R index 56ed87c..259c080 100644 --- a/R/print.R +++ b/R/print.R @@ -1,6 +1,6 @@ #' Print for shiny_benchmark class #' -#' @param object shiny_benchmark object to print +#' @param object shiny_benchmark object #' #' @method print shiny_benchmark #' @export diff --git a/R/summary.R b/R/summary.R new file mode 100644 index 0000000..48a426a --- /dev/null +++ b/R/summary.R @@ -0,0 +1,9 @@ +#' Summary for shiny_benchmark class +#' +#' @param object shiny_benchmark object +#' +#' @method summary shiny_benchmark +#' @export +summary.shiny_benchmark <- function(object){ + +} diff --git a/man/plot.shiny_benchmark.Rd b/man/plot.shiny_benchmark.Rd new file mode 100644 index 0000000..3a1a4e1 --- /dev/null +++ b/man/plot.shiny_benchmark.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plot.R +\name{plot.shiny_benchmark} +\alias{plot.shiny_benchmark} +\title{Plot for shiny_benchmark class} +\usage{ +\method{plot}{shiny_benchmark}(object) +} +\arguments{ +\item{object}{shiny_benchmark object} +} +\description{ +Plot for shiny_benchmark class +} diff --git a/man/print.shiny_benchmark.Rd b/man/print.shiny_benchmark.Rd new file mode 100644 index 0000000..e3c3a09 --- /dev/null +++ b/man/print.shiny_benchmark.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/print.R +\name{print.shiny_benchmark} +\alias{print.shiny_benchmark} +\title{Print for shiny_benchmark class} +\usage{ +\method{print}{shiny_benchmark}(object) +} +\arguments{ +\item{object}{shiny_benchmark object} +} +\description{ +Print for shiny_benchmark class +} diff --git a/man/summary.shiny_benchmark.Rd b/man/summary.shiny_benchmark.Rd new file mode 100644 index 0000000..f9ff1b5 --- /dev/null +++ b/man/summary.shiny_benchmark.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/summary.R +\name{summary.shiny_benchmark} +\alias{summary.shiny_benchmark} +\title{Summary for shiny_benchmark class} +\usage{ +\method{summary}{shiny_benchmark}(object) +} +\arguments{ +\item{object}{shiny_benchmark object} +} +\description{ +Summary for shiny_benchmark class +} From 9b2ada5af9eb74111f8d660c9fd3ce4c7de5a2fa Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Thu, 27 Oct 2022 16:29:53 +0200 Subject: [PATCH 130/225] docs: Add dependencies entry in the readme. --- DESCRIPTION | 2 +- README.md | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 44b5444..31233a1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -11,7 +11,7 @@ Authors@R: Description: Compare performance of several versions of a shiny app based on commit hashs. License: LGPL-3 URL: https://github.com/Appsilon/shiny.benchmark -SystemRequirements: yarn 1.22.17 or higher, cypress 9.4.1 or higher, xvfb +SystemRequirements: yarn 1.22.17 or higher, Node 12 or higher Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) diff --git a/README.md b/README.md index e292636..57f787f 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ ![R-CMD-check](https://github.com/Appsilon/shiny.benchmark/workflows/R-CMD-check/badge.svg) + How to install? --------------- @@ -15,7 +16,15 @@ remotes::install_github("Appsilon/shiny.benchmark") Dependencies ------------ -TODO +`shiny.benchmark` can use two different engines to test the change in the performance of your application: [shinytest2](https://rstudio.github.io/shinytest2/) and [Cypress](https://www.cypress.io/). +The latter requires `Node` (version 12 or higher) and `yarn` (version 1.22.17 or higher) to be available. +To install them on your computer, follow the guidelines on the documentation pages: + +- [Node](https://nodejs.org/en/download/) +- [yarn](https://yarnpkg.com/getting-started/install) + +Besides that, on Linux, it might be required to install other `Cypress` dependencies. +Check the [documentation](https://docs.cypress.io/guides/getting-started/installing-cypress#Linux-Prerequisites) to find out more. How to use it? -------------- From 7f047aedad137059686fa04d9836f818b7706863 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 16:45:32 +0200 Subject: [PATCH 131/225] Adding summary method --- DESCRIPTION | 5 +++-- NAMESPACE | 1 + R/summary.R | 7 +++++++ R/utils.R | 19 +++++++++++++++++++ man/summarise_commit.Rd | 14 ++++++++++++++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 man/summarise_commit.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 44b5444..72d468c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,12 +17,13 @@ LazyData: true Roxygen: list(markdown = TRUE) RoxygenNote: 7.2.0 VignetteBuilder: knitr +Depends: + R (>= 3.1.0) Suggests: + dplyr, knitr, lintr, rcmdcheck -Depends: - R (>= 3.1.0) Imports: glue, jsonlite, diff --git a/NAMESPACE b/NAMESPACE index 7ebe85d..d704c33 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,6 +9,7 @@ export(ptest_shinytest2) export(run_cypress_ptest) export(run_shinytest2_ptest) exportClasses(shiny_benchmark) +import(dplyr) importFrom(glue,glue) importFrom(jsonlite,write_json) importFrom(methods,new) diff --git a/R/summary.R b/R/summary.R index 48a426a..85ac7fe 100644 --- a/R/summary.R +++ b/R/summary.R @@ -3,7 +3,14 @@ #' @param object shiny_benchmark object #' #' @method summary shiny_benchmark +#' @import dplyr #' @export summary.shiny_benchmark <- function(object){ + if (!require(dplyr)) + stop("dplyr is missing. Please, consider intalling dplyr.") + summary_results <- lapply(X = object$performance, FUN = summarise_commit) + summary_results <- bind_rows(summary_results, .id = "commit") + + return(summary_results) } diff --git a/R/utils.R b/R/utils.R index 520943d..14662bd 100644 --- a/R/utils.R +++ b/R/utils.R @@ -122,3 +122,22 @@ create_progress_bar <- function(total = 100) { return(pb) } + +#' @title Return statistics based on the set of tests replications +#' +#' @param object A shiny_benchmark object +#' @import dplyr +summarise_commit <- function(object) { + out <- bind_rows(object) %>% + group_by(test_name) %>% + summarise( + n = n(), + mean = mean(duration_ms), + median = median(duration_ms), + sd = sd(duration_ms), + min = min(duration_ms), + max = max(duration_ms) + ) + + return(out) +} diff --git a/man/summarise_commit.Rd b/man/summarise_commit.Rd new file mode 100644 index 0000000..fee44cf --- /dev/null +++ b/man/summarise_commit.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{summarise_commit} +\alias{summarise_commit} +\title{Return statistics based on the set of tests replications} +\usage{ +summarise_commit(object) +} +\arguments{ +\item{object}{A shiny_benchmark object} +} +\description{ +Return statistics based on the set of tests replications +} From 86acb752872ef4878199dbae2bd340db3dadc0f1 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 18:17:50 +0200 Subject: [PATCH 132/225] Resolving devtools::check problems --- DESCRIPTION | 4 ++- NAMESPACE | 3 ++ R/benchmark.R | 50 ++++++++++++++++++---------------- R/globals.R | 14 ++++++++++ R/plot.R | 31 +++++++++++++++++++-- R/print.R | 11 ++++---- R/summary.R | 6 ++-- R/utils.R | 2 ++ man/plot.shiny_benchmark.Rd | 6 ++-- man/print.shiny_benchmark.Rd | 6 ++-- man/summary.shiny_benchmark.Rd | 4 ++- 11 files changed, 97 insertions(+), 40 deletions(-) create mode 100644 R/globals.R diff --git a/DESCRIPTION b/DESCRIPTION index 72d468c..cd808a9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -20,13 +20,15 @@ VignetteBuilder: knitr Depends: R (>= 3.1.0) Suggests: - dplyr, knitr, lintr, rcmdcheck Imports: + dplyr, + ggplot2, glue, jsonlite, + methods, progress, renv, shinytest2, diff --git a/NAMESPACE b/NAMESPACE index 6e87074..0e36721 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,6 +10,7 @@ export(run_cypress_ptest) export(run_shinytest2_ptest) exportClasses(shiny_benchmark) import(dplyr) +import(ggplot2) importFrom(glue,glue) importFrom(jsonlite,write_json) importFrom(methods,new) @@ -17,6 +18,8 @@ importFrom(progress,progress_bar) importFrom(renv,activate) importFrom(renv,restore) importFrom(shinytest2,test_app) +importFrom(stats,median) importFrom(stringr,str_trim) importFrom(testthat,ListReporter) +importFrom(utils,globalVariables) importFrom(utils,read.table) diff --git a/R/benchmark.R b/R/benchmark.R index 4bb2837..b86fc3c 100644 --- a/R/benchmark.R +++ b/R/benchmark.R @@ -68,30 +68,32 @@ benchmark <- function( check_uncommitted_files() # run tests - if (type == "cypress") { - perf_list <- benchmark_cypress( - commit_list = commit_list, - cypress_dir = cypress_dir, - tests_pattern = tests_pattern, - app_dir = app_dir, - port = port, - use_renv = use_renv, - renv_prompt = renv_prompt, - n_rep = n_rep, - debug = debug - ) - } else { - perf_list <- benchmark_shinytest2( - commit_list, - shinytest2_dir, - tests_pattern = tests_pattern, - app_dir, - use_renv = use_renv, - renv_prompt = renv_prompt, - n_rep = n_rep, - debug = debug - ) - } + total_time <- system.time( + if (type == "cypress") { + perf_list <- benchmark_cypress( + commit_list = commit_list, + cypress_dir = cypress_dir, + tests_pattern = tests_pattern, + app_dir = app_dir, + port = port, + use_renv = use_renv, + renv_prompt = renv_prompt, + n_rep = n_rep, + debug = debug + ) + } else { + perf_list <- benchmark_shinytest2( + commit_list, + shinytest2_dir, + tests_pattern = tests_pattern, + app_dir, + use_renv = use_renv, + renv_prompt = renv_prompt, + n_rep = n_rep, + debug = debug + ) + } + ) out <- list( call = call_benchmark, diff --git a/R/globals.R b/R/globals.R new file mode 100644 index 0000000..06155fd --- /dev/null +++ b/R/globals.R @@ -0,0 +1,14 @@ +utils::globalVariables( + c( + "commit", + "date", + "duration_ms", + "max", + "mean", + "min", + "n", + "sd", + "test_name", + "total_time" + ) +) diff --git a/R/plot.R b/R/plot.R index 1a0aa57..eefca41 100644 --- a/R/plot.R +++ b/R/plot.R @@ -1,9 +1,36 @@ #' Plot for shiny_benchmark class #' -#' @param object shiny_benchmark object +#' @param x shiny_benchmark object +#' @param ... Other parameters #' #' @method plot shiny_benchmark +#' @import dplyr +#' @import ggplot2 +#' @importFrom utils globalVariables #' @export -plot.shiny_benchmark <- function(object){ +plot.shiny_benchmark <- function(x, ...){ + if (!requireNamespace(package = "ggplot2", quietly = TRUE)) + stop("ggplot2 is missing. Please, consider intalling ggplot2.") + plot_df <- lapply(X = x$performance, FUN = bind_rows) %>% + bind_rows(.id = "commit") %>% + arrange(date) %>% + mutate(commit = factor(x = commit, levels = unique(commit))) %>% + group_by(commit, test_name) %>% + summarise( + min = min(duration_ms), + mean = mean(duration_ms), + max = max(duration_ms), + .groups = "keep" + ) %>% + ungroup() + + g <- ggplot(data = plot_df, mapping = aes(x = commit, y = mean)) + + geom_pointrange(mapping = aes(ymin = min, ymax = max)) + + facet_wrap(~test_name) + + ylab("Duration (ms)") + + xlab("Commit") + + theme_bw() + + return(g) } diff --git a/R/print.R b/R/print.R index 259c080..33811ce 100644 --- a/R/print.R +++ b/R/print.R @@ -1,20 +1,21 @@ #' Print for shiny_benchmark class #' -#' @param object shiny_benchmark object +#' @param x shiny_benchmark object +#' @param ... Other parameters #' #' @method print shiny_benchmark #' @export -print.shiny_benchmark <- function(object){ +print.shiny_benchmark <- function(x, ...) { cat('Shiny benchmark: \n') cat('\n') cat('Call:') cat('\n') - print(object$call) + print(x$call) cat('\n') cat('Total time ellapsed:') cat('\n') - print(object$time[["elapsed"]]) + print(x$time[["elapsed"]]) cat('\n') cat('Fit measures: \n') - print(object$performance) + print(x$performance) } diff --git a/R/summary.R b/R/summary.R index 85ac7fe..56b1acb 100644 --- a/R/summary.R +++ b/R/summary.R @@ -1,12 +1,12 @@ #' Summary for shiny_benchmark class #' #' @param object shiny_benchmark object +#' @param ... Other parameters #' #' @method summary shiny_benchmark -#' @import dplyr #' @export -summary.shiny_benchmark <- function(object){ - if (!require(dplyr)) +summary.shiny_benchmark <- function(object, ...) { + if (!requireNamespace(package = "dplyr", quietly = TRUE)) stop("dplyr is missing. Please, consider intalling dplyr.") summary_results <- lapply(X = object$performance, FUN = summarise_commit) diff --git a/R/utils.R b/R/utils.R index 14662bd..d32a387 100644 --- a/R/utils.R +++ b/R/utils.R @@ -126,7 +126,9 @@ create_progress_bar <- function(total = 100) { #' @title Return statistics based on the set of tests replications #' #' @param object A shiny_benchmark object +#' #' @import dplyr +#' @importFrom stats median summarise_commit <- function(object) { out <- bind_rows(object) %>% group_by(test_name) %>% diff --git a/man/plot.shiny_benchmark.Rd b/man/plot.shiny_benchmark.Rd index 3a1a4e1..f3d2036 100644 --- a/man/plot.shiny_benchmark.Rd +++ b/man/plot.shiny_benchmark.Rd @@ -4,10 +4,12 @@ \alias{plot.shiny_benchmark} \title{Plot for shiny_benchmark class} \usage{ -\method{plot}{shiny_benchmark}(object) +\method{plot}{shiny_benchmark}(x, ...) } \arguments{ -\item{object}{shiny_benchmark object} +\item{x}{shiny_benchmark object} + +\item{...}{Other parameters} } \description{ Plot for shiny_benchmark class diff --git a/man/print.shiny_benchmark.Rd b/man/print.shiny_benchmark.Rd index e3c3a09..dcdea35 100644 --- a/man/print.shiny_benchmark.Rd +++ b/man/print.shiny_benchmark.Rd @@ -4,10 +4,12 @@ \alias{print.shiny_benchmark} \title{Print for shiny_benchmark class} \usage{ -\method{print}{shiny_benchmark}(object) +\method{print}{shiny_benchmark}(x, ...) } \arguments{ -\item{object}{shiny_benchmark object} +\item{x}{shiny_benchmark object} + +\item{...}{Other parameters} } \description{ Print for shiny_benchmark class diff --git a/man/summary.shiny_benchmark.Rd b/man/summary.shiny_benchmark.Rd index f9ff1b5..6b3c298 100644 --- a/man/summary.shiny_benchmark.Rd +++ b/man/summary.shiny_benchmark.Rd @@ -4,10 +4,12 @@ \alias{summary.shiny_benchmark} \title{Summary for shiny_benchmark class} \usage{ -\method{summary}{shiny_benchmark}(object) +\method{summary}{shiny_benchmark}(object, ...) } \arguments{ \item{object}{shiny_benchmark object} + +\item{...}{Other parameters} } \description{ Summary for shiny_benchmark class From f317514e1d2236bab6985197668f8fbc36dde217 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 18:19:47 +0200 Subject: [PATCH 133/225] lintr issues --- R/plot.R | 2 +- R/print.R | 18 +++++++++--------- R/shiny_benchmark-class.R | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/R/plot.R b/R/plot.R index eefca41..53a990d 100644 --- a/R/plot.R +++ b/R/plot.R @@ -8,7 +8,7 @@ #' @import ggplot2 #' @importFrom utils globalVariables #' @export -plot.shiny_benchmark <- function(x, ...){ +plot.shiny_benchmark <- function(x, ...) { if (!requireNamespace(package = "ggplot2", quietly = TRUE)) stop("ggplot2 is missing. Please, consider intalling ggplot2.") diff --git a/R/print.R b/R/print.R index 33811ce..f42d9e8 100644 --- a/R/print.R +++ b/R/print.R @@ -6,16 +6,16 @@ #' @method print shiny_benchmark #' @export print.shiny_benchmark <- function(x, ...) { - cat('Shiny benchmark: \n') - cat('\n') - cat('Call:') - cat('\n') + cat("Shiny benchmark: \n") + cat("\n") + cat("Call:") + cat("\n") print(x$call) - cat('\n') - cat('Total time ellapsed:') - cat('\n') + cat("\n") + cat("Total time ellapsed:") + cat("\n") print(x$time[["elapsed"]]) - cat('\n') - cat('Fit measures: \n') + cat("\n") + cat("Fit measures: \n") print(x$performance) } diff --git a/R/shiny_benchmark-class.R b/R/shiny_benchmark-class.R index e98cccb..260de03 100644 --- a/R/shiny_benchmark-class.R +++ b/R/shiny_benchmark-class.R @@ -9,10 +9,10 @@ #' @export shiny_benchmark_class <- setClass( - Class = 'shiny_benchmark', + Class = "shiny_benchmark", representation( - call = 'call', - time = 'proc_time', - performance = 'list' + call = "call", + time = "proc_time", + performance = "list" ) ) From a1416d67e4697a2b4664df84e6d892f02fbd96f1 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Thu, 27 Oct 2022 18:35:38 +0200 Subject: [PATCH 134/225] Adapting new object --- tests/end2end/run_tests.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/end2end/run_tests.R b/tests/end2end/run_tests.R index 42a56d0..bac5593 100644 --- a/tests/end2end/run_tests.R +++ b/tests/end2end/run_tests.R @@ -44,8 +44,8 @@ if (type == "cypress") { } # checks -stopifnot(length(out) == length(commit_list)) -stopifnot(length(out[[1]]) >= n_rep) +stopifnot(length(out$performance) == length(commit_list)) +stopifnot(length(out$performance[[1]]) >= n_rep) # deactivate renv renv::deactivate() From 046c61070d6ba6472ed9fa2e7cbebe1db61e32ab Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 28 Oct 2022 09:18:08 +0200 Subject: [PATCH 135/225] replacing system functions by native R functions --- R/benchmark_cypress.R | 3 +-- R/utils_cypress.R | 10 +++++----- R/utils_shinytest2.R | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/R/benchmark_cypress.R b/R/benchmark_cypress.R index 737b422..86c52ef 100644 --- a/R/benchmark_cypress.R +++ b/R/benchmark_cypress.R @@ -130,8 +130,7 @@ run_cypress_ptest <- function( # run tests there command <- glue( - "cd {project_path}; ", - "set -eu; exec yarn --cwd node performance-test" + "cd {project_path}; set -eu; exec yarn --cwd node performance-test" ) system(command, ignore.stdout = !debug, ignore.stderr = !debug) diff --git a/R/utils_cypress.R b/R/utils_cypress.R index 81a8f35..1efd47e 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -27,8 +27,7 @@ create_cypress_structure <- function(app_dir, port, debug) { dir.create(path = plugins_path, showWarnings = FALSE) # create a path root linked to the main directory app - symlink_cmd <- glue("cd {dir_tests}; ln -s {app_dir} {root_path}") - system(symlink_cmd) + file.symlink(from = app_dir, to = root_path) # create the packages.json file json_txt <- create_node_list(tests_path = tests_path, port = port) @@ -138,9 +137,10 @@ create_cypress_tests <- function(project_path, cypress_dir, tests_pattern) { ) # combine all files into one - cypress_files_string <- paste0(cypress_files, collapse = " ") # nolint - command <- glue("cat {cypress_files_string} > {js_file}") - system(command = command, intern = TRUE) + for (i in seq_along(cypress_files)) { + text <- readLines(con = cypress_files[i]) + writeLines(text = text, con = js_file) + } # file to store the times txt_file <- file.path(project_path, "tests", "cypress", "performance.txt") diff --git a/R/utils_shinytest2.R b/R/utils_shinytest2.R index b9bf9ab..686ccc2 100644 --- a/R/utils_shinytest2.R +++ b/R/utils_shinytest2.R @@ -25,7 +25,7 @@ create_shinytest2_structure <- function(app_dir) { #' @param shinytest2_dir The directory with tests recorded by shinytest2 move_shinytest2_tests <- function(project_path, shinytest2_dir) { # copy everything to the temporary directory - system(glue("cp -r {shinytest2_dir} {project_path}")) + file.copy(from = shinytest2_dir, to = project_path, recursive = TRUE) tests_dir <- file.path(project_path, "tests") return(tests_dir) From 35b9b55b94ce49eff10c3e09ff25bdbc3f651f91 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 28 Oct 2022 12:50:38 +0200 Subject: [PATCH 136/225] writeLines to write + append --- R/utils_cypress.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/utils_cypress.R b/R/utils_cypress.R index 1efd47e..921b159 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -139,7 +139,7 @@ create_cypress_tests <- function(project_path, cypress_dir, tests_pattern) { # combine all files into one for (i in seq_along(cypress_files)) { text <- readLines(con = cypress_files[i]) - writeLines(text = text, con = js_file) + write(text = text, con = js_file, append = TRUE) } # file to store the times From 9f9d45f406317305a74fb3c310862405b66a54b9 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 28 Oct 2022 12:53:59 +0200 Subject: [PATCH 137/225] Fixing package name --- tests/testthat.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testthat.R b/tests/testthat.R index 716c09b..85df7b5 100644 --- a/tests/testthat.R +++ b/tests/testthat.R @@ -1,4 +1,4 @@ library(testthat) -library(shiny.performance) +library(shiny.benchmark) -test_check("shiny.performance") +test_check("shiny.benchmark") From a40263619a22aa5a2aef5023b813dfff4eb8de0e Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 28 Oct 2022 13:23:45 +0200 Subject: [PATCH 138/225] fixing parameters --- R/utils_cypress.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/utils_cypress.R b/R/utils_cypress.R index 921b159..8877055 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -139,7 +139,7 @@ create_cypress_tests <- function(project_path, cypress_dir, tests_pattern) { # combine all files into one for (i in seq_along(cypress_files)) { text <- readLines(con = cypress_files[i]) - write(text = text, con = js_file, append = TRUE) + write(x = text, file = js_file, append = TRUE) } # file to store the times From cabf6ea9132fe0e69b0f8dec91c10f1c24065c62 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 28 Oct 2022 14:00:37 +0200 Subject: [PATCH 139/225] Removing js_file at the end of execution in each commit/branch --- .github/workflows/tests.yml | 2 +- R/benchmark_cypress.R | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9c56f25..3cda07c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,7 +38,7 @@ jobs: with: extra-packages: local::. # Necessary to avoid object usage linter errors. - - name: Install shiny.performance + - name: Install shiny.benchmark run: | R -e "install.packages('remotes')" R -e "remotes::install_github(repo = 'Appsilon/shiny.benchmark', auth_token = Sys.getenv('GITHUB_PAT'), ref = Sys.getenv('BRANCH_NAME'), quiet = TRUE)" diff --git a/R/benchmark_cypress.R b/R/benchmark_cypress.R index 86c52ef..ba904b5 100644 --- a/R/benchmark_cypress.R +++ b/R/benchmark_cypress.R @@ -119,6 +119,7 @@ run_cypress_ptest <- function( cypress_dir = cypress_dir, tests_pattern = tests_pattern ) + js_file <- files$js_file txt_file <- files$txt_file # replicate tests @@ -140,7 +141,7 @@ run_cypress_ptest <- function( colnames(perf_file[[i]]) <- c("date", "rep_id", "test_name", "duration_ms") # removing temp files - unlink(x = txt_file) + unlink(x = c(txt_file, js_file)) } # removing anything new in the github repo From 376eb6a6b1255b9c9c4a5099912a1862330ef328 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 28 Oct 2022 14:29:08 +0200 Subject: [PATCH 140/225] Removing js file after loop --- R/benchmark_cypress.R | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/R/benchmark_cypress.R b/R/benchmark_cypress.R index ba904b5..fdc99bc 100644 --- a/R/benchmark_cypress.R +++ b/R/benchmark_cypress.R @@ -140,10 +140,13 @@ run_cypress_ptest <- function( perf_file[[i]] <- cbind.data.frame(date = date, rep_id = i, perf_file[[i]]) colnames(perf_file[[i]]) <- c("date", "rep_id", "test_name", "duration_ms") - # removing temp files - unlink(x = c(txt_file, js_file)) + # removing txt measures + unlink(x = txt_file) } + # removing js tests + unlink(x = js_file) + # removing anything new in the github repo checkout_files(debug = debug) From 15263adbbba445b55ce1c1d0a1f4a8119332526f Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Fri, 28 Oct 2022 18:15:53 +0200 Subject: [PATCH 141/225] Adding inst/examples and a function to create examples structure --- NAMESPACE | 1 + R/utils.R | 25 ++++++++ inst/examples/app/.gitignore | 47 +++++++++++++++ inst/examples/app/global.R | 1 + inst/examples/app/server.R | 39 +++++++++++++ .../tests/cypress/cypress_use_this_one_1.js | 19 ++++++ .../tests/cypress/cypress_use_this_one_2.js | 19 ++++++ inst/examples/app/tests/testthat.R | 1 + inst/examples/app/tests/testthat/setup.R | 2 + .../app/tests/testthat/test-use_this_one_1.R | 19 ++++++ .../app/tests/testthat/test-use_this_one_2.R | 19 ++++++ inst/examples/app/ui.R | 20 +++++++ inst/examples/run_tests.R | 58 +++++++++++++++++++ man/load_example.Rd | 14 +++++ 14 files changed, 284 insertions(+) create mode 100644 inst/examples/app/.gitignore create mode 100644 inst/examples/app/global.R create mode 100644 inst/examples/app/server.R create mode 100644 inst/examples/app/tests/cypress/cypress_use_this_one_1.js create mode 100644 inst/examples/app/tests/cypress/cypress_use_this_one_2.js create mode 100644 inst/examples/app/tests/testthat.R create mode 100644 inst/examples/app/tests/testthat/setup.R create mode 100644 inst/examples/app/tests/testthat/test-use_this_one_1.R create mode 100644 inst/examples/app/tests/testthat/test-use_this_one_2.R create mode 100644 inst/examples/app/ui.R create mode 100644 inst/examples/run_tests.R create mode 100644 man/load_example.Rd diff --git a/NAMESPACE b/NAMESPACE index 0e36721..2539f0a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -6,6 +6,7 @@ S3method(summary,shiny_benchmark) export(benchmark) export(benchmark_cypress) export(benchmark_shinytest2) +export(load_example) export(run_cypress_ptest) export(run_shinytest2_ptest) exportClasses(shiny_benchmark) diff --git a/R/utils.R b/R/utils.R index d32a387..8f5502f 100644 --- a/R/utils.R +++ b/R/utils.R @@ -143,3 +143,28 @@ summarise_commit <- function(object) { return(out) } + +#' @title Load an application and instructions to run shiny.benchmark +#' +#' @param path A character vector of full path names +#' +#' @importFrom glue glue +#' @export +load_example <- function(path) { + # see if path exists + if (!file.exists(path)) + stop("You must provide a valid path") + + ex_path <- system.file( + "examples", + package = "shiny.benchmark", + mustWork = TRUE + ) + files <- list.files(path = ex_path, full.names = TRUE) + + invisible( + lapply(X = files, FUN = file.copy, to = path, recursive = TRUE) + ) + + message(glue("Follow instructions in {path}/run_tests.R")) +} diff --git a/inst/examples/app/.gitignore b/inst/examples/app/.gitignore new file mode 100644 index 0000000..c131308 --- /dev/null +++ b/inst/examples/app/.gitignore @@ -0,0 +1,47 @@ +# History files +.Rhistory +.Rapp.history + +# Session Data files +.RData + +# User-specific files +.Ruserdata + +# Example code in package build process +*-Ex.R + +# Output files from R CMD build +/*.tar.gz + +# Output files from R CMD check +/*.Rcheck/ + + # RStudio files + .Rproj.user/ + + # produced vignettes + vignettes/*.html +vignettes/*.pdf + +# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 +.httr-oauth + +# knitr and R markdown default cache directories +*_cache/ + /cache/ + + # Temporary files created by R markdown + *.utf8.md +*.knit.md + +# R Environment Variables +.Renviron +__pycache__ +.Rproj.user +TODO.txt + +# csv opened files +.~lock* + +renv/ diff --git a/inst/examples/app/global.R b/inst/examples/app/global.R new file mode 100644 index 0000000..5a0cab8 --- /dev/null +++ b/inst/examples/app/global.R @@ -0,0 +1 @@ +library(shiny) diff --git a/inst/examples/app/server.R b/inst/examples/app/server.R new file mode 100644 index 0000000..b843f12 --- /dev/null +++ b/inst/examples/app/server.R @@ -0,0 +1,39 @@ +function(input, output, session) { + # Sys.sleep + react1 <- eventReactive(input$run1, { + out <- system.time( + Sys.sleep(1 + rexp(n = 1, rate = 10)) + ) + + return(out[3]) + }) + + react2 <- eventReactive(input$run2, { + out <- system.time( + Sys.sleep(0.5 + rexp(n = 1, rate = 10)) + ) + + return(out[3]) + }) + + react3 <- eventReactive(input$run3, { + out <- system.time( + Sys.sleep(0.1 + rexp(n = 1, rate = 10)) + ) + + return(out[1]) + }) + + # outputs + output$out1 <- renderUI({ + tags$span(round(react1()), style = "font-size: 500px;") + }) + + output$out2 <- renderUI({ + tags$span(round(react2()), style = "font-size: 500px;") + }) + + output$out3 <- renderUI({ + tags$span(round(react3()), style = "font-size: 500px;") + }) +} diff --git a/inst/examples/app/tests/cypress/cypress_use_this_one_1.js b/inst/examples/app/tests/cypress/cypress_use_this_one_1.js new file mode 100644 index 0000000..015b865 --- /dev/null +++ b/inst/examples/app/tests/cypress/cypress_use_this_one_1.js @@ -0,0 +1,19 @@ +describe('Cypress test', () => { + it('Out1 time elapsed - 1', () => { + cy.visit('/'); + cy.get('#run1').click(); + cy.get('#out1', {timeout: 10000}).should('be.visible'); + }); + + // Test how long it takes to wait for out2 + it('Out2 time elapsed - 1', () => { + cy.get('#run2').click(); + cy.get('#out2', {timeout: 10000}).should('be.visible'); + }); + + // Test how long it takes to wait for out3 + it('Out3 time elapsed - 1', () => { + cy.get('#run3').click(); + cy.get('#out3', {timeout: 10000}).should('be.visible'); + }); +}); diff --git a/inst/examples/app/tests/cypress/cypress_use_this_one_2.js b/inst/examples/app/tests/cypress/cypress_use_this_one_2.js new file mode 100644 index 0000000..c01f42e --- /dev/null +++ b/inst/examples/app/tests/cypress/cypress_use_this_one_2.js @@ -0,0 +1,19 @@ +describe('Cypress test', () => { + it('Out1 time elapsed - 2', () => { + cy.visit('/'); + cy.get('#run1').click(); + cy.get('#out1', {timeout: 10000}).should('be.visible'); + }); + + // Test how long it takes to wait for out2 + it('Out2 time elapsed - 2', () => { + cy.get('#run2').click(); + cy.get('#out2', {timeout: 10000}).should('be.visible'); + }); + + // Test how long it takes to wait for out3 + it('Out3 time elapsed - 2', () => { + cy.get('#run3').click(); + cy.get('#out3', {timeout: 10000}).should('be.visible'); + }); +}); diff --git a/inst/examples/app/tests/testthat.R b/inst/examples/app/tests/testthat.R new file mode 100644 index 0000000..7d25b5b --- /dev/null +++ b/inst/examples/app/tests/testthat.R @@ -0,0 +1 @@ +shinytest2::test_app() diff --git a/inst/examples/app/tests/testthat/setup.R b/inst/examples/app/tests/testthat/setup.R new file mode 100644 index 0000000..be65b4f --- /dev/null +++ b/inst/examples/app/tests/testthat/setup.R @@ -0,0 +1,2 @@ +# Load application support files into testing environment +shinytest2::load_app_env() diff --git a/inst/examples/app/tests/testthat/test-use_this_one_1.R b/inst/examples/app/tests/testthat/test-use_this_one_1.R new file mode 100644 index 0000000..a530c0a --- /dev/null +++ b/inst/examples/app/tests/testthat/test-use_this_one_1.R @@ -0,0 +1,19 @@ +library(shinytest2) + +test_that("{shinytest2} recording: test1", { + app <- AppDriver$new(name = "test1", height = 975, width = 1619) + app$click("run1") + app$expect_values(output = "out1") +}) + +test_that("{shinytest2} recording: test2", { + app <- AppDriver$new(name = "test2", height = 975, width = 1619) + app$click("run2") + app$expect_values(output = "out2") +}) + +test_that("{shinytest2} recording: test3", { + app <- AppDriver$new(name = "test3", height = 975, width = 1619) + app$click("run3") + app$expect_values(output = "out3") +}) diff --git a/inst/examples/app/tests/testthat/test-use_this_one_2.R b/inst/examples/app/tests/testthat/test-use_this_one_2.R new file mode 100644 index 0000000..f2fc5c6 --- /dev/null +++ b/inst/examples/app/tests/testthat/test-use_this_one_2.R @@ -0,0 +1,19 @@ +library(shinytest2) + +test_that("{shinytest2} recording: test4", { + app <- AppDriver$new(name = "test1", height = 975, width = 1619) + app$click("run1") + app$expect_values(output = "out1") +}) + +test_that("{shinytest2} recording: test5", { + app <- AppDriver$new(name = "test2", height = 975, width = 1619) + app$click("run2") + app$expect_values(output = "out2") +}) + +test_that("{shinytest2} recording: test6", { + app <- AppDriver$new(name = "test3", height = 975, width = 1619) + app$click("run3") + app$expect_values(output = "out3") +}) diff --git a/inst/examples/app/ui.R b/inst/examples/app/ui.R new file mode 100644 index 0000000..8d3615a --- /dev/null +++ b/inst/examples/app/ui.R @@ -0,0 +1,20 @@ +function() { + bootstrapPage( + tags$h1("Measuring time in different commits"), + column( + width = 4, + actionButton(inputId = "run1", label = "Run 1"), + uiOutput(outputId = "out1") + ), + column( + width = 4, + actionButton(inputId = "run2", label = "Run 2"), + uiOutput(outputId = "out2") + ), + column( + width = 4, + actionButton(inputId = "run3", label = "Run 3"), + uiOutput(outputId = "out3") + ) + ) +} diff --git a/inst/examples/run_tests.R b/inst/examples/run_tests.R new file mode 100644 index 0000000..f656c55 --- /dev/null +++ b/inst/examples/run_tests.R @@ -0,0 +1,58 @@ +############################################################################### +# Start a git repo under app/ folder here and create some branches. It can be # +# more fun if you change the parameters in app/server.R # +# # +# suggestion: # +# git init # +# # +# main # +# git add . # +# git commit -m "first commit" # +# # +# # develop # +# git checkout -b develop # +# git commit --allow-empty -m "dummy commit to change hash" # +# # +# # feature # +# git checkout -b feature # +# git commit --allow-empty -m "dummy commit to change hash" # +############################################################################### + +# packages +library(shiny.benchmark) + +# commits to compare +type <- "cypress" +commit_list <- c("develop", "feature") # be sure you created these branches +dir <- "tests/cypress" +pattern <- "use_this_one_[0-9]" +use_renv <- FALSE +n_rep <- 5 + +if (type == "cypress") { + # run performance check using Cypress + out <- benchmark( + commit_list = commit_list, + cypress_dir = dir, + tests_pattern = pattern, + app_dir = getwd(), + use_renv = use_renv, + renv_prompt = TRUE, + port = 3333, + n_rep = n_rep, + debug = FALSE + ) +} else { + # run performance check using shinytest2 + out <- benchmark( + commit_list = commit_list, + shinytest2_dir = dir, + tests_pattern = pattern, + app_dir = getwd(), + use_renv = use_renv, + renv_prompt = TRUE, + port = 3333, + n_rep = n_rep, + debug = FALSE + ) +} diff --git a/man/load_example.Rd b/man/load_example.Rd new file mode 100644 index 0000000..ff409f8 --- /dev/null +++ b/man/load_example.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{load_example} +\alias{load_example} +\title{Load an application and instructions to run shiny.benchmark} +\usage{ +load_example(path) +} +\arguments{ +\item{path}{A character vector of full path names} +} +\description{ +Load an application and instructions to run shiny.benchmark +} From cc583df67a48cd47e8b14ed9ed312e1d85aa7b8e Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Sat, 29 Oct 2022 12:08:24 +0200 Subject: [PATCH 142/225] Adding How to use it section --- README.md | 126 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 57f787f..3121131 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,131 @@ Check the [documentation](https://docs.cypress.io/guides/getting-started/install How to use it? -------------- -TODO +The best way to start using `shiny.performance` is through an example. If you want a start point, you can use the `load_example` function. In order to use this, create a new folder in your computer and use the following code to generate an application to serve us as example for our performance checks: + +```r +library(shiny.benchmark) + +load_example(path = "path/to/new/project") +``` + +It will create some useful files under `path/to/new/project`. The most important one is the `run_tests.R` which provides several instructions at the very top. + +As we are comparing versions of the same application, we need different app versions in different branches/commits in `git`. Start using `cd app; git init` to iniciate git inside `app/` folder. + +Get familiar with `app/server.R` file in order to generate more interesting scenarios. The basic idea is to use the `Sys.sleep` function to simulate some app's functionalities. Remember that, when running the benchmark, that is the amount of time it will take to measure the performance. + +When you are ready, commit your changes in master/main using `git add .; git commit -m "your commit message"`. Make some editions and commit these new changes into a new branch or in the same branch your are testing (it will have a different commit hash). Repeat the process adding as many new modifications as you want. E.g. add renv, add more tests, change the names of the tests/test files and so on. + +Here is a complete example on how to setup your `git`: + +```git +# starting +git init +echo .Rproj.user >> .gitignore +echo .Rproj >> .gitignore +echo .Rprofile >> .gitignore +echo renv >> .gitignore +echo .Rprofile >> .gitignore + +# master +git add . +git commit -m "first commit" + +# develop (decrease Sys.sleep times in server.R) +git checkout -b develop +git add . +git commit -m "improving performance" + +## Using renv +git branch renv_shiny1 develop +git checkout renv_shiny1 +R -e 'renv::init()' +git add . +git commit -m "renv active" + +## Downgrading shiny +git checkout -b renv_shiny2 +R -e 'renv::install("shiny@1.7.0")' +R -e 'renv::snapshot()' +git add . +git commit -m "downgrading shiny" + +## Switching back to develop +git checkout develop +``` + +Now you are ready to go. The `benchmark` function provides several arguments to make your life easier when running your performance checks. The mandatory arguments are: + +- `commit_list`: A vector with commits, branches or anything else you can use in `git checkout` +- `cypress_dir` or `shinytest2_dir`: Folder containing the tests we want to check the performance. In our case it is `tests/cypress` and `tests` respectively. + +The default behavior is to try to use `renv` in your project. If you do not have the renv structure, you can turn `renv` off using `use_renv = FALSE` + +```r +library(shiny.benchmark) + +# commits to compare +commit_list <- c("develop", "renv_shiny1", "renv_shiny2") + +# run performance check using Cypress +benchmark( + commit_list = commit_list, + cypress_dir = "tests/cypress" +) +``` + +That is all you need to run your `Cypress` tests. If you don't use `Cypress`, you may want to use `shinytest2` instead: + +```r +benchmark( + commit_list = commit_list, + shinytest2_dir = "tests" +) +``` + +To run just specific tests, you can take advantage of the `tests_pattern` argument. It will filter the test file's names based on regular expression: + +```r +benchmark( + commit_list = commit_list, + shinytest2_dir = "tests", + tests_pattern = "use_this_one_[0-9]" +) +``` + +If your project has `renv` strucure, you can set `use_renv` to `TRUE` to guarantee that, for each application version your are using the correct packages. If you want to approve/reprove `renv::restore()`, you can set `renv_prompt = TRUE`. + +```r +benchmark( + commit_list = commit_list, + shinytest2_dir = "tests", + tests_pattern = "use_this_one_[0-9]", + use_renv = TRUE, # default + renv_prompt = TRUE +) +``` + +To have more acurate information about the time your application takes to perform some actions, you may need to replicate the tests. In this case, you can use the `n_rep` argument: + +```r +out <- benchmark( + commit_list = commit_list, + cypress_dir = "tests/cypress", + tests_pattern = "use_this_one_[0-9]", + use_renv = FALSE + n_rep = 15 +) + +out +``` + +For fast information about the tests's results, you can use the `summary` and also the `plot` methods: + +```r +summary(out) +plot(out) +``` How to contribute? ------------------ From 3f9ce14a7a71d69919eb8f99d125e7d0cc932e59 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Sat, 29 Oct 2022 12:15:44 +0200 Subject: [PATCH 143/225] Improving example --- inst/examples/app/.gitignore | 47 ------------------------------------ inst/examples/run_tests.R | 13 +++++++--- 2 files changed, 10 insertions(+), 50 deletions(-) delete mode 100644 inst/examples/app/.gitignore diff --git a/inst/examples/app/.gitignore b/inst/examples/app/.gitignore deleted file mode 100644 index c131308..0000000 --- a/inst/examples/app/.gitignore +++ /dev/null @@ -1,47 +0,0 @@ -# History files -.Rhistory -.Rapp.history - -# Session Data files -.RData - -# User-specific files -.Ruserdata - -# Example code in package build process -*-Ex.R - -# Output files from R CMD build -/*.tar.gz - -# Output files from R CMD check -/*.Rcheck/ - - # RStudio files - .Rproj.user/ - - # produced vignettes - vignettes/*.html -vignettes/*.pdf - -# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 -.httr-oauth - -# knitr and R markdown default cache directories -*_cache/ - /cache/ - - # Temporary files created by R markdown - *.utf8.md -*.knit.md - -# R Environment Variables -.Renviron -__pycache__ -.Rproj.user -TODO.txt - -# csv opened files -.~lock* - -renv/ diff --git a/inst/examples/run_tests.R b/inst/examples/run_tests.R index f656c55..568da2e 100644 --- a/inst/examples/run_tests.R +++ b/inst/examples/run_tests.R @@ -1,6 +1,6 @@ ############################################################################### -# Start a git repo under app/ folder here and create some branches. It can be # -# more fun if you change the parameters in app/server.R # +# Start a git repo under app/ folder and create some branches. It can be more # +# fun if you change the Sys.sleep time in app/server.R # # # # suggestion: # # git init # @@ -16,6 +16,9 @@ # # feature # # git checkout -b feature # # git commit --allow-empty -m "dummy commit to change hash" # +# # +# For a more complete example see: # +# https://github.com/Appsilon/shiny.benchmark # ############################################################################### # packages @@ -23,7 +26,7 @@ library(shiny.benchmark) # commits to compare type <- "cypress" -commit_list <- c("develop", "feature") # be sure you created these branches +commit_list <- c("develop", "feature") dir <- "tests/cypress" pattern <- "use_this_one_[0-9]" use_renv <- FALSE @@ -56,3 +59,7 @@ if (type == "cypress") { debug = FALSE ) } + +out +summary(out) +plot(out) From 88ff189c7a480f6f5d522afd5294b70dcd8bc415 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Sat, 29 Oct 2022 12:30:06 +0200 Subject: [PATCH 144/225] Ignoring Rproj correctly --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3121131..d5c896f 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Here is a complete example on how to setup your `git`: # starting git init echo .Rproj.user >> .gitignore -echo .Rproj >> .gitignore +echo *.Rproj >> .gitignore echo .Rprofile >> .gitignore echo renv >> .gitignore echo .Rprofile >> .gitignore From 87221292158caa2fe0670e2c003b58842bb6430e Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Sat, 29 Oct 2022 12:38:46 +0200 Subject: [PATCH 145/225] Missing comma --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d5c896f..e418615 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ out <- benchmark( commit_list = commit_list, cypress_dir = "tests/cypress", tests_pattern = "use_this_one_[0-9]", - use_renv = FALSE + use_renv = FALSE, n_rep = 15 ) From 4d144b3982563d124ea09088b7425b4c3b2d182c Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 31 Oct 2022 10:57:34 +0100 Subject: [PATCH 146/225] Adding more documentation to load_example as well as more information about what we are doing there during execution --- R/utils.R | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/R/utils.R b/R/utils.R index 8f5502f..3333d14 100644 --- a/R/utils.R +++ b/R/utils.R @@ -145,8 +145,14 @@ summarise_commit <- function(object) { } #' @title Load an application and instructions to run shiny.benchmark +#' @description This function aims to generate a template to be used +#' by shiny.benchmark. It will create the necessary structure on `path` with +#' some examples of tests using Cypress and shinytest2. Also, a simple +#' application will be added to the folder as well as instructions on how +#' to perform the performance checks. Be aware that a new git repo is need in +#' the selected `path`. #' -#' @param path A character vector of full path names +#' @param path A character vector of full path name #' #' @importFrom glue glue #' @export @@ -155,6 +161,16 @@ load_example <- function(path) { if (!file.exists(path)) stop("You must provide a valid path") + if (length(list.files("tst"))) { + choice <- menu( + choices = c("Yes", "No"), + title = glue("{path} seems to not be empty. Would you like to proceed?") + ) + + if (choice == 2) + stop("Process aborted by user. Consider creating a new empty path.") + } + ex_path <- system.file( "examples", package = "shiny.benchmark", @@ -162,9 +178,11 @@ load_example <- function(path) { ) files <- list.files(path = ex_path, full.names = TRUE) - invisible( - lapply(X = files, FUN = file.copy, to = path, recursive = TRUE) - ) + for (file in files) { + file.copy(from = file, to = path, recursive = TRUE) + print(glue("{basename(file)} created at {path}")) + } - message(glue("Follow instructions in {path}/run_tests.R")) + fpath <- file.path(path, "run_tests.R") + message(glue("Follow instructions in {fpath}")) } From 255c1d8f14fbad6f21dd62ce6afe48f1a6da80d4 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 31 Oct 2022 11:14:08 +0100 Subject: [PATCH 147/225] Updating documentation --- NAMESPACE | 1 + R/utils.R | 3 ++- man/load_example.Rd | 9 +++++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 2539f0a..a73c9a9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -23,4 +23,5 @@ importFrom(stats,median) importFrom(stringr,str_trim) importFrom(testthat,ListReporter) importFrom(utils,globalVariables) +importFrom(utils,menu) importFrom(utils,read.table) diff --git a/R/utils.R b/R/utils.R index 3333d14..a7e6245 100644 --- a/R/utils.R +++ b/R/utils.R @@ -155,6 +155,7 @@ summarise_commit <- function(object) { #' @param path A character vector of full path name #' #' @importFrom glue glue +#' @importFrom utils menu #' @export load_example <- function(path) { # see if path exists @@ -183,6 +184,6 @@ load_example <- function(path) { print(glue("{basename(file)} created at {path}")) } - fpath <- file.path(path, "run_tests.R") + fpath <- file.path(path, "run_tests.R") # nolint message(glue("Follow instructions in {fpath}")) } diff --git a/man/load_example.Rd b/man/load_example.Rd index ff409f8..912189f 100644 --- a/man/load_example.Rd +++ b/man/load_example.Rd @@ -7,8 +7,13 @@ load_example(path) } \arguments{ -\item{path}{A character vector of full path names} +\item{path}{A character vector of full path name} } \description{ -Load an application and instructions to run shiny.benchmark +This function aims to generate a template to be used +by shiny.benchmark. It will create the necessary structure on \code{path} with +some examples of tests using Cypress and shinytest2. Also, a simple +application will be added to the folder as well as instructions on how +to perform the performance checks. Be aware that a new git repo is need in +the selected \code{path}. } From d4a71d6607af31da7827af85d88443ce4d8dede2 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 31 Oct 2022 11:46:08 +0100 Subject: [PATCH 148/225] removing hardcoded path --- R/utils.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/utils.R b/R/utils.R index a7e6245..2ab0f14 100644 --- a/R/utils.R +++ b/R/utils.R @@ -162,7 +162,7 @@ load_example <- function(path) { if (!file.exists(path)) stop("You must provide a valid path") - if (length(list.files("tst"))) { + if (length(list.files(path))) { choice <- menu( choices = c("Yes", "No"), title = glue("{path} seems to not be empty. Would you like to proceed?") From e023efee945cca0720eed6a4b0b44a6deb0ce28a Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 31 Oct 2022 12:14:59 +0100 Subject: [PATCH 149/225] Add documentation page setup. --- pkgdown/_pkgdown.yml | 33 +++++++++++++++++++++++++++++++++ pkgdown/extra.css | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 pkgdown/_pkgdown.yml create mode 100644 pkgdown/extra.css diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml new file mode 100644 index 0000000..d33e38b --- /dev/null +++ b/pkgdown/_pkgdown.yml @@ -0,0 +1,33 @@ +title: shiny.benchmark +template: + bootstrap: 5 + bootswatch: pulse + bslib: + pkgdown-nav-height: 100px + includes: + in_header: | + + + + +url: + +navbar: + bg: primary + left: + - icon: fa-home + href: index.html + text: "Start" + - icon: fa-file-code-o + text: "Reference" + href: reference/index.html + right: + - icon: fa-github fa-lg + href: https://github.com/Appsilon/shiny.benchmark + - icon: fa-twitter fa-lg + href: https://twitter.com/Appsilon diff --git a/pkgdown/extra.css b/pkgdown/extra.css new file mode 100644 index 0000000..f7bcb77 --- /dev/null +++ b/pkgdown/extra.css @@ -0,0 +1,44 @@ +.navbar { + background-color: rgb(177, 30, 30) !important; +} + +#navbar > ul.navbar-nav > li.nav-item a:hover { + background-color: rgb(177, 30, 30) !important; +} + +.navbar-dark .navbar-nav .active>.nav-link { + background-color: rgb(177, 30, 30) !important; + color: #fff; +} + +.navbar-dark input[type="search"] { + background-color: #fff !important; + color: #444 !important; +} + +nav .text-muted { + color: #d8d8d8 !important; +} + +a { + color: rgb(124, 23, 23); +} + +a:hover { + color: rgb(177, 30, 30); +} + +button.btn.btn-primary.btn-copy-ex { + background-color: rgb(177, 30, 30); + border-color: rgb(177, 30, 30); +} + +.app-preview { + margin: 1.5em 0.75em; + padding: 0.25em; + box-shadow: + 0 3.9px 4.6px rgba(0, 0, 0, 0.08), + 0 12.3px 8.4px rgba(0, 0, 0, 0.056), + 0 18.8px 19.2px rgba(0, 0, 0, 0.037), + 0 22px 40px rgba(0, 0, 0, 0.019); +} From cc8ea4e45d263295de049d0fa04404a95d4686b9 Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 31 Oct 2022 12:17:06 +0100 Subject: [PATCH 150/225] Update authors. --- DESCRIPTION | 1 - 1 file changed, 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 1ce1cc2..49bcdf9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -4,7 +4,6 @@ Version: 0.1.1 Authors@R: c( person(given = "Douglas", family = "Azevedo", email = "douglas@appsilon.com", role = "aut"), - person(given = "Pedro", family = "Silva", email = "pedro@appsilon.com", role = "aut"), person("Developers", "Appsilon", email = "support+opensource@appsilon.com", role = "cre"), person(family = "Appsilon Sp. z o.o.", role = "cph") ) From 8ef5c4bce91d9c03e48b866d49f7652ce902ec35 Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 31 Oct 2022 12:26:54 +0100 Subject: [PATCH 151/225] adding description to README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index e418615..33a0a74 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ ![R-CMD-check](https://github.com/Appsilon/shiny.benchmark/workflows/R-CMD-check/badge.svg) +Description + +`shiny.benchmark` is a tool aimed to measure and compare the performance of different versions of a shiny application. Based on a list of different application versions, accessible by a git repo by its refs (commit hash or branch name), the user can write instructions to be executed using Cypress or shinytest2. These instructions are then evaluated by the different versions of your shiny application and therefore the performance's improvement/deterioration (time elapsed) are be recorded. + +The package is flexible enough to allow different sets of tests for the different refs as well as different package versions (via renv). Also, the user can replicate the tests to have more accurate measures of performance. + How to install? --------------- From e4a718fb26fdc2abe66725a6cd8d01a9ebbe333f Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 31 Oct 2022 12:37:36 +0100 Subject: [PATCH 152/225] Mark internal functions. --- DESCRIPTION | 2 +- NAMESPACE | 1 + R/utils.R | 16 ++++++++++++++++ R/utils_cypress.R | 12 ++++++++++++ R/utils_shinytest2.R | 4 ++++ man/add_sendtime2js.Rd | 1 + man/check_uncommitted_files.Rd | 1 + man/checkout.Rd | 1 + man/checkout_files.Rd | 1 + man/create_cypress_list.Rd | 1 + man/create_cypress_plugins.Rd | 1 + man/create_cypress_structure.Rd | 1 + man/create_cypress_tests.Rd | 1 + man/create_node_list.Rd | 1 + man/create_progress_bar.Rd | 1 + man/create_shinytest2_structure.Rd | 1 + man/get_commit_date.Rd | 1 + man/get_commit_hash.Rd | 1 + man/move_shinytest2_tests.Rd | 1 + man/restore_env.Rd | 1 + man/summarise_commit.Rd | 1 + 21 files changed, 50 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 49bcdf9..fe36ddc 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -14,7 +14,7 @@ SystemRequirements: yarn 1.22.17 or higher, Node 12 or higher Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.0 +RoxygenNote: 7.2.1 VignetteBuilder: knitr Depends: R (>= 3.1.0) diff --git a/NAMESPACE b/NAMESPACE index a73c9a9..a51ca51 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,6 +9,7 @@ export(benchmark_shinytest2) export(load_example) export(run_cypress_ptest) export(run_shinytest2_ptest) +export(shiny_benchmark_class) exportClasses(shiny_benchmark) import(dplyr) import(ggplot2) diff --git a/R/utils.R b/R/utils.R index 2ab0f14..a7cf9af 100644 --- a/R/utils.R +++ b/R/utils.R @@ -2,6 +2,8 @@ #' #' @param branch Commit hash code or branch name #' @importFrom glue glue +#' +#' @keywords internal get_commit_date <- function(branch) { date <- system( command = glue("git show -s --format=%ci {branch}"), @@ -16,6 +18,8 @@ get_commit_date <- function(branch) { #' #' @importFrom glue glue #' @importFrom stringr str_trim +#' +#' @keywords internal get_commit_hash <- function() { hash <- system(command = "git show -s --format=%H", intern = TRUE)[1] @@ -51,6 +55,8 @@ get_commit_hash <- function() { #' changing branches #' #' @param debug Logical. TRUE to display all the system messages on runtime +#' +#' @keywords internal checkout_files <- function(debug) { system( command = "git checkout .", @@ -65,6 +71,8 @@ checkout_files <- function(debug) { #' #' @param branch Commit hash code or branch name #' @param debug Logical. TRUE to display all the system messages on runtime +#' +#' @keywords internal checkout <- function(branch, debug) { system( command = glue("git checkout {branch}"), @@ -74,6 +82,8 @@ checkout <- function(branch, debug) { } #' @title Check for uncommitted files +#' +#' @keywords internal check_uncommitted_files <- function() { changes <- system("git status --porcelain", intern = TRUE) @@ -96,6 +106,8 @@ check_uncommitted_files <- function() { #' @param renv_prompt Prompt the user before taking any action? #' @importFrom glue glue #' @importFrom renv activate restore +#' +#' @keywords internal restore_env <- function(branch, renv_prompt) { # handling renv tryCatch( @@ -113,6 +125,8 @@ restore_env <- function(branch, renv_prompt) { #' #' @param total Total number of replications #' @importFrom progress progress_bar +#' +#' @keywords internal create_progress_bar <- function(total = 100) { pb <- progress_bar$new( format = "Iteration :current/:total", @@ -129,6 +143,8 @@ create_progress_bar <- function(total = 100) { #' #' @import dplyr #' @importFrom stats median +#' +#' @keywords internal summarise_commit <- function(object) { out <- bind_rows(object) %>% group_by(test_name) %>% diff --git a/R/utils_cypress.R b/R/utils_cypress.R index 8877055..c7bca8d 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -5,6 +5,8 @@ #' @param debug Logical. TRUE to display all the system messages on runtime #' #' @importFrom jsonlite write_json +#' +#' @keywords internal create_cypress_structure <- function(app_dir, port, debug) { # temp dir to run the tests dir_tests <- tempdir() @@ -57,6 +59,8 @@ create_cypress_structure <- function(app_dir, port, debug) { #' #' @param tests_path The path to project #' @param port Port to run the app +#' +#' @keywords internal create_node_list <- function(tests_path, port) { json_list <- list( private = TRUE, @@ -80,6 +84,8 @@ create_node_list <- function(tests_path, port) { #' #' @param plugins_file The path to the Cypress plugins #' @param port Port to run the app +#' +#' @keywords internal create_cypress_list <- function(plugins_file, port) { json_list <- list( baseUrl = glue("http://localhost:{port}"), @@ -91,6 +97,8 @@ create_cypress_list <- function(plugins_file, port) { } #' @title Create the JS code to track execution time +#' +#' @keywords internal create_cypress_plugins <- function() { js_txt <- " const fs = require('fs') @@ -117,6 +125,8 @@ create_cypress_plugins <- function() { #' @param cypress_dir The directory with tests recorded by Cypress #' @param tests_pattern Cypress files pattern. E.g. 'performance'. If it is NULL, #' all the content will be used +#' +#' @keywords internal create_cypress_tests <- function(project_path, cypress_dir, tests_pattern) { # locate files cypress_files <- list.files( @@ -154,6 +164,8 @@ create_cypress_tests <- function(project_path, cypress_dir, tests_pattern) { #' #' @param js_file Path to the .js file to add code #' @param txt_file Path to the file to record the execution times +#' +#' @keywords internal add_sendtime2js <- function(js_file, txt_file) { lines_to_add <- glue( " diff --git a/R/utils_shinytest2.R b/R/utils_shinytest2.R index 686ccc2..6392937 100644 --- a/R/utils_shinytest2.R +++ b/R/utils_shinytest2.R @@ -3,6 +3,8 @@ #' @param app_dir The path to the application root #' #' @importFrom glue glue +#' +#' @keywords internal create_shinytest2_structure <- function(app_dir) { # temp dir to run the tests dir_tests <- tempdir() @@ -23,6 +25,8 @@ create_shinytest2_structure <- function(app_dir) { #' #' @param project_path The path to the project #' @param shinytest2_dir The directory with tests recorded by shinytest2 +#' +#' @keywords internal move_shinytest2_tests <- function(project_path, shinytest2_dir) { # copy everything to the temporary directory file.copy(from = shinytest2_dir, to = project_path, recursive = TRUE) diff --git a/man/add_sendtime2js.Rd b/man/add_sendtime2js.Rd index c34dba9..f5ce1f8 100644 --- a/man/add_sendtime2js.Rd +++ b/man/add_sendtime2js.Rd @@ -14,3 +14,4 @@ add_sendtime2js(js_file, txt_file) \description{ Add the sendTime function to the .js file } +\keyword{internal} diff --git a/man/check_uncommitted_files.Rd b/man/check_uncommitted_files.Rd index 5ef9786..c26be49 100644 --- a/man/check_uncommitted_files.Rd +++ b/man/check_uncommitted_files.Rd @@ -9,3 +9,4 @@ check_uncommitted_files() \description{ Check for uncommitted files } +\keyword{internal} diff --git a/man/checkout.Rd b/man/checkout.Rd index 7e98bb5..090a811 100644 --- a/man/checkout.Rd +++ b/man/checkout.Rd @@ -14,3 +14,4 @@ checkout(branch, debug) \description{ checkout and go to a different branch } +\keyword{internal} diff --git a/man/checkout_files.Rd b/man/checkout_files.Rd index d427219..b27e7f7 100644 --- a/man/checkout_files.Rd +++ b/man/checkout_files.Rd @@ -13,3 +13,4 @@ checkout_files(debug) Checkout anything created by the app. It prevents errors when changing branches } +\keyword{internal} diff --git a/man/create_cypress_list.Rd b/man/create_cypress_list.Rd index 2fe110a..d970359 100644 --- a/man/create_cypress_list.Rd +++ b/man/create_cypress_list.Rd @@ -14,3 +14,4 @@ create_cypress_list(plugins_file, port) \description{ Create the cypress configuration list } +\keyword{internal} diff --git a/man/create_cypress_plugins.Rd b/man/create_cypress_plugins.Rd index cc97969..6e27410 100644 --- a/man/create_cypress_plugins.Rd +++ b/man/create_cypress_plugins.Rd @@ -9,3 +9,4 @@ create_cypress_plugins() \description{ Create the JS code to track execution time } +\keyword{internal} diff --git a/man/create_cypress_structure.Rd b/man/create_cypress_structure.Rd index 4881488..292c26a 100644 --- a/man/create_cypress_structure.Rd +++ b/man/create_cypress_structure.Rd @@ -16,3 +16,4 @@ create_cypress_structure(app_dir, port, debug) \description{ Create a temporary directory to store everything needed by Cypress } +\keyword{internal} diff --git a/man/create_cypress_tests.Rd b/man/create_cypress_tests.Rd index 924ceb6..6baad5d 100644 --- a/man/create_cypress_tests.Rd +++ b/man/create_cypress_tests.Rd @@ -18,3 +18,4 @@ all the content will be used} \description{ Create the cypress files under project directory } +\keyword{internal} diff --git a/man/create_node_list.Rd b/man/create_node_list.Rd index 8b79fcf..4c6dd4f 100644 --- a/man/create_node_list.Rd +++ b/man/create_node_list.Rd @@ -14,3 +14,4 @@ create_node_list(tests_path, port) \description{ Create the list of needed libraries } +\keyword{internal} diff --git a/man/create_progress_bar.Rd b/man/create_progress_bar.Rd index 551c970..f1bcf92 100644 --- a/man/create_progress_bar.Rd +++ b/man/create_progress_bar.Rd @@ -12,3 +12,4 @@ create_progress_bar(total = 100) \description{ Create a progress bar to follow the execution } +\keyword{internal} diff --git a/man/create_shinytest2_structure.Rd b/man/create_shinytest2_structure.Rd index 965adf1..ef03ad8 100644 --- a/man/create_shinytest2_structure.Rd +++ b/man/create_shinytest2_structure.Rd @@ -12,3 +12,4 @@ create_shinytest2_structure(app_dir) \description{ Create a temporary directory to store everything needed by shinytest2 } +\keyword{internal} diff --git a/man/get_commit_date.Rd b/man/get_commit_date.Rd index f218074..fd23414 100644 --- a/man/get_commit_date.Rd +++ b/man/get_commit_date.Rd @@ -12,3 +12,4 @@ get_commit_date(branch) \description{ Get the commit date in POSIXct format } +\keyword{internal} diff --git a/man/get_commit_hash.Rd b/man/get_commit_hash.Rd index 90f2e44..514dd25 100644 --- a/man/get_commit_hash.Rd +++ b/man/get_commit_hash.Rd @@ -9,3 +9,4 @@ get_commit_hash() \description{ Find the hash code of the current commit } +\keyword{internal} diff --git a/man/move_shinytest2_tests.Rd b/man/move_shinytest2_tests.Rd index 3aa5b3b..a47c0f7 100644 --- a/man/move_shinytest2_tests.Rd +++ b/man/move_shinytest2_tests.Rd @@ -14,3 +14,4 @@ move_shinytest2_tests(project_path, shinytest2_dir) \description{ Move tests to a temporary folder } +\keyword{internal} diff --git a/man/restore_env.Rd b/man/restore_env.Rd index 57de19a..ec881e0 100644 --- a/man/restore_env.Rd +++ b/man/restore_env.Rd @@ -17,3 +17,4 @@ Check whether renv is in use in the current branch. Raise error if renv is not in use or apply renv:restore() in the case the package is present } +\keyword{internal} diff --git a/man/summarise_commit.Rd b/man/summarise_commit.Rd index fee44cf..623d10a 100644 --- a/man/summarise_commit.Rd +++ b/man/summarise_commit.Rd @@ -12,3 +12,4 @@ summarise_commit(object) \description{ Return statistics based on the set of tests replications } +\keyword{internal} From 5dd6a7a9863aa4a2294110c2baa12b1f0f9916f7 Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 31 Oct 2022 12:38:27 +0100 Subject: [PATCH 153/225] Update ignore files. --- .Rbuildignore | 1 + .gitignore | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.Rbuildignore b/.Rbuildignore index c7eaab0..1a759a0 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -3,3 +3,4 @@ .github .lintr tests/end2end +pkgdown diff --git a/.gitignore b/.gitignore index fae8299..c7b3280 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ vignettes/*.pdf # R Environment Variables .Renviron + +# documentation page +docs From f16026cc3a5b50bd7945989dd628348f9db1b2bc Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 31 Oct 2022 12:46:35 +0100 Subject: [PATCH 154/225] Add sections to the reference page. --- pkgdown/_pkgdown.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index d33e38b..2baf27b 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -31,3 +31,21 @@ navbar: href: https://github.com/Appsilon/shiny.benchmark - icon: fa-twitter fa-lg href: https://twitter.com/Appsilon + +reference: +- title: Performance tests + contents: + - '`benchmark`' + - '`benchmark_cypress`' + - '`benchmark_shinytest2`' + - '`run_cypress_ptest`' + - '`run_shinytest2_ptest`' +- title: Shiny Benchmark Class + contents: + - '`shiny_benchmark-class`' + - '`summary.shiny_benchmark`' + - '`plot.shiny_benchmark`' + - '`print.shiny_benchmark`' +- title: Other + contents: + - '`load_example`' From b397a19d81be9e90a55fa3bcdcaaff20af818a35 Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 31 Oct 2022 12:51:18 +0100 Subject: [PATCH 155/225] Automate page deployment. --- .github/workflows/pkgdown.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/pkgdown.yml diff --git a/.github/workflows/pkgdown.yml b/.github/workflows/pkgdown.yml new file mode 100644 index 0000000..274c643 --- /dev/null +++ b/.github/workflows/pkgdown.yml @@ -0,0 +1,35 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + push: + branches: + - develop + - kuba-74-add-documentation-page + workflow_dispatch: + +name: pkgdown + +jobs: + pkgdown: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v3 + + - uses: r-lib/actions/setup-pandoc@v2 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::pkgdown, local::. + needs: website + + - name: Deploy to gh-pages branch + run: | + git config --local user.name "$GITHUB_ACTOR" + git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" + Rscript -e 'pkgdown::deploy_to_branch(new_process = FALSE)' From 10e01a420e43b8b6251854488640709f6b6054d7 Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 31 Oct 2022 12:57:42 +0100 Subject: [PATCH 156/225] Add url. --- .github/workflows/pkgdown.yml | 1 - pkgdown/_pkgdown.yml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pkgdown.yml b/.github/workflows/pkgdown.yml index 274c643..352ce54 100644 --- a/.github/workflows/pkgdown.yml +++ b/.github/workflows/pkgdown.yml @@ -4,7 +4,6 @@ on: push: branches: - develop - - kuba-74-add-documentation-page workflow_dispatch: name: pkgdown diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index 2baf27b..b6d8b23 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -15,7 +15,7 @@ template: gtag('config', 'G-RS06EY8KNQ'); -url: +url: https://github.com/Appsilon/shiny.benchmark/ navbar: bg: primary From 4ab91cbf1a41027c1118215dcb29d9f9dd296eea Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 31 Oct 2022 13:11:07 +0100 Subject: [PATCH 157/225] Update runners and checkout version in R-CMD-check --- .github/workflows/main.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2a4a55a..8c76f40 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,11 +14,15 @@ jobs: fail-fast: false matrix: config: + - {os: macOS-latest, r: 'release'} + - {os: windows-latest, r: 'release'} + - {os: ubuntu-22.04, r: 'devel'} - {os: ubuntu-22.04, r: 'release'} + - {os: ubuntu-22.04, r: 'oldrel'} steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install R uses: r-lib/actions/setup-r@v2 From addbb5473016ee29c700c03aa808af2e937eef6f Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 31 Oct 2022 14:25:15 +0100 Subject: [PATCH 158/225] Use different OS for e2e tests. --- .github/workflows/tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3cda07c..df0e91f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,7 +18,11 @@ jobs: fail-fast: false matrix: config: + - {os: macOS-latest, r: 'release'} + - {os: windows-latest, r: 'release'} + - {os: ubuntu-22.04, r: 'devel'} - {os: ubuntu-22.04, r: 'release'} + - {os: ubuntu-22.04, r: 'oldrel'} env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} From 047e52c5c4481f518186f669b225408170e0b9ea Mon Sep 17 00:00:00 2001 From: DouglasMesquita Date: Mon, 31 Oct 2022 14:25:15 +0100 Subject: [PATCH 159/225] removing description title and adding `` to package names --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 33a0a74..8157ae3 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,9 @@ ![R-CMD-check](https://github.com/Appsilon/shiny.benchmark/workflows/R-CMD-check/badge.svg) -Description +`shiny.benchmark` is a tool aimed to measure and compare the performance of different versions of a `shiny` application. Based on a list of different application versions, accessible by a git repo by its refs (commit hash or branch name), the user can write instructions to be executed using Cypress or `shinytest2`. These instructions are then evaluated by the different versions of your `shiny` application and therefore the performance's improvement/deterioration (time elapsed) are be recorded. -`shiny.benchmark` is a tool aimed to measure and compare the performance of different versions of a shiny application. Based on a list of different application versions, accessible by a git repo by its refs (commit hash or branch name), the user can write instructions to be executed using Cypress or shinytest2. These instructions are then evaluated by the different versions of your shiny application and therefore the performance's improvement/deterioration (time elapsed) are be recorded. - -The package is flexible enough to allow different sets of tests for the different refs as well as different package versions (via renv). Also, the user can replicate the tests to have more accurate measures of performance. +The package is flexible enough to allow different sets of tests for the different refs as well as different package versions (via `renv`). Also, the user can replicate the tests to have more accurate measures of performance. How to install? --------------- From 319626a767388b752dcef03542ec07363a347087 Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 31 Oct 2022 14:39:44 +0100 Subject: [PATCH 160/225] Use Rscript for all actions. --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index df0e91f..666e01c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,8 +44,8 @@ jobs: - name: Install shiny.benchmark run: | - R -e "install.packages('remotes')" - R -e "remotes::install_github(repo = 'Appsilon/shiny.benchmark', auth_token = Sys.getenv('GITHUB_PAT'), ref = Sys.getenv('BRANCH_NAME'), quiet = TRUE)" + Rscript -e "install.packages('remotes')" + Rscript -e "remotes::install_github(repo = 'Appsilon/shiny.benchmark', auth_token = Sys.getenv('GITHUB_PAT'), ref = Sys.getenv('BRANCH_NAME'), quiet = TRUE)" - name: Create app structure run: | From 93b8dae4420fc01ed3207cce7ff9d1dd70ff92ec Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 31 Oct 2022 14:58:21 +0100 Subject: [PATCH 161/225] Simplify package installation. --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 666e01c..8341244 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,14 +38,14 @@ jobs: r-version: ${{ matrix.config.r }} - name: Install R package dependencies - uses: r-lib/actions/setup-r-dependencies@v2 + uses: r-lib/actions/setup-r-dependencies@v3 with: extra-packages: local::. # Necessary to avoid object usage linter errors. - name: Install shiny.benchmark run: | Rscript -e "install.packages('remotes')" - Rscript -e "remotes::install_github(repo = 'Appsilon/shiny.benchmark', auth_token = Sys.getenv('GITHUB_PAT'), ref = Sys.getenv('BRANCH_NAME'), quiet = TRUE)" + Rscript -e "remotes::install_local(quiet = TRUE)" - name: Create app structure run: | From b462b3e93dd0f3a60ff79f856380fced0f81f80f Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 31 Oct 2022 15:00:23 +0100 Subject: [PATCH 162/225] Use new checkout action. --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8341244..bcb8031 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,7 +30,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install R uses: r-lib/actions/setup-r@v2 @@ -38,7 +38,7 @@ jobs: r-version: ${{ matrix.config.r }} - name: Install R package dependencies - uses: r-lib/actions/setup-r-dependencies@v3 + uses: r-lib/actions/setup-r-dependencies@v2 with: extra-packages: local::. # Necessary to avoid object usage linter errors. From 4ce34f54a4f8d329ba9b82fbd54b718da39ea39f Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 31 Oct 2022 15:06:15 +0100 Subject: [PATCH 163/225] Test local working directory. --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bcb8031..fa8b23f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,6 +44,8 @@ jobs: - name: Install shiny.benchmark run: | + pwd + ls Rscript -e "install.packages('remotes')" Rscript -e "remotes::install_local(quiet = TRUE)" From 57f28d20023d27a98ddcfbb3246778cb61f4c761 Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 31 Oct 2022 15:10:03 +0100 Subject: [PATCH 164/225] fix path for local installation. --- .github/workflows/tests.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fa8b23f..b744bdf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,10 +44,8 @@ jobs: - name: Install shiny.benchmark run: | - pwd - ls Rscript -e "install.packages('remotes')" - Rscript -e "remotes::install_local(quiet = TRUE)" + Rscript -e "remotes::install_local("../../../", quiet = TRUE)" - name: Create app structure run: | From 75f6e9471174c3a49ca49272fa4b53b00e2cb21f Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 31 Oct 2022 15:12:49 +0100 Subject: [PATCH 165/225] Fix quotation marks. --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b744bdf..8f91b31 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -45,7 +45,7 @@ jobs: - name: Install shiny.benchmark run: | Rscript -e "install.packages('remotes')" - Rscript -e "remotes::install_local("../../../", quiet = TRUE)" + Rscript -e "remotes::install_local('../../../'', quiet = TRUE)" - name: Create app structure run: | From 90390afc0305985a286ffcf357608655a757d351 Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 31 Oct 2022 15:17:31 +0100 Subject: [PATCH 166/225] Fix quotation one more time. --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8f91b31..c34d443 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -45,7 +45,7 @@ jobs: - name: Install shiny.benchmark run: | Rscript -e "install.packages('remotes')" - Rscript -e "remotes::install_local('../../../'', quiet = TRUE)" + Rscript -e "remotes::install_local('../../../', quiet = TRUE)" - name: Create app structure run: | From 2636283efb4e6f04ee21a063809091958294134f Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Thu, 17 Nov 2022 14:42:26 +0100 Subject: [PATCH 167/225] docs: Add hex to the readme. --- README.md | 4 ++-- man/figures/shiny_benchmark.png | Bin 0 -> 137322 bytes 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 man/figures/shiny_benchmark.png diff --git a/README.md b/README.md index 8157ae3..b31f764 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# shiny.benchmark +# shiny.benchmark shiny.benchmark logo > _Tools to measure performance improvements in Shiny apps._ -![R-CMD-check](https://github.com/Appsilon/shiny.benchmark/workflows/R-CMD-check/badge.svg) +[![R-CMD-check](https://github.com/Appsilon/shiny.benchmark/workflows/R-CMD-check/badge.svg)](https://github.com/Appsilon/shiny.benchmark/actions?workflow=R-CMD-check) `shiny.benchmark` is a tool aimed to measure and compare the performance of different versions of a `shiny` application. Based on a list of different application versions, accessible by a git repo by its refs (commit hash or branch name), the user can write instructions to be executed using Cypress or `shinytest2`. These instructions are then evaluated by the different versions of your `shiny` application and therefore the performance's improvement/deterioration (time elapsed) are be recorded. diff --git a/man/figures/shiny_benchmark.png b/man/figures/shiny_benchmark.png new file mode 100644 index 0000000000000000000000000000000000000000..76f943b5723a02b7725052a1ece27db13fbd18e3 GIT binary patch literal 137322 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

MfxG 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%0A{{a#(|ci!2&lfp?^zZ(-ilr;QmIm>K>&@P2~ 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

yB%)r6<3f?m;nD(xV8* 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 Date: Thu, 17 Nov 2022 14:45:22 +0100 Subject: [PATCH 168/225] docs: Use hex color in the documentation page. --- pkgdown/extra.css | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkgdown/extra.css b/pkgdown/extra.css index f7bcb77..7f9c6f3 100644 --- a/pkgdown/extra.css +++ b/pkgdown/extra.css @@ -1,13 +1,13 @@ .navbar { - background-color: rgb(177, 30, 30) !important; + background-color: rgb(178, 9, 41) !important; } #navbar > ul.navbar-nav > li.nav-item a:hover { - background-color: rgb(177, 30, 30) !important; + background-color: rgb(178, 9, 41) !important; } .navbar-dark .navbar-nav .active>.nav-link { - background-color: rgb(177, 30, 30) !important; + background-color: rgb(178, 9, 41) !important; color: #fff; } @@ -21,16 +21,16 @@ nav .text-muted { } a { - color: rgb(124, 23, 23); + color: rgb(156, 19, 44); } a:hover { - color: rgb(177, 30, 30); + color: rgb(178, 9, 41); } button.btn.btn-primary.btn-copy-ex { - background-color: rgb(177, 30, 30); - border-color: rgb(177, 30, 30); + background-color: rgb(178, 9, 41); + border-color: rgb(178, 9, 41); } .app-preview { From fee2af3ab29f578ca9ce617c610b66c396e556d1 Mon Sep 17 00:00:00 2001 From: Marek Rogala Date: Thu, 17 Nov 2022 22:06:20 +0100 Subject: [PATCH 169/225] Update package name --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b31f764..7e49b35 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Check the [documentation](https://docs.cypress.io/guides/getting-started/install How to use it? -------------- -The best way to start using `shiny.performance` is through an example. If you want a start point, you can use the `load_example` function. In order to use this, create a new folder in your computer and use the following code to generate an application to serve us as example for our performance checks: +The best way to start using `shiny.benchmark` is through an example. If you want a start point, you can use the `load_example` function. In order to use this, create a new folder in your computer and use the following code to generate an application to serve us as example for our performance checks: ```r library(shiny.benchmark) From de7fbce032487ab0194a4293b0e06d8d21acb651 Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Wed, 23 Nov 2022 13:10:50 +0100 Subject: [PATCH 170/225] ci: Add spelling check to the CI. --- .github/workflows/main.yml | 10 ++++++++++ DESCRIPTION | 6 ++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8c76f40..a014779 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -47,3 +47,13 @@ jobs: lints <- lintr::lint_package() for (lint in lints) print(lint) quit(status = length(lints) > 0) + + - name: Spell Check + if : always() + shell: Rscript {0} + run: | + spell_check <- spelling::spell_check_package(use_wordlist = TRUE) + if (nrow(spell_check) > 0) { + print(spell_check) + } + quit(status = nrow(spell_check) > 0) diff --git a/DESCRIPTION b/DESCRIPTION index fe36ddc..e9932ef 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -7,7 +7,7 @@ Authors@R: person("Developers", "Appsilon", email = "support+opensource@appsilon.com", role = "cre"), person(family = "Appsilon Sp. z o.o.", role = "cph") ) -Description: Compare performance of several versions of a shiny app based on commit hashs. +Description: Compare performance of several versions of a shiny app based on commit hashes. License: LGPL-3 URL: https://github.com/Appsilon/shiny.benchmark SystemRequirements: yarn 1.22.17 or higher, Node 12 or higher @@ -21,7 +21,8 @@ Depends: Suggests: knitr, lintr, - rcmdcheck + rcmdcheck, + spelling Imports: dplyr, ggplot2, @@ -33,3 +34,4 @@ Imports: shinytest2, stringr, testthat +Language: en-US From deb2ccaf2d624c5281fbe8e6adfb21a077e179b5 Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Wed, 23 Nov 2022 13:26:19 +0100 Subject: [PATCH 171/225] docs: Fix spelling errors. --- R/shiny_benchmark-class.R | 2 +- README.md | 10 +++++----- inst/WORDLIST | 11 +++++++++++ man/shiny_benchmark-class.Rd | 2 +- 4 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 inst/WORDLIST diff --git a/R/shiny_benchmark-class.R b/R/shiny_benchmark-class.R index 260de03..34bf8ee 100644 --- a/R/shiny_benchmark-class.R +++ b/R/shiny_benchmark-class.R @@ -2,7 +2,7 @@ #' #' @slot call Function call #' @slot time Time elapsed -#' @slot performance List of measuraments (one entry for each commit) +#' @slot performance List of measurements (one entry for each commit) #' #' @importFrom methods new #' diff --git a/README.md b/README.md index 7e49b35..8ec6a79 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ load_example(path = "path/to/new/project") It will create some useful files under `path/to/new/project`. The most important one is the `run_tests.R` which provides several instructions at the very top. -As we are comparing versions of the same application, we need different app versions in different branches/commits in `git`. Start using `cd app; git init` to iniciate git inside `app/` folder. +As we are comparing versions of the same application, we need different app versions in different branches/commits in `git`. Start using `cd app; git init` to initiate git inside `app/` folder. Get familiar with `app/server.R` file in order to generate more interesting scenarios. The basic idea is to use the `Sys.sleep` function to simulate some app's functionalities. Remember that, when running the benchmark, that is the amount of time it will take to measure the performance. @@ -126,7 +126,7 @@ benchmark( ) ``` -If your project has `renv` strucure, you can set `use_renv` to `TRUE` to guarantee that, for each application version your are using the correct packages. If you want to approve/reprove `renv::restore()`, you can set `renv_prompt = TRUE`. +If your project has `renv` structure, you can set `use_renv` to `TRUE` to guarantee that, for each application version your are using the correct packages. If you want to approve/reprove `renv::restore()`, you can set `renv_prompt = TRUE`. ```r benchmark( @@ -138,7 +138,7 @@ benchmark( ) ``` -To have more acurate information about the time your application takes to perform some actions, you may need to replicate the tests. In this case, you can use the `n_rep` argument: +To have more accurate information about the time your application takes to perform some actions, you may need to replicate the tests. In this case, you can use the `n_rep` argument: ```r out <- benchmark( @@ -152,7 +152,7 @@ out <- benchmark( out ``` -For fast information about the tests's results, you can use the `summary` and also the `plot` methods: +For fast information about the tests results, you can use the `summary` and also the `plot` methods: ```r summary(out) @@ -171,7 +171,7 @@ Appsilon -Appsilon is the **Full Service Certified RStudio Partner**. Learn more +Appsilon is the **Full Service Certified Posit Partner**. Learn more at [appsilon.com](https://appsilon.com). Get in touch [opensource@appsilon.com](opensource@appsilon.com) diff --git a/inst/WORDLIST b/inst/WORDLIST new file mode 100644 index 0000000..f3ffef9 --- /dev/null +++ b/inst/WORDLIST @@ -0,0 +1,11 @@ +CMD +JS +POSIXct +Posit +appsilon +dir +js +renv +repo +sendTime +shinytest diff --git a/man/shiny_benchmark-class.Rd b/man/shiny_benchmark-class.Rd index 60483a8..6028c28 100644 --- a/man/shiny_benchmark-class.Rd +++ b/man/shiny_benchmark-class.Rd @@ -15,6 +15,6 @@ An object of 'shiny_benchmark' class \item{\code{time}}{Time elapsed} -\item{\code{performance}}{List of measuraments (one entry for each commit)} +\item{\code{performance}}{List of measurements (one entry for each commit)} }} From efaa0b22580eeb047ad6dbc7001a27b40daa233d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Fri, 25 Nov 2022 13:34:47 +0800 Subject: [PATCH 172/225] Correcting first bug with cd and symlink #45 --- DESCRIPTION | 5 +-- R/benchmark_cypress.R | 12 ++++--- R/globals.R | 3 ++ R/utils.R | 62 +++++++++++++++++++++++++++++++++++++ R/utils_cypress.R | 19 ++++++++++-- man/command_wrapper.Rd | 26 ++++++++++++++++ man/commit_exists.Rd | 19 ++++++++++++ man/performance_test_cmd.Rd | 15 +++++++++ 8 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 man/command_wrapper.Rd create mode 100644 man/commit_exists.Rd create mode 100644 man/performance_test_cmd.Rd diff --git a/DESCRIPTION b/DESCRIPTION index e9932ef..545b643 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -14,7 +14,7 @@ SystemRequirements: yarn 1.22.17 or higher, Node 12 or higher Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.1 +RoxygenNote: 7.2.2 VignetteBuilder: knitr Depends: R (>= 3.1.0) @@ -33,5 +33,6 @@ Imports: renv, shinytest2, stringr, - testthat + testthat, + logger Language: en-US diff --git a/R/benchmark_cypress.R b/R/benchmark_cypress.R index fdc99bc..7ad2606 100644 --- a/R/benchmark_cypress.R +++ b/R/benchmark_cypress.R @@ -55,6 +55,7 @@ benchmark_cypress <- function( ) }, error = function(e) { + logger::log_fatal("ERROR!!! {e}") message(e) }, finally = { @@ -69,6 +70,7 @@ benchmark_cypress <- function( # Cleaning the temporary directory unlink( x = c( + file.path(project_path, "node", "root"), file.path(project_path, "node"), file.path(project_path, "tests") ), @@ -107,6 +109,9 @@ run_cypress_ptest <- function( n_rep, debug ) { + + # Checks if commit exists + commit_exists(commit) # checkout to the desired commit checkout(branch = commit, debug = debug) date <- get_commit_date(branch = commit) @@ -130,10 +135,9 @@ run_cypress_ptest <- function( pb$tick() # run tests there - command <- glue( - "cd {project_path}; set -eu; exec yarn --cwd node performance-test" - ) - system(command, ignore.stdout = !debug, ignore.stderr = !debug) + command <- performance_test_cmd(project_path) + debug = TRUE + result <- command_wrapper(command, ignore.stdout = !debug, ignore.stderr = !debug) # read the file saved by cypress perf_file[[i]] <- read.table(file = txt_file, header = FALSE, sep = ";") diff --git a/R/globals.R b/R/globals.R index 06155fd..33b5afc 100644 --- a/R/globals.R +++ b/R/globals.R @@ -12,3 +12,6 @@ utils::globalVariables( "total_time" ) ) + +# Setting threshold to debug (temporary and should be removed) +logger::log_threshold(logger::DEBUG) diff --git a/R/utils.R b/R/utils.R index a7cf9af..d45d456 100644 --- a/R/utils.R +++ b/R/utils.R @@ -81,6 +81,68 @@ checkout <- function(branch, debug) { ) } +#' @title Running the node script "performance_test" is system-dependent +#' +#' @param project_path path to project directory (one level above node) +#' +#' @keywords internal +performance_test_cmd <- function(project_path) { + if (grepl("win", .Platform$OS.type)) { + glue("yarn --cwd \"{file.path(project_path, 'node')}\" performance-test") + } else { + glue("cd {project_path}; set -eu; exec yarn --cwd node performance-test") + } +} + +#' @title Wrapper to call on Operating System commands +#' +#' @param cmd command +#' @param system a logical (not NA) which indicates whether to use +#' shell or system call (system is a more low-level call) +#' @param intern a logical (not NA) which indicates whether to capture +#' the output of the command as an R character vector. +#' +#' @param ... Other paramters passed to shell or system +#' +#' @return see system or shell +#' +#' @keywords internal +command_wrapper <- function(cmd, system = FALSE, intern = FALSE, ...) { + logger::log_debug("command {ifelse(system, 'system', 'shell')}: {cmd}") + if (system) { + system(cmd, intern = inter, ...) + } else { + shell(cmd, intern = intern, ...) + } +} + +#' @title Check if git commit hash exists +#' +#' @description Can be anything git recognizes as a commit, such +#' as a commit hash, a branch, a tag, ... +#' +#' @param commit commit hash code, branch name or tag +#' +#' @return true if exists, an error message if not +#' +#' @keywords internal +commit_exists <- function (commit) { + result <- shell( + cmd = glue::glue("git rev-parse --verify {commit}"), + intern = FALSE, + mustWork = NA, + wait = TRUE + ) + if (result == 128) { + rlang::abort( + message = glue( + "git error:: Commit/branch/tag/.. '{commit}' doesn't exist" + ) + ) + } + TRUE +} + #' @title Check for uncommitted files #' #' @keywords internal diff --git a/R/utils_cypress.R b/R/utils_cypress.R index c7bca8d..6709221 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -29,7 +29,15 @@ create_cypress_structure <- function(app_dir, port, debug) { dir.create(path = plugins_path, showWarnings = FALSE) # create a path root linked to the main directory app - file.symlink(from = app_dir, to = root_path) + linked <- file.symlink(from = app_dir, to = root_path) + # If system cannot symlink then try to clone the repository + # This may happen on some windows versions + # This can be an expensive operation on big repositories + if (!linked) { + shell(glue::glue("git clone \"{app_dir}\" \"{root_path}\"")) + shell("git submodule init") + shell("git submodule update ") + } # create the packages.json file json_txt <- create_node_list(tests_path = tests_path, port = port) @@ -68,7 +76,11 @@ create_node_list <- function(tests_path, port) { "performance-test" = glue( "start-server-and-test run-app http://localhost:{port} run-cypress" ), - "run-app" = glue("cd root && Rscript -e 'shiny::runApp(port = {port})'"), + "run-app" = glue( + "cd root && ", + "Rscript -e \"installed.packages()[,1] |> sort()\" && ", + "Rscript -e \"shiny::runApp(port = {port})\"" + ), "run-cypress" = glue("cypress run --project {tests_path}") ), "devDependencies" = list( @@ -154,6 +166,9 @@ create_cypress_tests <- function(project_path, cypress_dir, tests_pattern) { # file to store the times txt_file <- file.path(project_path, "tests", "cypress", "performance.txt") + if (!file.exists(txt_file)) { + file.create(txt_file) # touch file if it doesn't exist + } add_sendtime2js(js_file = js_file, txt_file = txt_file) # returning the file location diff --git a/man/command_wrapper.Rd b/man/command_wrapper.Rd new file mode 100644 index 0000000..a112cd9 --- /dev/null +++ b/man/command_wrapper.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{command_wrapper} +\alias{command_wrapper} +\title{Wrapper to call on Operating System commands} +\usage{ +command_wrapper(cmd, system = FALSE, intern = FALSE, ...) +} +\arguments{ +\item{cmd}{command} + +\item{system}{a logical (not NA) which indicates whether to use +shell or system call (system is a more low-level call)} + +\item{intern}{a logical (not NA) which indicates whether to capture +the output of the command as an R character vector.} + +\item{...}{Other paramters passed to shell or system} +} +\value{ +see system or shell +} +\description{ +Wrapper to call on Operating System commands +} +\keyword{internal} diff --git a/man/commit_exists.Rd b/man/commit_exists.Rd new file mode 100644 index 0000000..5e66203 --- /dev/null +++ b/man/commit_exists.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{commit_exists} +\alias{commit_exists} +\title{Check if git commit hash exists} +\usage{ +commit_exists(commit) +} +\arguments{ +\item{commit}{commit hash code, branch name or tag} +} +\value{ +true if exists, an error message if not +} +\description{ +Can be anything git recognizes as a commit, such +as a commit hash, a branch, a tag, ... +} +\keyword{internal} diff --git a/man/performance_test_cmd.Rd b/man/performance_test_cmd.Rd new file mode 100644 index 0000000..82a390a --- /dev/null +++ b/man/performance_test_cmd.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{performance_test_cmd} +\alias{performance_test_cmd} +\title{Running the node script "performance_test" is system-dependent} +\usage{ +performance_test_cmd(project_path) +} +\arguments{ +\item{project_path}{path to project directory (one level above node)} +} +\description{ +Running the node script "performance_test" is system-dependent +} +\keyword{internal} From 069495288091879861e5c7f54e3c374cf674a1cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Fri, 25 Nov 2022 13:39:32 +0800 Subject: [PATCH 173/225] fixes lint erros --- R/benchmark_cypress.R | 4 ++-- R/utils.R | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/R/benchmark_cypress.R b/R/benchmark_cypress.R index 7ad2606..7f9d4b9 100644 --- a/R/benchmark_cypress.R +++ b/R/benchmark_cypress.R @@ -136,8 +136,8 @@ run_cypress_ptest <- function( # run tests there command <- performance_test_cmd(project_path) - debug = TRUE - result <- command_wrapper(command, ignore.stdout = !debug, ignore.stderr = !debug) + debug <- TRUE + command_wrapper(command, ignore.stdout = !debug, ignore.stderr = !debug) # read the file saved by cypress perf_file[[i]] <- read.table(file = txt_file, header = FALSE, sep = ";") diff --git a/R/utils.R b/R/utils.R index d45d456..e8d8e0d 100644 --- a/R/utils.R +++ b/R/utils.R @@ -110,7 +110,7 @@ performance_test_cmd <- function(project_path) { command_wrapper <- function(cmd, system = FALSE, intern = FALSE, ...) { logger::log_debug("command {ifelse(system, 'system', 'shell')}: {cmd}") if (system) { - system(cmd, intern = inter, ...) + system(cmd, intern = intern, ...) } else { shell(cmd, intern = intern, ...) } @@ -126,7 +126,7 @@ command_wrapper <- function(cmd, system = FALSE, intern = FALSE, ...) { #' @return true if exists, an error message if not #' #' @keywords internal -commit_exists <- function (commit) { +commit_exists <- function(commit) { result <- shell( cmd = glue::glue("git rev-parse --verify {commit}"), intern = FALSE, From 016e5b8640bdca5577050eaf5028c1673f69dc6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Fri, 25 Nov 2022 13:51:19 +0800 Subject: [PATCH 174/225] shell is only available in windows --- R/benchmark_cypress.R | 1 - R/utils.R | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/R/benchmark_cypress.R b/R/benchmark_cypress.R index 7f9d4b9..85a07f9 100644 --- a/R/benchmark_cypress.R +++ b/R/benchmark_cypress.R @@ -70,7 +70,6 @@ benchmark_cypress <- function( # Cleaning the temporary directory unlink( x = c( - file.path(project_path, "node", "root"), file.path(project_path, "node"), file.path(project_path, "tests") ), diff --git a/R/utils.R b/R/utils.R index e8d8e0d..94c7fc1 100644 --- a/R/utils.R +++ b/R/utils.R @@ -107,11 +107,12 @@ performance_test_cmd <- function(project_path) { #' @return see system or shell #' #' @keywords internal -command_wrapper <- function(cmd, system = FALSE, intern = FALSE, ...) { - logger::log_debug("command {ifelse(system, 'system', 'shell')}: {cmd}") - if (system) { +command_wrapper <- function(cmd, intern = FALSE, ...) { + if (grepl("win", .Platform$OS.type)) { + logger::log_debug("cmd (shell): {cmd}") system(cmd, intern = intern, ...) } else { + logger::log_debug("cmd (system): {cmd}") shell(cmd, intern = intern, ...) } } From 7fe7eb8efafffb6ecff0f12f79615ef2b4412cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Fri, 25 Nov 2022 14:09:31 +0800 Subject: [PATCH 175/225] Use fs::path instead of file.path --- DESCRIPTION | 1 + NAMESPACE | 1 + R/benchmark_cypress.R | 11 +++--- R/benchmark_shinytest2.R | 2 +- R/utils.R | 8 ++--- R/utils_cypress.R | 47 +++++++++++++++----------- R/utils_shinytest2.R | 4 +-- man/command_wrapper.Rd | 8 ++--- tests/testthat/test-utils_cypress.R | 6 ++-- tests/testthat/test-utils_shinytest2.R | 10 +++--- 10 files changed, 52 insertions(+), 46 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 545b643..63894bf 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -34,5 +34,6 @@ Imports: shinytest2, stringr, testthat, + fs, logger Language: en-US diff --git a/NAMESPACE b/NAMESPACE index a51ca51..b6b869e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -13,6 +13,7 @@ export(shiny_benchmark_class) exportClasses(shiny_benchmark) import(dplyr) import(ggplot2) +importFrom(fs,path) importFrom(glue,glue) importFrom(jsonlite,write_json) importFrom(methods,new) diff --git a/R/benchmark_cypress.R b/R/benchmark_cypress.R index 85a07f9..0424b2d 100644 --- a/R/benchmark_cypress.R +++ b/R/benchmark_cypress.R @@ -16,6 +16,8 @@ #' @param n_rep Number of replications desired #' @param debug Logical. TRUE to display all the system messages on runtime #' +#' @importFrom fs path link_create file_delete +#' #' @export benchmark_cypress <- function( commit_list, @@ -68,13 +70,8 @@ benchmark_cypress <- function( restore_env(branch = current_branch, renv_prompt = renv_prompt) # Cleaning the temporary directory - unlink( - x = c( - file.path(project_path, "node"), - file.path(project_path, "tests") - ), - recursive = TRUE - ) + file_delete(path(project_path, "node")) + file_delete(path(project_path, "tests")) } ) diff --git a/R/benchmark_shinytest2.R b/R/benchmark_shinytest2.R index e3fd9af..3e64eb1 100644 --- a/R/benchmark_shinytest2.R +++ b/R/benchmark_shinytest2.R @@ -62,7 +62,7 @@ benchmark_shinytest2 <- function( restore_env(branch = current_branch, renv_prompt = renv_prompt) # Cleaning the temporary directory - unlink(x = file.path(project_path, "tests"), recursive = TRUE) + file_delete(path(project_path, "tests")) } ) diff --git a/R/utils.R b/R/utils.R index 94c7fc1..954b9ec 100644 --- a/R/utils.R +++ b/R/utils.R @@ -87,8 +87,8 @@ checkout <- function(branch, debug) { #' #' @keywords internal performance_test_cmd <- function(project_path) { - if (grepl("win", .Platform$OS.type)) { - glue("yarn --cwd \"{file.path(project_path, 'node')}\" performance-test") + if (grepl("win", .Platform$OS.type, ignore.case = TRUE)) { + glue("yarn --cwd \"{path(project_path, 'node')}\" performance-test") } else { glue("cd {project_path}; set -eu; exec yarn --cwd node performance-test") } @@ -108,7 +108,7 @@ performance_test_cmd <- function(project_path) { #' #' @keywords internal command_wrapper <- function(cmd, intern = FALSE, ...) { - if (grepl("win", .Platform$OS.type)) { + if (grepl("win", .Platform$OS.type, ignore.case = TRUE)) { logger::log_debug("cmd (shell): {cmd}") system(cmd, intern = intern, ...) } else { @@ -263,6 +263,6 @@ load_example <- function(path) { print(glue("{basename(file)} created at {path}")) } - fpath <- file.path(path, "run_tests.R") # nolint + fpath <- path(path, "run_tests.R") # nolint message(glue("Follow instructions in {fpath}")) } diff --git a/R/utils_cypress.R b/R/utils_cypress.R index 6709221..e4d0cae 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -12,14 +12,14 @@ create_cypress_structure <- function(app_dir, port, debug) { dir_tests <- tempdir() # node path - node_path <- file.path(dir_tests, "node") - root_path <- file.path(node_path, "root") # nolint + node_path <- path(dir_tests, "node") + root_path <- path(node_path, "root") # nolint # test path - tests_path <- file.path(dir_tests, "tests") - cypress_path <- file.path(tests_path, "cypress") - integration_path <- file.path(cypress_path, "integration") - plugins_path <- file.path(cypress_path, "plugins") + tests_path <- path(dir_tests, "tests") + cypress_path <- path(tests_path, "cypress") + integration_path <- path(cypress_path, "integration") + plugins_path <- path(cypress_path, "plugins") # creating paths dir.create(path = node_path, showWarnings = FALSE) @@ -29,19 +29,26 @@ create_cypress_structure <- function(app_dir, port, debug) { dir.create(path = plugins_path, showWarnings = FALSE) # create a path root linked to the main directory app - linked <- file.symlink(from = app_dir, to = root_path) - # If system cannot symlink then try to clone the repository - # This may happen on some windows versions - # This can be an expensive operation on big repositories - if (!linked) { - shell(glue::glue("git clone \"{app_dir}\" \"{root_path}\"")) - shell("git submodule init") - shell("git submodule update ") - } + tryCatch( + expr = { + linked <- link_create(app_dir, root_path, symbolic = TRUE) + }, + error = function(e) { + # If system cannot symlink then try to clone the repository + # This may happen on some windows versions + # This can be an expensive operation on big repositories + logger::log_warn( + "Could not create symbolic link with fs package, ", + "trying with git clone..." + ) + command_wrapper(glue::glue("git clone \"{app_dir}\" \"{root_path}\"")) + command_wrapper("git submodule init") + command_wrapper("git submodule update ") + }) # create the packages.json file json_txt <- create_node_list(tests_path = tests_path, port = port) - json_file <- file.path(node_path, "package.json") + json_file <- path(node_path, "package.json") write_json(x = json_txt, path = json_file, pretty = TRUE, auto_unbox = TRUE) # install everything that is needed @@ -50,12 +57,12 @@ create_cypress_structure <- function(app_dir, port, debug) { # creating cypress plugin file js_txt <- create_cypress_plugins() - js_file <- file.path(plugins_path, "index.js") + js_file <- path(plugins_path, "index.js") writeLines(text = js_txt, con = js_file) # creating cypress.json json_txt <- create_cypress_list(plugins_file = js_file, port = port) - json_file <- file.path(tests_path, "cypress.json") + json_file <- path(tests_path, "cypress.json") write_json(x = json_txt, path = json_file, pretty = TRUE, auto_unbox = TRUE) # returning the project folder @@ -150,7 +157,7 @@ create_cypress_tests <- function(project_path, cypress_dir, tests_pattern) { cypress_files <- grep(x = cypress_files, pattern = "\\.js$", value = TRUE) # creating a copy to be able to edit the js file - js_file <- file.path( + js_file <- path( project_path, "tests", "cypress", @@ -165,7 +172,7 @@ create_cypress_tests <- function(project_path, cypress_dir, tests_pattern) { } # file to store the times - txt_file <- file.path(project_path, "tests", "cypress", "performance.txt") + txt_file <- path(project_path, "tests", "cypress", "performance.txt") if (!file.exists(txt_file)) { file.create(txt_file) # touch file if it doesn't exist } diff --git a/R/utils_shinytest2.R b/R/utils_shinytest2.R index 6392937..1cb3aa6 100644 --- a/R/utils_shinytest2.R +++ b/R/utils_shinytest2.R @@ -12,7 +12,7 @@ create_shinytest2_structure <- function(app_dir) { # shiny call writeLines( text = glue('shiny::runApp(appDir = "{app_dir}")'), - con = file.path(dir_tests, "app.R") + con = path(dir_tests, "app.R") ) # returning the project folder @@ -30,7 +30,7 @@ create_shinytest2_structure <- function(app_dir) { move_shinytest2_tests <- function(project_path, shinytest2_dir) { # copy everything to the temporary directory file.copy(from = shinytest2_dir, to = project_path, recursive = TRUE) - tests_dir <- file.path(project_path, "tests") + tests_dir <- path(project_path, "tests") return(tests_dir) } diff --git a/man/command_wrapper.Rd b/man/command_wrapper.Rd index a112cd9..7158d59 100644 --- a/man/command_wrapper.Rd +++ b/man/command_wrapper.Rd @@ -4,18 +4,18 @@ \alias{command_wrapper} \title{Wrapper to call on Operating System commands} \usage{ -command_wrapper(cmd, system = FALSE, intern = FALSE, ...) +command_wrapper(cmd, intern = FALSE, ...) } \arguments{ \item{cmd}{command} -\item{system}{a logical (not NA) which indicates whether to use -shell or system call (system is a more low-level call)} - \item{intern}{a logical (not NA) which indicates whether to capture the output of the command as an R character vector.} \item{...}{Other paramters passed to shell or system} + +\item{system}{a logical (not NA) which indicates whether to use +shell or system call (system is a more low-level call)} } \value{ see system or shell diff --git a/tests/testthat/test-utils_cypress.R b/tests/testthat/test-utils_cypress.R index faaa6f9..ced7fc0 100644 --- a/tests/testthat/test-utils_cypress.R +++ b/tests/testthat/test-utils_cypress.R @@ -1,11 +1,11 @@ test_that("Check if we are able to add Cypress code to a txt file", { tmp_dir <- tempdir() add_sendtime2js( - js_file = file.path(tmp_dir, "test.js"), + js_file = path(tmp_dir, "test.js"), txt_file = "test.txt" ) - expect_true(file.exists(file.path(tmp_dir, "test.js"))) + expect_true(file.exists(path(tmp_dir, "test.js"))) }) test_that("Check if we are able to copy file content from a file to another", { @@ -14,7 +14,7 @@ test_that("Check if we are able to copy file content from a file to another", { content_before <- "TEST" writeLines(text = content_before, con = tmp_file) - integration_dir <- file.path(tmp_dir, "tests", "cypress", "integration") + integration_dir <- path(tmp_dir, "tests", "cypress", "integration") dir.create(integration_dir, showWarnings = FALSE, recursive = TRUE) files <- create_cypress_tests( project_path = tmp_dir, diff --git a/tests/testthat/test-utils_shinytest2.R b/tests/testthat/test-utils_shinytest2.R index 410bd3e..881306f 100644 --- a/tests/testthat/test-utils_shinytest2.R +++ b/tests/testthat/test-utils_shinytest2.R @@ -1,13 +1,13 @@ test_that("Check if we are able to move files properly", { tmp_dir <- tempdir() - tmp_dir1 <- file.path(tmp_dir, "folder1") + tmp_dir1 <- path(tmp_dir, "folder1") dir.create(tmp_dir1, showWarnings = FALSE) - tmp_dir2 <- file.path(tmp_dir, "folder2") + tmp_dir2 <- path(tmp_dir, "folder2") dir.create(tmp_dir2, showWarnings = FALSE) - shinytest2_dir <- file.path(tmp_dir1, "tst") - shinytest2_dir_copy <- file.path(tmp_dir2, "tst") + shinytest2_dir <- path(tmp_dir1, "tst") + shinytest2_dir_copy <- path(tmp_dir2, "tst") dir.create(path = shinytest2_dir, showWarnings = FALSE) move_shinytest2_tests(project_path = tmp_dir2, shinytest2_dir = shinytest2_dir) @@ -17,5 +17,5 @@ test_that("Check if we are able to move files properly", { test_that("Check if we are able to create shinytest2 structure", { tmp_dir <- create_shinytest2_structure(app_dir = ".") - expect_true(file.exists(file.path(tmp_dir, "app.R"))) + expect_true(file.exists(path(tmp_dir, "app.R"))) }) From cc25a6029be05ff01908d7cb556939d33845b4a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Fri, 25 Nov 2022 14:12:28 +0800 Subject: [PATCH 176/225] Shell command that was missed --- R/utils.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/R/utils.R b/R/utils.R index 954b9ec..0b51e5f 100644 --- a/R/utils.R +++ b/R/utils.R @@ -128,10 +128,9 @@ command_wrapper <- function(cmd, intern = FALSE, ...) { #' #' @keywords internal commit_exists <- function(commit) { - result <- shell( + result <- command_wrapper( cmd = glue::glue("git rev-parse --verify {commit}"), intern = FALSE, - mustWork = NA, wait = TRUE ) if (result == 128) { From 3f7b3c17bde4289f47e8f9a4d3cbec6d71273c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Fri, 25 Nov 2022 14:22:16 +0800 Subject: [PATCH 177/225] Missing NAMESPACE update --- NAMESPACE | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NAMESPACE b/NAMESPACE index b6b869e..df5b118 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -13,6 +13,8 @@ export(shiny_benchmark_class) exportClasses(shiny_benchmark) import(dplyr) import(ggplot2) +importFrom(fs,file_delete) +importFrom(fs,link_create) importFrom(fs,path) importFrom(glue,glue) importFrom(jsonlite,write_json) From 668df2466287f0802782368bcfe7f3618b52c0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Fri, 25 Nov 2022 07:41:37 +0100 Subject: [PATCH 178/225] Had the body of the if inverted --- R/utils.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/utils.R b/R/utils.R index 0b51e5f..c84676a 100644 --- a/R/utils.R +++ b/R/utils.R @@ -108,12 +108,12 @@ performance_test_cmd <- function(project_path) { #' #' @keywords internal command_wrapper <- function(cmd, intern = FALSE, ...) { - if (grepl("win", .Platform$OS.type, ignore.case = TRUE)) { + if (grepl("windows", .Platform$OS.type, ignore.case = TRUE)) { logger::log_debug("cmd (shell): {cmd}") - system(cmd, intern = intern, ...) + shell(cmd, intern = intern, ...) } else { logger::log_debug("cmd (system): {cmd}") - shell(cmd, intern = intern, ...) + system(cmd, intern = intern, ...) } } From 9ff157350e1ed230f28c2d33bea464ac011f5f15 Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Fri, 25 Nov 2022 12:00:34 +0100 Subject: [PATCH 179/225] docs: Update DESCRIPTION file. --- DESCRIPTION | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index e9932ef..8b9fbcb 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,15 +1,15 @@ Package: shiny.benchmark -Title: Compare performance of several versions of a shiny app +Title: Benchmark the Performance of Shiny Applications Version: 0.1.1 Authors@R: c( - person(given = "Douglas", family = "Azevedo", email = "douglas@appsilon.com", role = "aut"), - person("Developers", "Appsilon", email = "support+opensource@appsilon.com", role = "cre"), - person(family = "Appsilon Sp. z o.o.", role = "cph") + person(given = "Douglas", family = "Azevedo", email = "opensource+douglas@appsilon.com", role = c("aut", "cre")), + person(family = "Appsilon Sp. z o.o.", role = "cph", email = "opensource@appsilon.com") ) -Description: Compare performance of several versions of a shiny app based on commit hashes. +Description: Compare performance between different versions of a Shiny application based on git commit hashes. License: LGPL-3 -URL: https://github.com/Appsilon/shiny.benchmark +URL: https://github.com/Appsilon/shiny.benchmark, https://github.com/Appsilon/shiny.benchmark +BugReports: https://github.com/Appsilon/shiny.benchmark/issues SystemRequirements: yarn 1.22.17 or higher, Node 12 or higher Encoding: UTF-8 LazyData: true From 6cc5104d120545211192353f069fae96a36a2455 Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Fri, 25 Nov 2022 12:00:51 +0100 Subject: [PATCH 180/225] chore: Add docs to .Rbuildignore. --- .Rbuildignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.Rbuildignore b/.Rbuildignore index 1a759a0..6694056 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -4,3 +4,4 @@ .lintr tests/end2end pkgdown +docs From 119fe0d394f02091e2a8fbf7775f57238b2a088c Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Fri, 25 Nov 2022 12:37:35 +0100 Subject: [PATCH 181/225] docs: Update Description field. --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 8b9fbcb..7249d90 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -6,7 +6,7 @@ Authors@R: person(given = "Douglas", family = "Azevedo", email = "opensource+douglas@appsilon.com", role = c("aut", "cre")), person(family = "Appsilon Sp. z o.o.", role = "cph", email = "opensource@appsilon.com") ) -Description: Compare performance between different versions of a Shiny application based on git commit hashes. +Description: Compare performance between different versions of a Shiny application based on Git references. License: LGPL-3 URL: https://github.com/Appsilon/shiny.benchmark, https://github.com/Appsilon/shiny.benchmark BugReports: https://github.com/Appsilon/shiny.benchmark/issues From 91cbb4ee47b8c95120024498c2e755eb4a6eb105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Fri, 25 Nov 2022 15:11:21 +0100 Subject: [PATCH 182/225] Shell no longer necessary and is removed It's not available outside windows which might make it hard to use as `do.call` would be needed with some hacking. --- DESCRIPTION | 1 + R/benchmark_cypress.R | 4 ++-- R/utils.R | 11 +++-------- R/utils_cypress.R | 2 +- man/command_wrapper.Rd | 8 ++++---- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 63894bf..7015a61 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -34,6 +34,7 @@ Imports: shinytest2, stringr, testthat, + rlang, fs, logger Language: en-US diff --git a/R/benchmark_cypress.R b/R/benchmark_cypress.R index 0424b2d..a71b0b0 100644 --- a/R/benchmark_cypress.R +++ b/R/benchmark_cypress.R @@ -141,11 +141,11 @@ run_cypress_ptest <- function( colnames(perf_file[[i]]) <- c("date", "rep_id", "test_name", "duration_ms") # removing txt measures - unlink(x = txt_file) + file_delete(txt_file) } # removing js tests - unlink(x = js_file) + file_delete(js_file) # removing anything new in the github repo checkout_files(debug = debug) diff --git a/R/utils.R b/R/utils.R index c84676a..9d5134e 100644 --- a/R/utils.R +++ b/R/utils.R @@ -107,14 +107,9 @@ performance_test_cmd <- function(project_path) { #' @return see system or shell #' #' @keywords internal -command_wrapper <- function(cmd, intern = FALSE, ...) { - if (grepl("windows", .Platform$OS.type, ignore.case = TRUE)) { - logger::log_debug("cmd (shell): {cmd}") - shell(cmd, intern = intern, ...) - } else { - logger::log_debug("cmd (system): {cmd}") - system(cmd, intern = intern, ...) - } +command_wrapper <- function(cmd, ...) { + logger::log_debug("cmd (system): {cmd}") + system(command = cmd, ...) } #' @title Check if git commit hash exists diff --git a/R/utils_cypress.R b/R/utils_cypress.R index e4d0cae..5242781 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -31,7 +31,7 @@ create_cypress_structure <- function(app_dir, port, debug) { # create a path root linked to the main directory app tryCatch( expr = { - linked <- link_create(app_dir, root_path, symbolic = TRUE) + link_create(app_dir, root_path, symbolic = TRUE) }, error = function(e) { # If system cannot symlink then try to clone the repository diff --git a/man/command_wrapper.Rd b/man/command_wrapper.Rd index 7158d59..0564d07 100644 --- a/man/command_wrapper.Rd +++ b/man/command_wrapper.Rd @@ -4,18 +4,18 @@ \alias{command_wrapper} \title{Wrapper to call on Operating System commands} \usage{ -command_wrapper(cmd, intern = FALSE, ...) +command_wrapper(cmd, ...) } \arguments{ \item{cmd}{command} -\item{intern}{a logical (not NA) which indicates whether to capture -the output of the command as an R character vector.} - \item{...}{Other paramters passed to shell or system} \item{system}{a logical (not NA) which indicates whether to use shell or system call (system is a more low-level call)} + +\item{intern}{a logical (not NA) which indicates whether to capture +the output of the command as an R character vector.} } \value{ see system or shell From 1e75c359d77a37356168d26dc8ec704115ac7fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Fri, 25 Nov 2022 15:21:05 +0100 Subject: [PATCH 183/225] Typo on function param --- R/utils.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/utils.R b/R/utils.R index 9d5134e..721472b 100644 --- a/R/utils.R +++ b/R/utils.R @@ -102,7 +102,7 @@ performance_test_cmd <- function(project_path) { #' @param intern a logical (not NA) which indicates whether to capture #' the output of the command as an R character vector. #' -#' @param ... Other paramters passed to shell or system +#' @param ... Other parameters passed to shell or system #' #' @return see system or shell #' From fb9fb646d78c2b9046ec30669960cef932b43675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Fri, 25 Nov 2022 15:24:39 +0100 Subject: [PATCH 184/225] Missed the document() generation --- man/command_wrapper.Rd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/command_wrapper.Rd b/man/command_wrapper.Rd index 0564d07..d0a4815 100644 --- a/man/command_wrapper.Rd +++ b/man/command_wrapper.Rd @@ -9,7 +9,7 @@ command_wrapper(cmd, ...) \arguments{ \item{cmd}{command} -\item{...}{Other paramters passed to shell or system} +\item{...}{Other parameters passed to shell or system} \item{system}{a logical (not NA) which indicates whether to use shell or system call (system is a more low-level call)} From 4469f3517f6d02b7f1430f4f45a651d49c6d395e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Fri, 25 Nov 2022 15:42:05 +0100 Subject: [PATCH 185/225] Test to see if it's on the wrong directory EBUSY error on shinytest2 --- R/benchmark_shinytest2.R | 3 +++ 1 file changed, 3 insertions(+) diff --git a/R/benchmark_shinytest2.R b/R/benchmark_shinytest2.R index 3e64eb1..7f7e4cc 100644 --- a/R/benchmark_shinytest2.R +++ b/R/benchmark_shinytest2.R @@ -62,6 +62,9 @@ benchmark_shinytest2 <- function( restore_env(branch = current_branch, renv_prompt = renv_prompt) # Cleaning the temporary directory + message(getwd()) + message(system('pwd')) + message(system('dir')) file_delete(path(project_path, "tests")) } ) From cd752fd7d7d9b511d01f2b18bc96c8076e319bb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Fri, 25 Nov 2022 16:02:47 +0100 Subject: [PATCH 186/225] Testing github machines --- R/benchmark_shinytest2.R | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/R/benchmark_shinytest2.R b/R/benchmark_shinytest2.R index 7f7e4cc..33328b4 100644 --- a/R/benchmark_shinytest2.R +++ b/R/benchmark_shinytest2.R @@ -26,12 +26,24 @@ benchmark_shinytest2 <- function( n_rep, debug ) { + message("Andre start: Temp messages to check the pwd dir") + message("getwd(): ", getwd()) + message("pwd: ", system('pwd')) + message("dir: ", dir()) + # creating the structure project_path <- create_shinytest2_structure(app_dir = app_dir) # getting the current branch current_branch <- get_commit_hash() + message("Andre After Create: Temp messages to check the pwd dir") + message("project_path: ", project_path) + message("getwd(): ", getwd()) + message("pwd: ", system('pwd')) + message("dir: ", dir()) + message("dir(project_path): ", dir(project_path)) + # apply the tests for each branch/commit perf_list <- tryCatch( expr = { @@ -62,9 +74,13 @@ benchmark_shinytest2 <- function( restore_env(branch = current_branch, renv_prompt = renv_prompt) # Cleaning the temporary directory - message(getwd()) - message(system('pwd')) - message(system('dir')) + message("Andre Finally: Temp messages to check the pwd dir") + message("project_path: ", project_path) + message("getwd(): ", getwd()) + message("pwd: ", system('pwd')) + message("dir: ", dir()) + message("dir(project_path): ", dir(project_path)) + system(glue("cd {path('..', '..')}")) file_delete(path(project_path, "tests")) } ) From 960ae36cdadd0390e50840ebf1d7a85b89096e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Fri, 25 Nov 2022 16:05:26 +0100 Subject: [PATCH 187/225] Corrects lint problems --- R/benchmark_shinytest2.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/benchmark_shinytest2.R b/R/benchmark_shinytest2.R index 33328b4..4c19f2b 100644 --- a/R/benchmark_shinytest2.R +++ b/R/benchmark_shinytest2.R @@ -28,7 +28,7 @@ benchmark_shinytest2 <- function( ) { message("Andre start: Temp messages to check the pwd dir") message("getwd(): ", getwd()) - message("pwd: ", system('pwd')) + message("pwd: ", system("pwd")) message("dir: ", dir()) # creating the structure @@ -40,7 +40,7 @@ benchmark_shinytest2 <- function( message("Andre After Create: Temp messages to check the pwd dir") message("project_path: ", project_path) message("getwd(): ", getwd()) - message("pwd: ", system('pwd')) + message("pwd: ", system("pwd")) message("dir: ", dir()) message("dir(project_path): ", dir(project_path)) From 88fee3ed129cd5976e06c4e310bf78fa27c99627 Mon Sep 17 00:00:00 2001 From: Andre Verissimo Date: Fri, 25 Nov 2022 16:51:10 +0100 Subject: [PATCH 188/225] revert to unlink (fails silently) --- R/benchmark_shinytest2.R | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/R/benchmark_shinytest2.R b/R/benchmark_shinytest2.R index 4c19f2b..77647bb 100644 --- a/R/benchmark_shinytest2.R +++ b/R/benchmark_shinytest2.R @@ -26,10 +26,6 @@ benchmark_shinytest2 <- function( n_rep, debug ) { - message("Andre start: Temp messages to check the pwd dir") - message("getwd(): ", getwd()) - message("pwd: ", system("pwd")) - message("dir: ", dir()) # creating the structure project_path <- create_shinytest2_structure(app_dir = app_dir) @@ -37,13 +33,6 @@ benchmark_shinytest2 <- function( # getting the current branch current_branch <- get_commit_hash() - message("Andre After Create: Temp messages to check the pwd dir") - message("project_path: ", project_path) - message("getwd(): ", getwd()) - message("pwd: ", system("pwd")) - message("dir: ", dir()) - message("dir(project_path): ", dir(project_path)) - # apply the tests for each branch/commit perf_list <- tryCatch( expr = { @@ -74,14 +63,7 @@ benchmark_shinytest2 <- function( restore_env(branch = current_branch, renv_prompt = renv_prompt) # Cleaning the temporary directory - message("Andre Finally: Temp messages to check the pwd dir") - message("project_path: ", project_path) - message("getwd(): ", getwd()) - message("pwd: ", system('pwd')) - message("dir: ", dir()) - message("dir(project_path): ", dir(project_path)) - system(glue("cd {path('..', '..')}")) - file_delete(path(project_path, "tests")) + unlink(path(project_path, "tests")) } ) From d27460eb3a32a117017b14d578de968c4a7a1e7c Mon Sep 17 00:00:00 2001 From: Andre Verissimo Date: Fri, 25 Nov 2022 17:17:43 +0100 Subject: [PATCH 189/225] Use of fs library instead of base Cross platform and heavily tested --- NAMESPACE | 3 --- R/benchmark_cypress.R | 10 ++++------ R/utils.R | 8 ++++++-- R/utils_cypress.R | 17 ++++++++--------- R/utils_shinytest2.R | 7 ++++++- tests/testthat/test-utils_cypress.R | 4 ++-- tests/testthat/test-utils_shinytest2.R | 10 +++++----- 7 files changed, 31 insertions(+), 28 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index df5b118..a51ca51 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -13,9 +13,6 @@ export(shiny_benchmark_class) exportClasses(shiny_benchmark) import(dplyr) import(ggplot2) -importFrom(fs,file_delete) -importFrom(fs,link_create) -importFrom(fs,path) importFrom(glue,glue) importFrom(jsonlite,write_json) importFrom(methods,new) diff --git a/R/benchmark_cypress.R b/R/benchmark_cypress.R index a71b0b0..8364a43 100644 --- a/R/benchmark_cypress.R +++ b/R/benchmark_cypress.R @@ -16,8 +16,6 @@ #' @param n_rep Number of replications desired #' @param debug Logical. TRUE to display all the system messages on runtime #' -#' @importFrom fs path link_create file_delete -#' #' @export benchmark_cypress <- function( commit_list, @@ -70,8 +68,8 @@ benchmark_cypress <- function( restore_env(branch = current_branch, renv_prompt = renv_prompt) # Cleaning the temporary directory - file_delete(path(project_path, "node")) - file_delete(path(project_path, "tests")) + fs::file_delete(path(project_path, "node")) + fs::file_delete(path(project_path, "tests")) } ) @@ -141,11 +139,11 @@ run_cypress_ptest <- function( colnames(perf_file[[i]]) <- c("date", "rep_id", "test_name", "duration_ms") # removing txt measures - file_delete(txt_file) + fs::file_delete(txt_file) } # removing js tests - file_delete(js_file) + fs::file_delete(js_file) # removing anything new in the github repo checkout_files(debug = debug) diff --git a/R/utils.R b/R/utils.R index 721472b..cbf7f1e 100644 --- a/R/utils.R +++ b/R/utils.R @@ -232,7 +232,7 @@ summarise_commit <- function(object) { #' @export load_example <- function(path) { # see if path exists - if (!file.exists(path)) + if (!fs::file_exists(path)) stop("You must provide a valid path") if (length(list.files(path))) { @@ -253,7 +253,11 @@ load_example <- function(path) { files <- list.files(path = ex_path, full.names = TRUE) for (file in files) { - file.copy(from = file, to = path, recursive = TRUE) + if (fs::is_dir(file)) { + fs::dir_copy(file, path) + } else { + fs::file_copy(file, path) + } print(glue("{basename(file)} created at {path}")) } diff --git a/R/utils_cypress.R b/R/utils_cypress.R index 5242781..9f9ae0c 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -22,16 +22,16 @@ create_cypress_structure <- function(app_dir, port, debug) { plugins_path <- path(cypress_path, "plugins") # creating paths - dir.create(path = node_path, showWarnings = FALSE) - dir.create(path = tests_path, showWarnings = FALSE) - dir.create(path = cypress_path, showWarnings = FALSE) - dir.create(path = integration_path, showWarnings = FALSE) - dir.create(path = plugins_path, showWarnings = FALSE) + fs::dir_create(path = node_path) + fs::dir_create(path = tests_path) + fs::dir_create(path = cypress_path) + fs::dir_create(path = integration_path) + fs::dir_create(path = plugins_path) # create a path root linked to the main directory app tryCatch( expr = { - link_create(app_dir, root_path, symbolic = TRUE) + fs::link_create(app_dir, root_path, symbolic = TRUE) }, error = function(e) { # If system cannot symlink then try to clone the repository @@ -85,7 +85,6 @@ create_node_list <- function(tests_path, port) { ), "run-app" = glue( "cd root && ", - "Rscript -e \"installed.packages()[,1] |> sort()\" && ", "Rscript -e \"shiny::runApp(port = {port})\"" ), "run-cypress" = glue("cypress run --project {tests_path}") @@ -173,8 +172,8 @@ create_cypress_tests <- function(project_path, cypress_dir, tests_pattern) { # file to store the times txt_file <- path(project_path, "tests", "cypress", "performance.txt") - if (!file.exists(txt_file)) { - file.create(txt_file) # touch file if it doesn't exist + if (!fs::file_exists(txt_file)) { + fs::file_create(txt_file) # touch file if it doesn't exist } add_sendtime2js(js_file = js_file, txt_file = txt_file) diff --git a/R/utils_shinytest2.R b/R/utils_shinytest2.R index 1cb3aa6..ddb2834 100644 --- a/R/utils_shinytest2.R +++ b/R/utils_shinytest2.R @@ -29,7 +29,12 @@ create_shinytest2_structure <- function(app_dir) { #' @keywords internal move_shinytest2_tests <- function(project_path, shinytest2_dir) { # copy everything to the temporary directory - file.copy(from = shinytest2_dir, to = project_path, recursive = TRUE) + if (fs::is_dir(shinytest2_dir)) { + fs::dir_copy(path = shinytest2_dir, new_path = project_path) + } else { + # should never reach this + fs::file_copy(path = shinytest2_dir, new_path = project_path) + } tests_dir <- path(project_path, "tests") return(tests_dir) diff --git a/tests/testthat/test-utils_cypress.R b/tests/testthat/test-utils_cypress.R index ced7fc0..b488675 100644 --- a/tests/testthat/test-utils_cypress.R +++ b/tests/testthat/test-utils_cypress.R @@ -5,7 +5,7 @@ test_that("Check if we are able to add Cypress code to a txt file", { txt_file = "test.txt" ) - expect_true(file.exists(path(tmp_dir, "test.js"))) + expect_true(fs::file_exists(path(tmp_dir, "test.js"))) }) test_that("Check if we are able to copy file content from a file to another", { @@ -15,7 +15,7 @@ test_that("Check if we are able to copy file content from a file to another", { writeLines(text = content_before, con = tmp_file) integration_dir <- path(tmp_dir, "tests", "cypress", "integration") - dir.create(integration_dir, showWarnings = FALSE, recursive = TRUE) + fs::dir_create(path = integration_dir) files <- create_cypress_tests( project_path = tmp_dir, cypress_dir = tmp_dir, diff --git a/tests/testthat/test-utils_shinytest2.R b/tests/testthat/test-utils_shinytest2.R index 881306f..aaee10e 100644 --- a/tests/testthat/test-utils_shinytest2.R +++ b/tests/testthat/test-utils_shinytest2.R @@ -2,20 +2,20 @@ test_that("Check if we are able to move files properly", { tmp_dir <- tempdir() tmp_dir1 <- path(tmp_dir, "folder1") - dir.create(tmp_dir1, showWarnings = FALSE) + fs::dir_create(path = tmp_dir1) tmp_dir2 <- path(tmp_dir, "folder2") - dir.create(tmp_dir2, showWarnings = FALSE) + fs::dir_create(path = tmp_dir2) shinytest2_dir <- path(tmp_dir1, "tst") shinytest2_dir_copy <- path(tmp_dir2, "tst") - dir.create(path = shinytest2_dir, showWarnings = FALSE) + fs::dir_create(path = shinytest2_dir) move_shinytest2_tests(project_path = tmp_dir2, shinytest2_dir = shinytest2_dir) - expect_true(file.exists(shinytest2_dir_copy)) + expect_true(fs::file_exists(shinytest2_dir_copy)) }) test_that("Check if we are able to create shinytest2 structure", { tmp_dir <- create_shinytest2_structure(app_dir = ".") - expect_true(file.exists(path(tmp_dir, "app.R"))) + expect_true(fs::file_exists(path(tmp_dir, "app.R"))) }) From 696dcd4ddf294dd64d37a9926965dd82ff85c76e Mon Sep 17 00:00:00 2001 From: Andre Verissimo Date: Fri, 25 Nov 2022 17:19:29 +0100 Subject: [PATCH 190/225] cleanup debugging --- R/benchmark_cypress.R | 1 - R/globals.R | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/R/benchmark_cypress.R b/R/benchmark_cypress.R index 8364a43..1e8d6a1 100644 --- a/R/benchmark_cypress.R +++ b/R/benchmark_cypress.R @@ -130,7 +130,6 @@ run_cypress_ptest <- function( # run tests there command <- performance_test_cmd(project_path) - debug <- TRUE command_wrapper(command, ignore.stdout = !debug, ignore.stderr = !debug) # read the file saved by cypress diff --git a/R/globals.R b/R/globals.R index 33b5afc..b5709d2 100644 --- a/R/globals.R +++ b/R/globals.R @@ -14,4 +14,4 @@ utils::globalVariables( ) # Setting threshold to debug (temporary and should be removed) -logger::log_threshold(logger::DEBUG) +logger::log_threshold(logger::INFO) From cc7b22fb2a184c6ce92294d54f0d5d7d9655ed21 Mon Sep 17 00:00:00 2001 From: Andre Verissimo Date: Fri, 25 Nov 2022 17:52:23 +0100 Subject: [PATCH 191/225] Didn't update path to fs::path after removing importFrom --- R/benchmark_cypress.R | 4 ++-- R/benchmark_shinytest2.R | 2 +- R/utils.R | 4 ++-- R/utils_cypress.R | 22 +++++++++++----------- R/utils_shinytest2.R | 4 ++-- tests/testthat/test-utils_cypress.R | 6 +++--- tests/testthat/test-utils_shinytest2.R | 10 +++++----- 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/R/benchmark_cypress.R b/R/benchmark_cypress.R index 1e8d6a1..d412581 100644 --- a/R/benchmark_cypress.R +++ b/R/benchmark_cypress.R @@ -68,8 +68,8 @@ benchmark_cypress <- function( restore_env(branch = current_branch, renv_prompt = renv_prompt) # Cleaning the temporary directory - fs::file_delete(path(project_path, "node")) - fs::file_delete(path(project_path, "tests")) + fs::file_delete(fs::path(project_path, "node")) + fs::file_delete(fs::path(project_path, "tests")) } ) diff --git a/R/benchmark_shinytest2.R b/R/benchmark_shinytest2.R index 77647bb..aed7926 100644 --- a/R/benchmark_shinytest2.R +++ b/R/benchmark_shinytest2.R @@ -63,7 +63,7 @@ benchmark_shinytest2 <- function( restore_env(branch = current_branch, renv_prompt = renv_prompt) # Cleaning the temporary directory - unlink(path(project_path, "tests")) + unlink(fs::path(project_path, "tests")) } ) diff --git a/R/utils.R b/R/utils.R index cbf7f1e..bf4a8e8 100644 --- a/R/utils.R +++ b/R/utils.R @@ -88,7 +88,7 @@ checkout <- function(branch, debug) { #' @keywords internal performance_test_cmd <- function(project_path) { if (grepl("win", .Platform$OS.type, ignore.case = TRUE)) { - glue("yarn --cwd \"{path(project_path, 'node')}\" performance-test") + glue("yarn --cwd \"{fs::path(project_path, 'node')}\" performance-test") } else { glue("cd {project_path}; set -eu; exec yarn --cwd node performance-test") } @@ -261,6 +261,6 @@ load_example <- function(path) { print(glue("{basename(file)} created at {path}")) } - fpath <- path(path, "run_tests.R") # nolint + fpath <- fs::path(path, "run_tests.R") # nolint message(glue("Follow instructions in {fpath}")) } diff --git a/R/utils_cypress.R b/R/utils_cypress.R index 9f9ae0c..700e99a 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -12,14 +12,14 @@ create_cypress_structure <- function(app_dir, port, debug) { dir_tests <- tempdir() # node path - node_path <- path(dir_tests, "node") - root_path <- path(node_path, "root") # nolint + node_path <- fs::path(dir_tests, "node") + root_path <- fs::path(node_path, "root") # nolint # test path - tests_path <- path(dir_tests, "tests") - cypress_path <- path(tests_path, "cypress") - integration_path <- path(cypress_path, "integration") - plugins_path <- path(cypress_path, "plugins") + tests_path <- fs::path(dir_tests, "tests") + cypress_path <- fs::path(tests_path, "cypress") + integration_path <- fs::path(cypress_path, "integration") + plugins_path <- fs::path(cypress_path, "plugins") # creating paths fs::dir_create(path = node_path) @@ -48,7 +48,7 @@ create_cypress_structure <- function(app_dir, port, debug) { # create the packages.json file json_txt <- create_node_list(tests_path = tests_path, port = port) - json_file <- path(node_path, "package.json") + json_file <- fs::path(node_path, "package.json") write_json(x = json_txt, path = json_file, pretty = TRUE, auto_unbox = TRUE) # install everything that is needed @@ -57,12 +57,12 @@ create_cypress_structure <- function(app_dir, port, debug) { # creating cypress plugin file js_txt <- create_cypress_plugins() - js_file <- path(plugins_path, "index.js") + js_file <- fs::path(plugins_path, "index.js") writeLines(text = js_txt, con = js_file) # creating cypress.json json_txt <- create_cypress_list(plugins_file = js_file, port = port) - json_file <- path(tests_path, "cypress.json") + json_file <- fs::path(tests_path, "cypress.json") write_json(x = json_txt, path = json_file, pretty = TRUE, auto_unbox = TRUE) # returning the project folder @@ -156,7 +156,7 @@ create_cypress_tests <- function(project_path, cypress_dir, tests_pattern) { cypress_files <- grep(x = cypress_files, pattern = "\\.js$", value = TRUE) # creating a copy to be able to edit the js file - js_file <- path( + js_file <- fs::path( project_path, "tests", "cypress", @@ -171,7 +171,7 @@ create_cypress_tests <- function(project_path, cypress_dir, tests_pattern) { } # file to store the times - txt_file <- path(project_path, "tests", "cypress", "performance.txt") + txt_file <- fs::path(project_path, "tests", "cypress", "performance.txt") if (!fs::file_exists(txt_file)) { fs::file_create(txt_file) # touch file if it doesn't exist } diff --git a/R/utils_shinytest2.R b/R/utils_shinytest2.R index ddb2834..8584e8b 100644 --- a/R/utils_shinytest2.R +++ b/R/utils_shinytest2.R @@ -12,7 +12,7 @@ create_shinytest2_structure <- function(app_dir) { # shiny call writeLines( text = glue('shiny::runApp(appDir = "{app_dir}")'), - con = path(dir_tests, "app.R") + con = fs::path(dir_tests, "app.R") ) # returning the project folder @@ -35,7 +35,7 @@ move_shinytest2_tests <- function(project_path, shinytest2_dir) { # should never reach this fs::file_copy(path = shinytest2_dir, new_path = project_path) } - tests_dir <- path(project_path, "tests") + tests_dir <- fs::path(project_path, "tests") return(tests_dir) } diff --git a/tests/testthat/test-utils_cypress.R b/tests/testthat/test-utils_cypress.R index b488675..b24d12d 100644 --- a/tests/testthat/test-utils_cypress.R +++ b/tests/testthat/test-utils_cypress.R @@ -1,11 +1,11 @@ test_that("Check if we are able to add Cypress code to a txt file", { tmp_dir <- tempdir() add_sendtime2js( - js_file = path(tmp_dir, "test.js"), + js_file = fs::path(tmp_dir, "test.js"), txt_file = "test.txt" ) - expect_true(fs::file_exists(path(tmp_dir, "test.js"))) + expect_true(fs::file_exists(fs::path(tmp_dir, "test.js"))) }) test_that("Check if we are able to copy file content from a file to another", { @@ -14,7 +14,7 @@ test_that("Check if we are able to copy file content from a file to another", { content_before <- "TEST" writeLines(text = content_before, con = tmp_file) - integration_dir <- path(tmp_dir, "tests", "cypress", "integration") + integration_dir <- fs::path(tmp_dir, "tests", "cypress", "integration") fs::dir_create(path = integration_dir) files <- create_cypress_tests( project_path = tmp_dir, diff --git a/tests/testthat/test-utils_shinytest2.R b/tests/testthat/test-utils_shinytest2.R index aaee10e..5dbc08b 100644 --- a/tests/testthat/test-utils_shinytest2.R +++ b/tests/testthat/test-utils_shinytest2.R @@ -1,13 +1,13 @@ test_that("Check if we are able to move files properly", { tmp_dir <- tempdir() - tmp_dir1 <- path(tmp_dir, "folder1") + tmp_dir1 <- fs::path(tmp_dir, "folder1") fs::dir_create(path = tmp_dir1) - tmp_dir2 <- path(tmp_dir, "folder2") + tmp_dir2 <- fs::path(tmp_dir, "folder2") fs::dir_create(path = tmp_dir2) - shinytest2_dir <- path(tmp_dir1, "tst") - shinytest2_dir_copy <- path(tmp_dir2, "tst") + shinytest2_dir <- fs::path(tmp_dir1, "tst") + shinytest2_dir_copy <- fs::path(tmp_dir2, "tst") fs::dir_create(path = shinytest2_dir) move_shinytest2_tests(project_path = tmp_dir2, shinytest2_dir = shinytest2_dir) @@ -17,5 +17,5 @@ test_that("Check if we are able to move files properly", { test_that("Check if we are able to create shinytest2 structure", { tmp_dir <- create_shinytest2_structure(app_dir = ".") - expect_true(fs::file_exists(path(tmp_dir, "app.R"))) + expect_true(fs::file_exists(fs::path(tmp_dir, "app.R"))) }) From 1bdb8827c81c0854ecd541c1730abbd0291b8d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Fri, 25 Nov 2022 23:34:14 +0100 Subject: [PATCH 192/225] overwrite on copy, to keep behaviour of file.copy --- R/utils.R | 4 ++-- R/utils_shinytest2.R | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/R/utils.R b/R/utils.R index bf4a8e8..8054d97 100644 --- a/R/utils.R +++ b/R/utils.R @@ -254,9 +254,9 @@ load_example <- function(path) { for (file in files) { if (fs::is_dir(file)) { - fs::dir_copy(file, path) + fs::dir_copy(file, path, overwrite = TRUE) } else { - fs::file_copy(file, path) + fs::file_copy(file, path, overwrite = TRUE) } print(glue("{basename(file)} created at {path}")) } diff --git a/R/utils_shinytest2.R b/R/utils_shinytest2.R index 8584e8b..a438207 100644 --- a/R/utils_shinytest2.R +++ b/R/utils_shinytest2.R @@ -30,10 +30,18 @@ create_shinytest2_structure <- function(app_dir) { move_shinytest2_tests <- function(project_path, shinytest2_dir) { # copy everything to the temporary directory if (fs::is_dir(shinytest2_dir)) { - fs::dir_copy(path = shinytest2_dir, new_path = project_path) + fs::dir_copy( + path = shinytest2_dir, + new_path = project_path, + overwrite = TRUE + ) } else { # should never reach this - fs::file_copy(path = shinytest2_dir, new_path = project_path) + fs::file_copy( + path = shinytest2_dir, + new_path = project_path, + overwrite = TRUE + ) } tests_dir <- fs::path(project_path, "tests") From 153f31f21a591c4d3acf303b2a9dd3992b5594e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Mon, 28 Nov 2022 11:49:10 +0100 Subject: [PATCH 193/225] corrects dir_copy bug, replaces system, adds tests - fs::dir_copy(..., overwrite = TRUE) has a slightly different behavior from base::file.copy - uses command_call instead of system, to prepare for future where shell call might be needed on windows - adds tests to load_example - reverts some unnecessary changes --- R/benchmark_cypress.R | 3 +- R/benchmark_shinytest2.R | 5 ++- R/utils.R | 36 +++++++------------- R/utils_cypress.R | 10 +++--- R/utils_shinytest2.R | 6 ++-- man/command_wrapper.Rd | 26 -------------- man/load_example.Rd | 4 ++- tests/testthat/test-test-load-example.R | 40 ++++++++++++++++++++++ tests/testthat/test-utils_shinytest2.R | 45 ++++++++++++++++++++----- 9 files changed, 105 insertions(+), 70 deletions(-) delete mode 100644 man/command_wrapper.Rd create mode 100644 tests/testthat/test-test-load-example.R diff --git a/R/benchmark_cypress.R b/R/benchmark_cypress.R index d412581..2e86144 100644 --- a/R/benchmark_cypress.R +++ b/R/benchmark_cypress.R @@ -55,7 +55,6 @@ benchmark_cypress <- function( ) }, error = function(e) { - logger::log_fatal("ERROR!!! {e}") message(e) }, finally = { @@ -130,7 +129,7 @@ run_cypress_ptest <- function( # run tests there command <- performance_test_cmd(project_path) - command_wrapper(command, ignore.stdout = !debug, ignore.stderr = !debug) + system(command, ignore.stdout = !debug, ignore.stderr = !debug) # read the file saved by cypress perf_file[[i]] <- read.table(file = txt_file, header = FALSE, sep = ";") diff --git a/R/benchmark_shinytest2.R b/R/benchmark_shinytest2.R index aed7926..8e62086 100644 --- a/R/benchmark_shinytest2.R +++ b/R/benchmark_shinytest2.R @@ -106,7 +106,10 @@ run_shinytest2_ptest <- function( if (use_renv) restore_env(branch = commit, renv_prompt = renv_prompt) # move test files to the project folder - tests_dir <- move_shinytest2_tests(project_path = project_path, shinytest2_dir = shinytest2_dir) + tests_dir <- move_shinytest2_tests( + project_path = project_path, + shinytest2_dir = shinytest2_dir + ) perf_file <- list() pb <- create_progress_bar(total = n_rep) diff --git a/R/utils.R b/R/utils.R index 8054d97..a38302a 100644 --- a/R/utils.R +++ b/R/utils.R @@ -94,24 +94,6 @@ performance_test_cmd <- function(project_path) { } } -#' @title Wrapper to call on Operating System commands -#' -#' @param cmd command -#' @param system a logical (not NA) which indicates whether to use -#' shell or system call (system is a more low-level call) -#' @param intern a logical (not NA) which indicates whether to capture -#' the output of the command as an R character vector. -#' -#' @param ... Other parameters passed to shell or system -#' -#' @return see system or shell -#' -#' @keywords internal -command_wrapper <- function(cmd, ...) { - logger::log_debug("cmd (system): {cmd}") - system(command = cmd, ...) -} - #' @title Check if git commit hash exists #' #' @description Can be anything git recognizes as a commit, such @@ -123,8 +105,8 @@ command_wrapper <- function(cmd, ...) { #' #' @keywords internal commit_exists <- function(commit) { - result <- command_wrapper( - cmd = glue::glue("git rev-parse --verify {commit}"), + result <- system( + command = glue::glue("git rev-parse --verify {commit}"), intern = FALSE, wait = TRUE ) @@ -226,16 +208,17 @@ summarise_commit <- function(object) { #' the selected `path`. #' #' @param path A character vector of full path name +#' @param force Create example even if directory is not empty #' #' @importFrom glue glue #' @importFrom utils menu #' @export -load_example <- function(path) { +load_example <- function(path, force = FALSE) { # see if path exists if (!fs::file_exists(path)) stop("You must provide a valid path") - if (length(list.files(path))) { + if (!force && length(list.files(path))) { choice <- menu( choices = c("Yes", "No"), title = glue("{path} seems to not be empty. Would you like to proceed?") @@ -243,6 +226,11 @@ load_example <- function(path) { if (choice == 2) stop("Process aborted by user. Consider creating a new empty path.") + } else if (length(list.files(path))) { + message(glue( + "{path} seems to not be empty. ", + "Continuing as parameter 'force' is TRUE" + )) } ex_path <- system.file( @@ -254,7 +242,9 @@ load_example <- function(path) { for (file in files) { if (fs::is_dir(file)) { - fs::dir_copy(file, path, overwrite = TRUE) + # Due to overwrite = TRUE the destination must include the name of the + # directory to be created + fs::dir_copy(file, fs::path(path, fs::path_file(file)), overwrite = TRUE) } else { fs::file_copy(file, path, overwrite = TRUE) } diff --git a/R/utils_cypress.R b/R/utils_cypress.R index 700e99a..6fef334 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -41,9 +41,9 @@ create_cypress_structure <- function(app_dir, port, debug) { "Could not create symbolic link with fs package, ", "trying with git clone..." ) - command_wrapper(glue::glue("git clone \"{app_dir}\" \"{root_path}\"")) - command_wrapper("git submodule init") - command_wrapper("git submodule update ") + system(glue::glue("git clone \"{app_dir}\" \"{root_path}\"")) + system("git submodule init") + system("git submodule update ") }) # create the packages.json file @@ -172,9 +172,7 @@ create_cypress_tests <- function(project_path, cypress_dir, tests_pattern) { # file to store the times txt_file <- fs::path(project_path, "tests", "cypress", "performance.txt") - if (!fs::file_exists(txt_file)) { - fs::file_create(txt_file) # touch file if it doesn't exist - } + add_sendtime2js(js_file = js_file, txt_file = txt_file) # returning the file location diff --git a/R/utils_shinytest2.R b/R/utils_shinytest2.R index a438207..f73c4e7 100644 --- a/R/utils_shinytest2.R +++ b/R/utils_shinytest2.R @@ -28,22 +28,22 @@ create_shinytest2_structure <- function(app_dir) { #' #' @keywords internal move_shinytest2_tests <- function(project_path, shinytest2_dir) { + tests_dir <- fs::path(project_path, "tests") # copy everything to the temporary directory if (fs::is_dir(shinytest2_dir)) { fs::dir_copy( path = shinytest2_dir, - new_path = project_path, + new_path = tests_dir, overwrite = TRUE ) } else { # should never reach this fs::file_copy( path = shinytest2_dir, - new_path = project_path, + new_path = tests_dir, overwrite = TRUE ) } - tests_dir <- fs::path(project_path, "tests") return(tests_dir) } diff --git a/man/command_wrapper.Rd b/man/command_wrapper.Rd deleted file mode 100644 index d0a4815..0000000 --- a/man/command_wrapper.Rd +++ /dev/null @@ -1,26 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils.R -\name{command_wrapper} -\alias{command_wrapper} -\title{Wrapper to call on Operating System commands} -\usage{ -command_wrapper(cmd, ...) -} -\arguments{ -\item{cmd}{command} - -\item{...}{Other parameters passed to shell or system} - -\item{system}{a logical (not NA) which indicates whether to use -shell or system call (system is a more low-level call)} - -\item{intern}{a logical (not NA) which indicates whether to capture -the output of the command as an R character vector.} -} -\value{ -see system or shell -} -\description{ -Wrapper to call on Operating System commands -} -\keyword{internal} diff --git a/man/load_example.Rd b/man/load_example.Rd index 912189f..63d28ab 100644 --- a/man/load_example.Rd +++ b/man/load_example.Rd @@ -4,10 +4,12 @@ \alias{load_example} \title{Load an application and instructions to run shiny.benchmark} \usage{ -load_example(path) +load_example(path, force = FALSE) } \arguments{ \item{path}{A character vector of full path name} + +\item{force}{Create example even if directory is not empty} } \description{ This function aims to generate a template to be used diff --git a/tests/testthat/test-test-load-example.R b/tests/testthat/test-test-load-example.R new file mode 100644 index 0000000..ad12052 --- /dev/null +++ b/tests/testthat/test-test-load-example.R @@ -0,0 +1,40 @@ +# Necessary test as fs::dir_copy with overwrite has a different behavior +# than file.copy. +# It copies the content of the directory of the "from" path into the +# destination, instead of the directory itself +test_that("Load example creates correct structure", { + example_path <- fs::path(tempdir(), "load_example") + fs::dir_create(example_path) + load_example(example_path, force = TRUE) + + files <- example_path |> + fs::path( + c( + "run_tests.R", + fs::path("app", "ui.R"), + fs::path("app", "server.R"), + fs::path("app", "global.R"), + fs::path("app", "tests", "testthat.R"), + fs::path("app", "tests", "testthat", "setup.R"), + fs::path("app", "tests", "testthat", "test-use_this_one_1.R"), + fs::path("app", "tests", "testthat", "test-use_this_one_2.R") + ) + ) + + dirs <- example_path |> + fs::path( + c( + fs::path("app", "tests"), + fs::path("app", "tests", "cypress"), + fs::path("app", "tests", "testthat") + ) + ) + + expect_true(all(fs::file_exists(files))) + expect_true(all(fs::is_file(files))) + expect_false(all(fs::is_dir(files))) + + expect_true(all(fs::dir_exists(dirs))) + expect_true(all(fs::is_dir(dirs))) + expect_false(all(fs::is_file(dirs))) +}) diff --git a/tests/testthat/test-utils_shinytest2.R b/tests/testthat/test-utils_shinytest2.R index 5dbc08b..42558ac 100644 --- a/tests/testthat/test-utils_shinytest2.R +++ b/tests/testthat/test-utils_shinytest2.R @@ -1,18 +1,47 @@ +library(fs) + test_that("Check if we are able to move files properly", { tmp_dir <- tempdir() - tmp_dir1 <- fs::path(tmp_dir, "folder1") - fs::dir_create(path = tmp_dir1) - tmp_dir2 <- fs::path(tmp_dir, "folder2") - fs::dir_create(path = tmp_dir2) + # Create the folder that contains the tests, it's named tst + # on purpuse as it won't be a valid directory and shinytests2 would + # fail + tmp_dir1 <- file.path(tmp_dir, "folder1") + dir.create(path = tmp_dir1, recursive = TRUE) shinytest2_dir <- fs::path(tmp_dir1, "tst") - shinytest2_dir_copy <- fs::path(tmp_dir2, "tst") - fs::dir_create(path = shinytest2_dir) + dir.create(shinytest2_dir, showWarnings = FALSE) + file.create(fs::path(shinytest2_dir, "some_example.txt")) + file.create(fs::path(shinytest2_dir, "some_example2.txt")) + file.create(fs::path(shinytest2_dir, "some_example3.txt")) + + # Create a mock project path base directory + tmp_dir2 <- fs::path(tmp_dir, "folder2") + dir.create(tmp_dir2, recursive = TRUE) + project_path <- tmp_dir2 + dir.create(fs::path(tmp_dir2, "something")) + file.create(fs::path(shinytest2_dir, "root.txt")) + + # The result of the copy should land on tests folder + shinytest2_dir_copy_manual <- file.path(tmp_dir2, "tests") + + # [ACTION] Actual copy of the shinytest2 files + shinytest2_dir_copy_auto <- move_shinytest2_tests( + project_path = tmp_dir2, + shinytest2_dir = shinytest2_dir + ) - move_shinytest2_tests(project_path = tmp_dir2, shinytest2_dir = shinytest2_dir) + # Test if the copy was successful + expect_true(dir.exists(shinytest2_dir_copy_manual)) + expect_true(dir.exists(shinytest2_dir_copy_auto)) + expect_equal(shinytest2_dir_copy_auto, shinytest2_dir_copy_manual) - expect_true(fs::file_exists(shinytest2_dir_copy)) + short_var <- shinytest2_dir_copy_auto + str_mask <- "some_example{input}.txt" + c("", 2, 3) |> + sapply(function(input) { + expect_true(file.exists(fs::path(short_var, glue(str_mask)))) + }) }) test_that("Check if we are able to create shinytest2 structure", { From b2fe148642abcb0fb15f528f57fac0300ae88213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Mon, 28 Nov 2022 14:34:47 +0100 Subject: [PATCH 194/225] revert load_example changes --- R/utils.R | 19 +++--------- tests/testthat/test-test-load-example.R | 40 ------------------------- 2 files changed, 4 insertions(+), 55 deletions(-) delete mode 100644 tests/testthat/test-test-load-example.R diff --git a/R/utils.R b/R/utils.R index a38302a..84ac1ef 100644 --- a/R/utils.R +++ b/R/utils.R @@ -215,10 +215,10 @@ summarise_commit <- function(object) { #' @export load_example <- function(path, force = FALSE) { # see if path exists - if (!fs::file_exists(path)) + if (!file.exists(path)) stop("You must provide a valid path") - if (!force && length(list.files(path))) { + if (length(list.files(path))) { choice <- menu( choices = c("Yes", "No"), title = glue("{path} seems to not be empty. Would you like to proceed?") @@ -226,11 +226,6 @@ load_example <- function(path, force = FALSE) { if (choice == 2) stop("Process aborted by user. Consider creating a new empty path.") - } else if (length(list.files(path))) { - message(glue( - "{path} seems to not be empty. ", - "Continuing as parameter 'force' is TRUE" - )) } ex_path <- system.file( @@ -241,16 +236,10 @@ load_example <- function(path, force = FALSE) { files <- list.files(path = ex_path, full.names = TRUE) for (file in files) { - if (fs::is_dir(file)) { - # Due to overwrite = TRUE the destination must include the name of the - # directory to be created - fs::dir_copy(file, fs::path(path, fs::path_file(file)), overwrite = TRUE) - } else { - fs::file_copy(file, path, overwrite = TRUE) - } + file.copy(from = file, to = path, recursive = TRUE) print(glue("{basename(file)} created at {path}")) } - fpath <- fs::path(path, "run_tests.R") # nolint + fpath <- file.path(path, "run_tests.R") # nolint message(glue("Follow instructions in {fpath}")) } diff --git a/tests/testthat/test-test-load-example.R b/tests/testthat/test-test-load-example.R deleted file mode 100644 index ad12052..0000000 --- a/tests/testthat/test-test-load-example.R +++ /dev/null @@ -1,40 +0,0 @@ -# Necessary test as fs::dir_copy with overwrite has a different behavior -# than file.copy. -# It copies the content of the directory of the "from" path into the -# destination, instead of the directory itself -test_that("Load example creates correct structure", { - example_path <- fs::path(tempdir(), "load_example") - fs::dir_create(example_path) - load_example(example_path, force = TRUE) - - files <- example_path |> - fs::path( - c( - "run_tests.R", - fs::path("app", "ui.R"), - fs::path("app", "server.R"), - fs::path("app", "global.R"), - fs::path("app", "tests", "testthat.R"), - fs::path("app", "tests", "testthat", "setup.R"), - fs::path("app", "tests", "testthat", "test-use_this_one_1.R"), - fs::path("app", "tests", "testthat", "test-use_this_one_2.R") - ) - ) - - dirs <- example_path |> - fs::path( - c( - fs::path("app", "tests"), - fs::path("app", "tests", "cypress"), - fs::path("app", "tests", "testthat") - ) - ) - - expect_true(all(fs::file_exists(files))) - expect_true(all(fs::is_file(files))) - expect_false(all(fs::is_dir(files))) - - expect_true(all(fs::dir_exists(dirs))) - expect_true(all(fs::is_dir(dirs))) - expect_false(all(fs::is_file(dirs))) -}) From 7c045392c2d6300ed6c8e2f6b92d4fa38cef4048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Mon, 28 Nov 2022 14:39:29 +0100 Subject: [PATCH 195/225] revert move_shinytest2_tests changes --- R/utils_shinytest2.R | 17 ++-------- tests/testthat/test-utils_shinytest2.R | 47 +++++--------------------- 2 files changed, 11 insertions(+), 53 deletions(-) diff --git a/R/utils_shinytest2.R b/R/utils_shinytest2.R index f73c4e7..eaeadd8 100644 --- a/R/utils_shinytest2.R +++ b/R/utils_shinytest2.R @@ -28,22 +28,9 @@ create_shinytest2_structure <- function(app_dir) { #' #' @keywords internal move_shinytest2_tests <- function(project_path, shinytest2_dir) { - tests_dir <- fs::path(project_path, "tests") # copy everything to the temporary directory - if (fs::is_dir(shinytest2_dir)) { - fs::dir_copy( - path = shinytest2_dir, - new_path = tests_dir, - overwrite = TRUE - ) - } else { - # should never reach this - fs::file_copy( - path = shinytest2_dir, - new_path = tests_dir, - overwrite = TRUE - ) - } + file.copy(from = shinytest2_dir, to = project_path, recursive = TRUE) + tests_dir <- file.path(project_path, "tests") return(tests_dir) } diff --git a/tests/testthat/test-utils_shinytest2.R b/tests/testthat/test-utils_shinytest2.R index 42558ac..410bd3e 100644 --- a/tests/testthat/test-utils_shinytest2.R +++ b/tests/testthat/test-utils_shinytest2.R @@ -1,50 +1,21 @@ -library(fs) - test_that("Check if we are able to move files properly", { tmp_dir <- tempdir() - # Create the folder that contains the tests, it's named tst - # on purpuse as it won't be a valid directory and shinytests2 would - # fail tmp_dir1 <- file.path(tmp_dir, "folder1") + dir.create(tmp_dir1, showWarnings = FALSE) + tmp_dir2 <- file.path(tmp_dir, "folder2") + dir.create(tmp_dir2, showWarnings = FALSE) - dir.create(path = tmp_dir1, recursive = TRUE) - shinytest2_dir <- fs::path(tmp_dir1, "tst") - dir.create(shinytest2_dir, showWarnings = FALSE) - file.create(fs::path(shinytest2_dir, "some_example.txt")) - file.create(fs::path(shinytest2_dir, "some_example2.txt")) - file.create(fs::path(shinytest2_dir, "some_example3.txt")) - - # Create a mock project path base directory - tmp_dir2 <- fs::path(tmp_dir, "folder2") - dir.create(tmp_dir2, recursive = TRUE) - project_path <- tmp_dir2 - dir.create(fs::path(tmp_dir2, "something")) - file.create(fs::path(shinytest2_dir, "root.txt")) - - # The result of the copy should land on tests folder - shinytest2_dir_copy_manual <- file.path(tmp_dir2, "tests") - - # [ACTION] Actual copy of the shinytest2 files - shinytest2_dir_copy_auto <- move_shinytest2_tests( - project_path = tmp_dir2, - shinytest2_dir = shinytest2_dir - ) + shinytest2_dir <- file.path(tmp_dir1, "tst") + shinytest2_dir_copy <- file.path(tmp_dir2, "tst") + dir.create(path = shinytest2_dir, showWarnings = FALSE) - # Test if the copy was successful - expect_true(dir.exists(shinytest2_dir_copy_manual)) - expect_true(dir.exists(shinytest2_dir_copy_auto)) - expect_equal(shinytest2_dir_copy_auto, shinytest2_dir_copy_manual) + move_shinytest2_tests(project_path = tmp_dir2, shinytest2_dir = shinytest2_dir) - short_var <- shinytest2_dir_copy_auto - str_mask <- "some_example{input}.txt" - c("", 2, 3) |> - sapply(function(input) { - expect_true(file.exists(fs::path(short_var, glue(str_mask)))) - }) + expect_true(file.exists(shinytest2_dir_copy)) }) test_that("Check if we are able to create shinytest2 structure", { tmp_dir <- create_shinytest2_structure(app_dir = ".") - expect_true(fs::file_exists(fs::path(tmp_dir, "app.R"))) + expect_true(file.exists(file.path(tmp_dir, "app.R"))) }) From 283636a3159951a1c9886962671f8244ef03b6c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Thu, 1 Dec 2022 13:45:42 +0100 Subject: [PATCH 196/225] remove logger references --- DESCRIPTION | 3 +-- R/globals.R | 3 --- R/utils_cypress.R | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 7015a61..8dbd1c3 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -35,6 +35,5 @@ Imports: stringr, testthat, rlang, - fs, - logger + fs Language: en-US diff --git a/R/globals.R b/R/globals.R index b5709d2..06155fd 100644 --- a/R/globals.R +++ b/R/globals.R @@ -12,6 +12,3 @@ utils::globalVariables( "total_time" ) ) - -# Setting threshold to debug (temporary and should be removed) -logger::log_threshold(logger::INFO) diff --git a/R/utils_cypress.R b/R/utils_cypress.R index 6fef334..66772ba 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -37,7 +37,7 @@ create_cypress_structure <- function(app_dir, port, debug) { # If system cannot symlink then try to clone the repository # This may happen on some windows versions # This can be an expensive operation on big repositories - logger::log_warn( + messsage( "Could not create symbolic link with fs package, ", "trying with git clone..." ) From 46d57d0bdd2ad3d2f51cc9f81256621c8d9f22ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Thu, 1 Dec 2022 14:13:40 +0100 Subject: [PATCH 197/225] clean this extra parameter, different issue --- R/utils.R | 3 +-- man/load_example.Rd | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/R/utils.R b/R/utils.R index 84ac1ef..1bc3959 100644 --- a/R/utils.R +++ b/R/utils.R @@ -208,12 +208,11 @@ summarise_commit <- function(object) { #' the selected `path`. #' #' @param path A character vector of full path name -#' @param force Create example even if directory is not empty #' #' @importFrom glue glue #' @importFrom utils menu #' @export -load_example <- function(path, force = FALSE) { +load_example <- function(path) { # see if path exists if (!file.exists(path)) stop("You must provide a valid path") diff --git a/man/load_example.Rd b/man/load_example.Rd index 63d28ab..912189f 100644 --- a/man/load_example.Rd +++ b/man/load_example.Rd @@ -4,12 +4,10 @@ \alias{load_example} \title{Load an application and instructions to run shiny.benchmark} \usage{ -load_example(path, force = FALSE) +load_example(path) } \arguments{ \item{path}{A character vector of full path name} - -\item{force}{Create example even if directory is not empty} } \description{ This function aims to generate a template to be used From 5de0f391a6acaa90282001c65720679f022cb306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Thu, 1 Dec 2022 14:31:58 +0100 Subject: [PATCH 198/225] removes commit_exist.. another PR --- DESCRIPTION | 1 - R/benchmark_cypress.R | 3 --- R/utils.R | 26 -------------------------- 3 files changed, 30 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 8dbd1c3..efcc633 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -34,6 +34,5 @@ Imports: shinytest2, stringr, testthat, - rlang, fs Language: en-US diff --git a/R/benchmark_cypress.R b/R/benchmark_cypress.R index 2e86144..cee35c5 100644 --- a/R/benchmark_cypress.R +++ b/R/benchmark_cypress.R @@ -102,9 +102,6 @@ run_cypress_ptest <- function( n_rep, debug ) { - - # Checks if commit exists - commit_exists(commit) # checkout to the desired commit checkout(branch = commit, debug = debug) date <- get_commit_date(branch = commit) diff --git a/R/utils.R b/R/utils.R index 1bc3959..742bed3 100644 --- a/R/utils.R +++ b/R/utils.R @@ -94,32 +94,6 @@ performance_test_cmd <- function(project_path) { } } -#' @title Check if git commit hash exists -#' -#' @description Can be anything git recognizes as a commit, such -#' as a commit hash, a branch, a tag, ... -#' -#' @param commit commit hash code, branch name or tag -#' -#' @return true if exists, an error message if not -#' -#' @keywords internal -commit_exists <- function(commit) { - result <- system( - command = glue::glue("git rev-parse --verify {commit}"), - intern = FALSE, - wait = TRUE - ) - if (result == 128) { - rlang::abort( - message = glue( - "git error:: Commit/branch/tag/.. '{commit}' doesn't exist" - ) - ) - } - TRUE -} - #' @title Check for uncommitted files #' #' @keywords internal From 0e57e4dfb8b64ce221bcb3a2f0fa579a357ffac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Thu, 1 Dec 2022 14:32:33 +0100 Subject: [PATCH 199/225] implement comments from review --- R/utils.R | 6 +----- R/utils_cypress.R | 14 +++++++++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/R/utils.R b/R/utils.R index 742bed3..33ebd6a 100644 --- a/R/utils.R +++ b/R/utils.R @@ -87,11 +87,7 @@ checkout <- function(branch, debug) { #' #' @keywords internal performance_test_cmd <- function(project_path) { - if (grepl("win", .Platform$OS.type, ignore.case = TRUE)) { - glue("yarn --cwd \"{fs::path(project_path, 'node')}\" performance-test") - } else { - glue("cd {project_path}; set -eu; exec yarn --cwd node performance-test") - } + glue("yarn --cwd \"{fs::path(project_path, 'node')}\" performance-test") } #' @title Check for uncommitted files diff --git a/R/utils_cypress.R b/R/utils_cypress.R index 66772ba..c7a7718 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -34,6 +34,18 @@ create_cypress_structure <- function(app_dir, port, debug) { fs::link_create(app_dir, root_path, symbolic = TRUE) }, error = function(e) { + + choice <- menu( + choices = c("Yes", "No"), + title = glue( + "A symbolic link cannot be created, it is possible to clone ", + "the repository, but it can take some time and space on disk. ", + "Would you like to proceed with this operations?") + ) + + if (choice == 2) + stop("Process aborted by user.") + # If system cannot symlink then try to clone the repository # This may happen on some windows versions # This can be an expensive operation on big repositories @@ -84,7 +96,7 @@ create_node_list <- function(tests_path, port) { "start-server-and-test run-app http://localhost:{port} run-cypress" ), "run-app" = glue( - "cd root && ", + "cd root; ", "Rscript -e \"shiny::runApp(port = {port})\"" ), "run-cypress" = glue("cypress run --project {tests_path}") From dd12fcf6b65f20753734c41be312c230c8043d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Thu, 1 Dec 2022 14:34:41 +0100 Subject: [PATCH 200/225] documents why unlink has to be used instead of fs::file_delete --- R/benchmark_shinytest2.R | 2 ++ 1 file changed, 2 insertions(+) diff --git a/R/benchmark_shinytest2.R b/R/benchmark_shinytest2.R index 8e62086..590d673 100644 --- a/R/benchmark_shinytest2.R +++ b/R/benchmark_shinytest2.R @@ -63,6 +63,8 @@ benchmark_shinytest2 <- function( restore_env(branch = current_branch, renv_prompt = renv_prompt) # Cleaning the temporary directory + # couldn't use fs::file_delete / fs::directory_delete as a process + # is accessing one of the files and it fails. unlink does not unlink(fs::path(project_path, "tests")) } ) From 012ecaf8c0d70f0c51a548286fcdbb404b13f5f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Thu, 1 Dec 2022 15:04:17 +0100 Subject: [PATCH 201/225] removes extra documentation --- man/commit_exists.Rd | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 man/commit_exists.Rd diff --git a/man/commit_exists.Rd b/man/commit_exists.Rd deleted file mode 100644 index 5e66203..0000000 --- a/man/commit_exists.Rd +++ /dev/null @@ -1,19 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils.R -\name{commit_exists} -\alias{commit_exists} -\title{Check if git commit hash exists} -\usage{ -commit_exists(commit) -} -\arguments{ -\item{commit}{commit hash code, branch name or tag} -} -\value{ -true if exists, an error message if not -} -\description{ -Can be anything git recognizes as a commit, such -as a commit hash, a branch, a tag, ... -} -\keyword{internal} From c399b4f7f5fe2d6961a6a1d35bddc46ea158fd08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Thu, 1 Dec 2022 15:05:54 +0100 Subject: [PATCH 202/225] testing solution && may need to be used --- R/utils_cypress.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/utils_cypress.R b/R/utils_cypress.R index c7a7718..6129421 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -96,7 +96,7 @@ create_node_list <- function(tests_path, port) { "start-server-and-test run-app http://localhost:{port} run-cypress" ), "run-app" = glue( - "cd root; ", + "cd root && ", "Rscript -e \"shiny::runApp(port = {port})\"" ), "run-cypress" = glue("cypress run --project {tests_path}") From 74bd9077740e24e3eee04b36faeb9f1e598e744d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Thu, 1 Dec 2022 16:39:17 +0100 Subject: [PATCH 203/225] an extra 's' in the function call --- R/utils_cypress.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/utils_cypress.R b/R/utils_cypress.R index 6129421..7533d05 100644 --- a/R/utils_cypress.R +++ b/R/utils_cypress.R @@ -49,7 +49,7 @@ create_cypress_structure <- function(app_dir, port, debug) { # If system cannot symlink then try to clone the repository # This may happen on some windows versions # This can be an expensive operation on big repositories - messsage( + message( "Could not create symbolic link with fs package, ", "trying with git clone..." ) From e74336ba73ffcdfa32738eff884fcac03b659ce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Mon, 5 Dec 2022 19:30:41 +0100 Subject: [PATCH 204/225] converts load_example to fs library and adds tests --- R/utils.R | 29 +++++++--- tests/testthat/test-load_example.R | 85 ++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 tests/testthat/test-load_example.R diff --git a/R/utils.R b/R/utils.R index 33ebd6a..a15774c 100644 --- a/R/utils.R +++ b/R/utils.R @@ -178,16 +178,22 @@ summarise_commit <- function(object) { #' the selected `path`. #' #' @param path A character vector of full path name +#' @param force Create example even if directory does not exist or is not empty #' #' @importFrom glue glue #' @importFrom utils menu #' @export -load_example <- function(path) { +#' @examples +#' load_example(file.path(tempdir(), "example_destination"), force = TRUE) +load_example <- function(path, force = FALSE) { # see if path exists - if (!file.exists(path)) + if (!force && !fs::file_exists(path)) stop("You must provide a valid path") + else if (!fs::file_exists(path)) { + fs::dir_create(path, recurse = TRUE) + } - if (length(list.files(path))) { + if (!force && length(fs::dir_ls(path))) { choice <- menu( choices = c("Yes", "No"), title = glue("{path} seems to not be empty. Would you like to proceed?") @@ -195,6 +201,11 @@ load_example <- function(path) { if (choice == 2) stop("Process aborted by user. Consider creating a new empty path.") + } else if (length(fs::dir_ls(path))) { + message(glue( + "{path} seems to not be empty. ", + "Continuing as parameter `force = TRUE`" + )) } ex_path <- system.file( @@ -202,13 +213,19 @@ load_example <- function(path) { package = "shiny.benchmark", mustWork = TRUE ) - files <- list.files(path = ex_path, full.names = TRUE) + files <- fs::dir_ls(path = ex_path, fun = fs::path_real) for (file in files) { - file.copy(from = file, to = path, recursive = TRUE) + if (fs::is_dir(file)) { + # Due to overwrite = TRUE the destination must include the name of the + # directory to be created + fs::dir_copy(file, fs::path(path, fs::path_file(file)), overwrite = TRUE) + } else { + fs::file_copy(file, path, overwrite = TRUE) + } print(glue("{basename(file)} created at {path}")) } - fpath <- file.path(path, "run_tests.R") # nolint + fpath <- fs::path(path, "run_tests.R") # nolint message(glue("Follow instructions in {fpath}")) } diff --git a/tests/testthat/test-load_example.R b/tests/testthat/test-load_example.R new file mode 100644 index 0000000..2e66eae --- /dev/null +++ b/tests/testthat/test-load_example.R @@ -0,0 +1,85 @@ +# Necessary test as fs::dir_copy with overwrite has a different behavior +# than file.copy. +# It copies the content of the directory of the "from" path into the +# destination, instead of the directory itself +test_that("Load example creates correct structure", { + example_path <- fs::path(tempdir(), "load_example") + fs::dir_create(example_path) + local({ + local_mock(menu = function(...) stop("Opps, shouldn't reach this")) + load_example(example_path, force = TRUE) + }) + + files <- example_path |> + fs::path( + c( + "run_tests.R", + fs::path("app", "ui.R"), + fs::path("app", "server.R"), + fs::path("app", "global.R"), + fs::path("app", "tests", "testthat.R"), + fs::path("app", "tests", "testthat", "setup.R"), + fs::path("app", "tests", "testthat", "test-use_this_one_1.R"), + fs::path("app", "tests", "testthat", "test-use_this_one_2.R") + ) + ) + + dirs <- example_path |> + fs::path( + c( + fs::path("app", "tests"), + fs::path("app", "tests", "cypress"), + fs::path("app", "tests", "testthat") + ) + ) + + expect_true(all(fs::file_exists(files))) + expect_true(all(fs::is_file(files))) + expect_false(all(fs::is_dir(files))) + + expect_true(all(fs::dir_exists(dirs))) + expect_true(all(fs::is_dir(dirs))) + expect_false(all(fs::is_file(dirs))) +}) + +test_that("Does not create load_examples on non-existing directory", { + example_path <- fs::path( + tempdir(), + glue::glue("load_example_not_existing{unclass(Sys.time())}") + ) + + local({ + local_mock(menu = function(...) stop("Opps, shouldn't reach this")) + load_example(example_path) |> + expect_error("You must provide a valid path") + }) + + fs::dir_create(example_path) + local({ + local_mock(menu = function(...) stop("Opps, shouldn't reach this")) + load_example(example_path) |> + expect_output("app created at") + }) +}) + +test_that("Does not create load_examples if there is a file in directory", { + example_path <- fs::path( + tempdir(), + glue::glue("load_example_not_empty{unclass(Sys.time())}") + ) + fs::dir_create(example_path) + fs::file_create(fs::path(example_path, "touch.txt")) + + local({ + local_mock(menu = function(...) 2) + load_example(example_path) |> + expect_error("Consider creating a new empty path.") + }) + + local({ + local_mock(menu = function(...) 1) + load_example(example_path) |> + expect_output("app created at") + }) +}) + From ff6539b995d9f83b07132a7d19c625ee2a114f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Mon, 5 Dec 2022 19:35:55 +0100 Subject: [PATCH 205/225] didn't run devtools::document --- man/load_example.Rd | 7 ++++++- tests/testthat/test-load_example.R | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/man/load_example.Rd b/man/load_example.Rd index 912189f..df8ada0 100644 --- a/man/load_example.Rd +++ b/man/load_example.Rd @@ -4,10 +4,12 @@ \alias{load_example} \title{Load an application and instructions to run shiny.benchmark} \usage{ -load_example(path) +load_example(path, force = FALSE) } \arguments{ \item{path}{A character vector of full path name} + +\item{force}{Create example even if directory does not exist or is not empty} } \description{ This function aims to generate a template to be used @@ -17,3 +19,6 @@ application will be added to the folder as well as instructions on how to perform the performance checks. Be aware that a new git repo is need in the selected \code{path}. } +\examples{ +load_example(file.path(tempdir(), "example_destination"), force = TRUE) +} diff --git a/tests/testthat/test-load_example.R b/tests/testthat/test-load_example.R index 2e66eae..20688e8 100644 --- a/tests/testthat/test-load_example.R +++ b/tests/testthat/test-load_example.R @@ -82,4 +82,3 @@ test_that("Does not create load_examples if there is a file in directory", { expect_output("app created at") }) }) - From 43f1e16f1217d17a9097b136fbd7a2eaf83f1b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ver=C3=ADssimo?= Date: Mon, 5 Dec 2022 20:47:35 +0100 Subject: [PATCH 206/225] adds mockr to suggest packages --- DESCRIPTION | 1 + 1 file changed, 1 insertion(+) diff --git a/DESCRIPTION b/DESCRIPTION index 79b8f4b..0e1ac0d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -22,6 +22,7 @@ Suggests: knitr, lintr, rcmdcheck, + mockr, spelling Imports: dplyr, From c99b1d8d6587409bbf4c921d8b792b50e0fa1a36 Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Fri, 9 Dec 2022 10:42:34 +0100 Subject: [PATCH 207/225] ci: Add code coverage to the CI. --- .github/workflows/main.yml | 5 +++++ DESCRIPTION | 1 + 2 files changed, 6 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a014779..ff8cef2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -57,3 +57,8 @@ jobs: print(spell_check) } quit(status = nrow(spell_check) > 0) + + - name: Test coverage + if: matrix.config.os == 'ubuntu-22.04' && matrix.config.r == 'release' + run: | + Rscript -e 'covr::codecov(token = "${{secrets.CODECOV_TOKEN}}")' diff --git a/DESCRIPTION b/DESCRIPTION index 0e1ac0d..789af5e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -19,6 +19,7 @@ VignetteBuilder: knitr Depends: R (>= 3.1.0) Suggests: + codecov, knitr, lintr, rcmdcheck, From 3b9b7e0b7141a9e20a8c2afd027843ae9006cb1b Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Fri, 9 Dec 2022 10:45:08 +0100 Subject: [PATCH 208/225] fix: Fix covr package name. --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 789af5e..9e839df 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -19,7 +19,7 @@ VignetteBuilder: knitr Depends: R (>= 3.1.0) Suggests: - codecov, + covr, knitr, lintr, rcmdcheck, From ab29e27c2ed78b2a0a4b398f6c1d7ff8f05f57ba Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Fri, 9 Dec 2022 10:52:30 +0100 Subject: [PATCH 209/225] chore: Add code coverage badge. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8ec6a79..9a85724 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![R-CMD-check](https://github.com/Appsilon/shiny.benchmark/workflows/R-CMD-check/badge.svg)](https://github.com/Appsilon/shiny.benchmark/actions?workflow=R-CMD-check) +[![codecov](https://codecov.io/github/Appsilon/shiny.benchmark/branch/develop/graph/badge.svg?token=JBEL2P5GIO)](https://codecov.io/github/Appsilon/shiny.benchmark) `shiny.benchmark` is a tool aimed to measure and compare the performance of different versions of a `shiny` application. Based on a list of different application versions, accessible by a git repo by its refs (commit hash or branch name), the user can write instructions to be executed using Cypress or `shinytest2`. These instructions are then evaluated by the different versions of your `shiny` application and therefore the performance's improvement/deterioration (time elapsed) are be recorded. From 4dcf66bb2c3a4b8d8b2c03c65624f49bd4e3ce4d Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Fri, 9 Dec 2022 10:58:35 +0100 Subject: [PATCH 210/225] chore: Update WORDLIST. --- inst/WORDLIST | 1 + 1 file changed, 1 insertion(+) diff --git a/inst/WORDLIST b/inst/WORDLIST index f3ffef9..884a8e8 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -1,3 +1,4 @@ +codecov CMD JS POSIXct From 4afea22f90a9798ac85363c0f37420fc53d55f79 Mon Sep 17 00:00:00 2001 From: douglas Date: Fri, 9 Dec 2022 11:04:38 +0100 Subject: [PATCH 211/225] creating a tutorial --- .../how-to-measure-apps-performance.Rmd | 410 ++++++++++++++++++ vignettes/tutorial/images/app.png | Bin 0 -> 33130 bytes vignettes/tutorial/images/console_basic.png | Bin 0 -> 43064 bytes vignettes/tutorial/images/plot.png | Bin 0 -> 10007 bytes 4 files changed, 410 insertions(+) create mode 100644 vignettes/tutorial/how-to-measure-apps-performance.Rmd create mode 100644 vignettes/tutorial/images/app.png create mode 100644 vignettes/tutorial/images/console_basic.png create mode 100644 vignettes/tutorial/images/plot.png diff --git a/vignettes/tutorial/how-to-measure-apps-performance.Rmd b/vignettes/tutorial/how-to-measure-apps-performance.Rmd new file mode 100644 index 0000000..cf8f341 --- /dev/null +++ b/vignettes/tutorial/how-to-measure-apps-performance.Rmd @@ -0,0 +1,410 @@ +--- +title: "Tutorial: Compare performance of different versions of a shiny application" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Tutorial: Compare performance of different versions of a shiny application} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +# Setup + +## How to install shiny.benchmark? + +`shiny.benchmark` can use two different engines to test the changes in the performance of your application: [shinytest2](https://rstudio.github.io/shinytest2/) and [Cypress](https://www.cypress.io/). The latter requires `Node` (version 12 or higher) and `yarn` (version 1.22.17 or higher) to be available. To install them on your computer, follow the guidelines on the documentation pages: + +- [Node](https://nodejs.org/en/download/) +- [yarn](https://yarnpkg.com/getting-started/install) + +Besides that, on Linux, it might be required to install other `Cypress` dependencies. Check the [documentation](https://docs.cypress.io/guides/getting-started/installing-cypress#Linux-Prerequisites) to find out more. + +To install `shiny.benchmark` use the following command: + +```r +remotes::install_github("Appsilon/shiny.benchmark") +``` + +---- + +# Create an initial application + +Let's start creating an application that will serve us as a guide through the `shiny.benchmark` functionalities. + +Save the following code as `ui.R`. It is a simple UI containing three columns with one action button in each. Also each column has an output which will be created in the server file later. + +```r +function() { + bootstrapPage( + tags$h1("Measuring time in different commits"), + column( + width = 4, + actionButton(inputId = "run1", label = "Run 1"), + uiOutput(outputId = "out1") + ), + column( + width = 4, + actionButton(inputId = "run2", label = "Run 2"), + uiOutput(outputId = "out2") + ), + column( + width = 4, + actionButton(inputId = "run3", label = "Run 3"), + uiOutput(outputId = "out3") + ) + ) +} +``` + +In the server side, the application will use the `Sys.sleep` function to simulate a task every time the user press a button. This will be helpful for us since we can easily increase/decrease the sleep time to simulate improvements/deterioration of the application. Save the following code as `server.R`: + +```r +times <- c(10, 5, 2) + +function(input, output, session) { + # Sys.sleep + react1 <- eventReactive(input$run1, { + out <- system.time( + Sys.sleep(times[1] + rexp(n = 1, rate = 10)) # we will play with the time here + ) + + return(out[3]) + }) + + react2 <- eventReactive(input$run2, { + out <- system.time( + Sys.sleep(times[2] + rexp(n = 1, rate = 10)) # we will play with the time here + ) + + return(out[3]) + }) + + react3 <- eventReactive(input$run3, { + out <- system.time( + Sys.sleep(times[3] + rexp(n = 1, rate = 10)) # we will play with the time here + ) + + return(out[1]) + }) + + # outputs + output$out1 <- renderUI({ + tags$span(round(react1()), style = "font-size: 500px;") + }) + + output$out2 <- renderUI({ + tags$span(round(react2()), style = "font-size: 500px;") + }) + + output$out3 <- renderUI({ + tags$span(round(react3()), style = "font-size: 500px;") + }) +} +``` + +The application should look like this: + +```r +shiny::runApp() +``` + + + +# Tests engines + +`shiny.benchmark` works under two different engines: `Cypress` and `shinytest2`. + +## Cypress + +Cypress is a widely used end to end testing JavaScript library. Because its broader usage, this engine allows the user to take advantage of a huge number of functionalities in order to test its applications. Also, the community is active and therefore it is easier to find solution for bugs you may encounter while coding. + +Save the following code as `tests/cypress/test-set1.js`: + +```r +describe('Cypress test', () => { + it('Out1 time elapsed - set1', () => { // replace set1 by set2 + cy.visit('/'); + cy.get('#run1').click(); + cy.get('#out1', {timeout: 10000}).should('be.visible'); + }); + + // Test how long it takes to wait for out2 + it('Out2 time elapsed - set1', () => { // replace set1 by set2 + cy.get('#run2').click(); + cy.get('#out2', {timeout: 10000}).should('be.visible'); + }); + + // Test how long it takes to wait for out3 + it('Out3 time elapsed - set1', () => { // replace set1 by set2 + cy.get('#run3').click(); + cy.get('#out3', {timeout: 10000}).should('be.visible'); + }); +}); +``` + +This code is simulating clicks in the three buttons we have in our application. Also it waits for the output to appear. Replace `set1` by `set2` in the code and save it as `tests/cypress/test-set2.js` as well. It will be useful to present some functionalities later. + +## shinytest2 + +`shinytest2` is an R package maintained by `Posit`. It is handy for R users since all tests can be done using R only (differently than Cypress). To set up it easily, you can run `shinytest2::use_shinytest2()`. It will create configuration files which you do not need to change for this tutorial. + +Save the following code as `tests/testthat/test-set1.R`: + +```r +test_that("Out1 time elapsed - set1", { + app <- AppDriver$new(name = "test1", height = 975, width = 1619) + app$click("run1") + app$expect_values(output = "out1") +}) + +test_that("Out2 time elapsed - set1", { + app <- AppDriver$new(name = "test2", height = 975, width = 1619) + app$click("run2") + app$expect_values(output = "out2") +}) + +test_that("Out3 time elapsed - set1", { + app <- AppDriver$new(name = "test3", height = 975, width = 1619) + app$click("run3") + app$expect_values(output = "out3") +}) +``` + +Again, replace `set1` by `set2` in the code and save it as `tests/testthat/test-set2.R` as well. + +# Package management + +During the development process, it is normal to use different packages/package versions. `renv` allow us to manage package versions and is used by `shiny.benchmark` by default. Run the following code to setup `renv` in our test application. + +```r +renv::init() +renv::install("remotes") +remotes::install_github("Appsilon/shiny.benchmark") +renv::snapshot(prompt = FALSE) +``` + +# Simulating app versions + +In a regular project, you may use `git` to maintain the code. In this case, it is natural to have different app's versions in different branches/commits/releases. `shiny.benchmark` take advantage of these different `git` refs to run tests under different code versions. Add the following code to `.gitignore` to avoid problems with uncommitted files later: + +```git +.Rhistory +.Rproj.user/ +.Rproj.user +renv/ +``` + +Now, lets create a `git` repo and commit the current application into the `develop` branch: + +```git +git init +git checkout -b develop +git add . +git commit -m "first commit" +``` + +Also, let's create a new branch called `feature1`: + +```git +git checkout -b feature1 +``` + +At this point, we can simulate improvement in our application. To do so, let's change `Sys.sleep` time in the server function. Replace `times <- c(10, 5, 2)` by `times <- c(5, 2.5, 1)` in `server.R` and then commit the changes. + +```git +git add server.R +git commit -m "improving performance" +``` + +To play with `renv` let's downgrade `shiny` version and snapshoot it: + +```git +git checkout -b feature2 +``` + +Replace `times <- c(5, 2.5, 1)` by `times <- c(2.5, 1.25, 0.5)` in `server.R`. Also, run the following code to downgrade `shiny`: + +```r +renv::install("shiny@1.7.0") +renv::snapshot(prompt = FALSE) +``` + +Commit the changes: + +```git +git add . +git commit -m "downgrading shiny" +git checkout develop +``` + +And we are all set! + +# shiny.benchmark + +Now we have all ingredients needed: An application, a set of tests and different versions in a `git` repo. `shiny.benchmark::benchmark` function has only two mandatory arguments: + +- `commit_list`: a named list of `git` refs (commit hashes, branch names, tags, ...) +- `cypress_dir` or `shinytest2_dir`: path to `Cypress` or `shinytest2` tests + +By default, `shiny.benchmark` uses `renv`. To turn `renv` off just set `use_renv = FALSE`. + +```r +library(shiny.benchmark) + +commits <- list( + "develop" = "710fce371b3bf25c9223a11c70d5b27e5d16448e", # develop + "feature1" = "feature1", + "using_renv" = "feature2" +) + +cypress_dir <- "tests/cypress/" +testthat_dir <- "tests/" + +cypress_out <- benchmark( + commit_list = commits, + cypress_dir = cypress_dir, + use_renv = FALSE +) + +shinytest2_out <- benchmark( + commit_list = commits, + shinytest2_dir = testthat_dir, + use_renv = FALSE +) +``` + +For the sake of illustration, we are using the hash code in the develop branch (which is not needed). The console should display something similar to: + + + +You can access the results using `cypress_out$performance` or `shinytest2_out$performance`: + +```r +cypress_out$performance +``` + +```{r echo = FALSE, eval = TRUE} +list(develop = list(structure(list(date = structure(c(1670530838, +1670530838, 1670530838, 1670530838, 1670530838, 1670530838), class = c("POSIXct", +"POSIXt"), tzone = ""), rep_id = c(1L, 1L, 1L, 1L, 1L, 1L), test_name = c("Out1 time elapsed - set1", +"Out2 time elapsed - set1", "Out3 time elapsed - set1", "Out1 time elapsed - set2", +"Out2 time elapsed - set2", "Out3 time elapsed - set2"), duration_ms = c(10743L, +5215L, 2309L, 10526L, 5354L, 2267L)), class = "data.frame", row.names = c(NA, +-6L))), feature1 = list(structure(list(date = structure(c(1670530879, +1670530879, 1670530879, 1670530879, 1670530879, 1670530879), class = c("POSIXct", +"POSIXt"), tzone = ""), rep_id = c(1L, 1L, 1L, 1L, 1L, 1L), test_name = c("Out1 time elapsed - set1", +"Out2 time elapsed - set1", "Out3 time elapsed - set1", "Out1 time elapsed - set2", +"Out2 time elapsed - set2", "Out3 time elapsed - set2"), duration_ms = c(5764L, +2798L, 1321L, 5316L, 2713L, 1169L)), class = "data.frame", row.names = c(NA, +-6L))), using_renv = list(structure(list(date = structure(c(1670530923, +1670530923, 1670530923, 1670530923, 1670530923, 1670530923), class = c("POSIXct", +"POSIXt"), tzone = ""), rep_id = c(1L, 1L, 1L, 1L, 1L, 1L), test_name = c("Out1 time elapsed - set1", +"Out2 time elapsed - set1", "Out3 time elapsed - set1", "Out1 time elapsed - set2", +"Out2 time elapsed - set2", "Out3 time elapsed - set2"), duration_ms = c(3435L, +1509L, 719L, 2978L, 1431L, 763L)), class = "data.frame", row.names = c(NA, +-6L)))) +``` + +You can notice that both files are reported (`test-set1` and `test-set2`). Also, the result is a list of `data.frames` in which each entry correspond to a specific commit. + +For now on we will use only `shinytest2`. However, everything is also applied for Cypress. + +## Package management + +In order to use `renv`, simply assign `use_renv = TRUE`. You can also use `renv_prompt = TRUE` if you want to see what renv is applying in the background. + +```r +shinytest2_out <- benchmark( + commit_list = commits, + shinytest2_dir = testthat_dir, + use_renv = TRUE, + renv_prompt = TRUE +) +``` + +## Handling multiple files + +Sometimes it is not our interest to measure performance of all the tests we have. in order to select specific files you can use the argument `tests_pattern`. This argument accept either a vector of files (one for each item in commit list). Also, it is possible to search for a pattern in `tests` files. + +```r +shinytest2_out <- benchmark( + commit_list = commits, + shinytest2_dir = testthat_dir, + use_renv = FALSE, + tests_pattern = c("set[0-9]", "set1", "set2") +) +shinytest2_out$performance +``` + +```{r echo=FALSE} +list(develop = list(structure(list(date = structure(c(1670530838, +1670530838, 1670530838, 1670530838, 1670530838, 1670530838), class = c("POSIXct", +"POSIXt"), tzone = ""), rep_id = c(1L, 1L, 1L, 1L, 1L, 1L), test_name = c("Out1 time elapsed - set1", +"Out2 time elapsed - set1", "Out3 time elapsed - set1", "Out1 time elapsed - set2", +"Out2 time elapsed - set2", "Out3 time elapsed - set2"), duration_ms = c(12.649, +7.88999999999999, 4.51400000000001, 12.3340000000001, 7.47699999999998, +4.11500000000001)), class = "data.frame", row.names = c(NA, -6L +))), feature1 = list(structure(list(date = structure(c(1670530879, +1670530879, 1670530879), class = c("POSIXct", "POSIXt"), tzone = ""), + rep_id = c(1L, 1L, 1L), test_name = c("Out1 time elapsed - set1", + "Out2 time elapsed - set1", "Out3 time elapsed - set1"), + duration_ms = c(7.07800000000009, 5.447, 3.49199999999996 + )), class = "data.frame", row.names = c(NA, -3L))), using_renv = list( + structure(list(date = structure(c(1670530923, 1670530923, + 1670530923), class = c("POSIXct", "POSIXt"), tzone = ""), + rep_id = c(1L, 1L, 1L), test_name = c("Out1 time elapsed - set2", + "Out2 time elapsed - set2", "Out3 time elapsed - set2" + ), duration_ms = c(4.79999999999995, 3.63, 3.1339999999999 + )), class = "data.frame", row.names = c(NA, -3L)))) +``` + +Now the output is sightly different. For `develop` branch both files are in use (they match the pattern). For `feature1` and `feature2` only one file is in use. It can be useful when new tests are added during the development process and you need to run different tests for different versions. + +## Repetitions + +Sometimes it is important to repeat the measurement several times to have a distribution of the performance times instead of an unique measurement. To do so, it is possible to use the `n_rep argument` as follows: + +```r +shinytest2_out <- benchmark( + commit_list = commits, + shinytest2_dir = testthat_dir, + use_renv = FALSE, + tests_pattern = "set1", + n_rep = 5 +) +``` + +Some methods are implemented to make it easy to explore the results: + +```r +summary(shinytest2_out) +``` + +```{r echo = FALSE} +structure(list(commit = c("develop", "develop", "develop", "feature1", +"feature1", "feature1", "using_renv", "using_renv", "using_renv" +), test_name = c("Out1 time elapsed - set1", "Out2 time elapsed - set1", +"Out3 time elapsed - set1", "Out1 time elapsed - set1", "Out2 time elapsed - set1", +"Out3 time elapsed - set1", "Out1 time elapsed - set1", "Out2 time elapsed - set1", +"Out3 time elapsed - set1"), n = c(10L, 10L, 10L, 10L, 10L, 10L, +10L, 10L, 10L), mean = c(12.2756, 7.64179999999999, 4.70679999999998, +7.18020000000001, 5.22919999999999, 3.46759999999995, 5.05980000000004, +3.75569999999996, 2.82280000000001), median = c(12.238, 7.62050000000005, +4.77099999999996, 7.14600000000007, 5.1925, 3.40049999999985, +5.06850000000009, 3.60749999999996, 2.79049999999997), sd = c(0.212946106901357, +0.145640195916746, 0.210462876114088, 0.253890527590115, 0.191039728270784, +0.215106278641769, 0.340268456108157, 0.484944910960702, 0.259766904059041 +), min = c(11.963, 7.46699999999998, 4.35799999999995, 6.89200000000005, +4.89699999999993, 3.19999999999982, 4.57500000000005, 3.19899999999984, +2.49700000000007), max = c(12.602, 7.95299999999997, 4.97799999999984, +7.61400000000003, 5.56999999999994, 3.81999999999994, 5.75900000000001, +4.97299999999996, 3.21599999999989)), class = c("tbl_df", "tbl", +"data.frame"), row.names = c(NA, -9L)) +``` + +```r +plot(shinytest2_out) +``` + + + +## Creating a report diff --git a/vignettes/tutorial/images/app.png b/vignettes/tutorial/images/app.png new file mode 100644 index 0000000000000000000000000000000000000000..ffa484f234cd1ab748620b6e9635ac9cdd82450e GIT binary patch literal 33130 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,oWJe~9hbxB!BaU};hKDFkPfc3) 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

AiPVk&k-;_8-MR3Y%sZse!+|lLUgvyxH3Nu`k?p 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
  • >`DZ957(m! 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 Date: Fri, 9 Dec 2022 12:36:47 +0100 Subject: [PATCH 212/225] updating tutorial --- .../how-to-measure-apps-performance.Rmd | 30 ++++++++++++------ vignettes/tutorial/images/app.png | Bin 33130 -> 25032 bytes 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/vignettes/tutorial/how-to-measure-apps-performance.Rmd b/vignettes/tutorial/how-to-measure-apps-performance.Rmd index cf8f341..5b26b2e 100644 --- a/vignettes/tutorial/how-to-measure-apps-performance.Rmd +++ b/vignettes/tutorial/how-to-measure-apps-performance.Rmd @@ -109,6 +109,8 @@ shiny::runApp() +---- + # Tests engines `shiny.benchmark` works under two different engines: `Cypress` and `shinytest2`. @@ -141,11 +143,11 @@ describe('Cypress test', () => { }); ``` -This code is simulating clicks in the three buttons we have in our application. Also it waits for the output to appear. Replace `set1` by `set2` in the code and save it as `tests/cypress/test-set2.js` as well. It will be useful to present some functionalities later. +This code is simulating clicks in the three buttons we have in our application. Also it waits for the outputs to appear. In a new file, replace `set1` by `set2` in the code and save it as `tests/cypress/test-set2.js` as well. It will be useful to present some functionalities later. ## shinytest2 -`shinytest2` is an R package maintained by `Posit`. It is handy for R users since all tests can be done using R only (differently than Cypress). To set up it easily, you can run `shinytest2::use_shinytest2()`. It will create configuration files which you do not need to change for this tutorial. +`shinytest2` is an R package maintained by [Posit](https://posit.co/). It is handy for R users since all tests can be done using R only (differently than Cypress). To set up it easily, run `shinytest2::use_shinytest2()`. It will create configuration files which you do not need to change for this tutorial. Save the following code as `tests/testthat/test-set1.R`: @@ -171,6 +173,9 @@ test_that("Out3 time elapsed - set1", { Again, replace `set1` by `set2` in the code and save it as `tests/testthat/test-set2.R` as well. + +---- + # Package management During the development process, it is normal to use different packages/package versions. `renv` allow us to manage package versions and is used by `shiny.benchmark` by default. Run the following code to setup `renv` in our test application. @@ -182,6 +187,9 @@ remotes::install_github("Appsilon/shiny.benchmark") renv::snapshot(prompt = FALSE) ``` + +---- + # Simulating app versions In a regular project, you may use `git` to maintain the code. In this case, it is natural to have different app's versions in different branches/commits/releases. `shiny.benchmark` take advantage of these different `git` refs to run tests under different code versions. Add the following code to `.gitignore` to avoid problems with uncommitted files later: @@ -236,7 +244,9 @@ git commit -m "downgrading shiny" git checkout develop ``` -And we are all set! +Great! We are all set! + +---- # shiny.benchmark @@ -304,9 +314,9 @@ list(develop = list(structure(list(date = structure(c(1670530838, -6L)))) ``` -You can notice that both files are reported (`test-set1` and `test-set2`). Also, the result is a list of `data.frames` in which each entry correspond to a specific commit. +You can notice that both tests files are reported (`test-set1` and `test-set2`). Also, the result is a list of `data.frames` in which each entry correspond to a specific commit. -For now on we will use only `shinytest2`. However, everything is also applied for Cypress. +For now on we will use only `shinytest2`. However, everything is also applied for `Cypress`. ## Package management @@ -323,7 +333,7 @@ shinytest2_out <- benchmark( ## Handling multiple files -Sometimes it is not our interest to measure performance of all the tests we have. in order to select specific files you can use the argument `tests_pattern`. This argument accept either a vector of files (one for each item in commit list). Also, it is possible to search for a pattern in `tests` files. +Sometimes it is not our interest to measure performance of all the tests we have. In order to select specific files you can use the argument `tests_pattern`. This argument accept either a vector of files (one for each item in commit list). Also, it is possible to search for a pattern in `tests` files. ```r shinytest2_out <- benchmark( @@ -369,11 +379,11 @@ shinytest2_out <- benchmark( shinytest2_dir = testthat_dir, use_renv = FALSE, tests_pattern = "set1", - n_rep = 5 + n_rep = 10 ) ``` -Some methods are implemented to make it easy to explore the results: +Some methods are implemented to make it easy to explore the results. `summary` brings summarized statistics as mean, median, minimum and maximum while `plot` shows a plot with the average times for each `git` ref and test. Also it presents maximum and minimum range. ```r summary(shinytest2_out) @@ -407,4 +417,6 @@ plot(shinytest2_out) -## Creating a report +---- + +**Congratulations! You are now able to apply your knoledge to check the performance improvments in your own projects!** diff --git a/vignettes/tutorial/images/app.png b/vignettes/tutorial/images/app.png index ffa484f234cd1ab748620b6e9635ac9cdd82450e..d862262ff7e39feec60ae0d61a49313249d12423 100644 GIT binary patch literal 25032 zcmeFZbzIZ$_b@y}P*Fe$QIwLD78oTZN|$tvl9JBRrHFn&x*L()2Ag!3NDD}e5yF5G z(%lT6H-5g~`?;S#@B8)q`P)C+-aFU1PM>p~>p=Kxbp=uqS`rWlM5?6tN(%%cLV`d9 zXE%v~CoC>EA;8;p4_PIho4_mJrqw^d_lKTx2A2j({bXDdIAD5f|Opp(DBJ!pZ0$X@y$Xzruhsqj z68=20>K#Wks}rMMI=kPsvdLuhyKy^uhilbOsKjFRK2}nVF)Ktk)E=Nzxur>l~ zM^Dn+;rJ#}+-pYuc1d6Cq4uAeF3)cQ#jP_T@6X8KmX7UMQj(e87UREE#0FPe`GY`r z+=?{Ry2PoVBbT$B^cr0sZiQXKG>eFFg5KVgZXN6*{;~fO&2inB#qQYZwHij-E=d=Sem{WT+iX#F>Wa@O~Omw z4J2IGb;f0heQ*@!<{+!hTChuPZ5#0DjeY?cez*w6GetF?ywO$?rhkz_tILq1RpwF? zu{( z)m0?82@Fr;l3WEA&+4l1CH_IGvb@=SO7CMA5->=SBQSca(G*N=e?Ysa2eBdDTJh{w1T^#KRwAoXo8> zgdj{e(AAi)MVinhaH;6!>YB=?Xa73>(6Eb-B`g#IC!KmA6mGv+fqjU_RYy81c68bMm#vaCD=ieZ0t*P=Yu2l83dbsS_@_V}L zeu_hOJ%06&3s|wdzE9Z?5hj!K6bIT9PIziUESCyBs&rc8M%7k<5qiBU#kc7L8`5;N zV6}npA z^zmW=OIEydA{;fKv-o_~U-FBTQu%vvx>cP5+p)8w(c(S%iuWsnE(?k8*=5~7_o6+k zRH~#SpYFS*oXX2nH{`@BH|}o0ezkp+M<=o&{HLF{uS#Ny9BSAWSXkCjPA56Apo*s@hX+)dLI+Ebbc5GM+~oKU;I`XX zD!hh5_!f`vhStduy7=ONmS@_BtuR}k9PS`np6#Ufr=8ZfkbU%E6|s6^Gd9gu@5%ow z>HLhnLFfFFixAi&4>4CmNTL`o0Y7(rRb*Qe>U$U6y2Z=sDG1+bh^9!0uRl#N(mnsI%Z91@`ui282Y1N0 z{MeBnxP5~q);#vPADc}cr-hPaDg_aP8KR0$(paVUo5bZgc7qf1q9^8g z;oRVv(6*corT60wN~D$V0)9tBr`|Tl)Td4_NrQS-_xfLVN_B8{mE6#DHIp$h`#{Z5 zSGb+=LYEsVAuXBvjtdvheR4#!ST|hpv#&N?6q|2p`#qv4^y3qiWC-hKmyn-y;m{-e zYA{UA#6_R`PBk0*sP}xTxN`@M#8zGxImiBzA+%9XoNr(8?Y?SWx)h!Mn;FY#7rKtl z#~qy?(ZMji4ciQ=-}OJI444rtNsM*c%D*?YMtY<=w6=m@R9BAUK5K7T3FLkdEU6aB z0K3|bS??s=602#i_jz6tobhKgEa62zz8<2MDUHlyh#K(S3O~b<1WO^ex7zJjOZ1;W z^Q=p4bmFaga0lIjVg;|Bpk=<}G~MD@xDGD+o|frw_aq@;K01<@o~kBh{j?TOkUM<5 za3@Dd_+FE^=tN$pNCOq#YwY>wE+O&p+@az;p!8FQhp^*7)q_8S*98q6;k9{g(nXl3DAp;J?<<6&ck`LnrDNpMou;HAZOQ~PIpF`)Y zVDiS~u#?r2K?6XspU_7G#cwu=pv_h;^LuarmYyg%%kDJbBCvG& z$I2~8e7(vB%F15yuNi{`ESwRscKX@2TvQx!`y2ldUqDy-=BH7_LIL_(N-{}j$>i91 z{S4@O-naRYxyaF2Uzju)1q_xPa(i3u;=;t!%e~gi|R|y+C^yosLG{kf&}2%6(#^CeRaDg^&E1$1senAl={(q2LEeGz$ddExu5D$V8hjuXw;@8g5uhYxRe#ADEGI1qhb>+7d?}tGb&k zsqe!a`0)+py8dkWaf1vd(6Y0GnNizcDBL@A(3l%(OA1Npm5fRan{qx02?%^`yS-um z+m;;@8rm-o4RYplI$DcP1IW|N?IgDeZl)Ox65?>tbe9}EKg^@Ff@vHLZSBs41np^9 z_?kKEZOwN7T-Hm2zxGCY!qyx!CWBn;u;IK1SVg1*Z=)G|3;O|Gz@!Lb*jF$+_#VRO zMhAHNg}*@M;j!+KGm2QDQ^?Cf)v`uj$Y-;1^_r=jIC5K|f3=}va_in-zj6H7VzkXS z#i_FwsSuASeOwy`TosEQ5l4#cvS#1@Axhe5)V0!^PXP=a?`M^codm62mDg z2_&8~=alrEJ!N-lNo?M)k_a`mD7#r@&LzKk%?)-3o#qiPgrZiNLbFV%dP}fk!)F$? zF3*kL-%GzK%|0YBO^hki1sAEenareNs;f_^0uCz0u^cJ7Rz3LOue4^Cv9%?hch4D1 z9L%=Hc045QgDXs{P@}NJiKMAlKdIb1+exNx!8dST=nbwUj3ukFI#5SXg{ND6j$LsJ zbLlUor)ze7_RyUAMCUT7VKwKVN1FvUju>8!LEtVObylV|qj)b^T^d(Xzt=R{pBI}z z2TvDx%B-CBVDoP)R&&&6Sl#jzXrQk*TAXwiwM~5^cznGQ5r+#qqi7KFtEFatIETv- z%<@;9RnMh-O8rc+&PoEzrKy|QUn44A%3gmXdf~yTwBdt%DPC{x{Gk@R;R&oTeAmu6 z`;E$&p7BLWI{I9BHD4}3$8t|YnA=ty{<-CmTC;wGp3@eFt5#6g(rh7aYr+p2G|GW( z8mH*nkXPdHEL6$6rQ{VgIhd`tF*E9-*0WeLf8doBh=3&L|LEsE_??cAk|dFuE^{Dp zLFi*n)WP_2^((o((O-2=J2fq`q-~yqFNN}gTPPA;GFTDg@!tBc54Pwv!m^QHMY;t1 zo~W1|!Eo}c*MF~Nt@>2RUu2p3O3+UfP{V^;w4?k>797m}3~gJTzKbRv*k|V|Rp|wS zsuy}I1AmeXpHJCXm1ul^@-S#RFFUS7VKWF^GCrt**n1UqUag?#w(H>#RN-zT>Ao{k ze16%aQe^ft#l^whMEmr&8@MGsAZVIF*E3v>WvVEmzh>NuGk5^$%bUF*@lCpemG!3a)p=6lz`QdWc+`KLBx4VlAIL!qFnxSSCy%5vmUdV=5F05mjM!!2txbwh4+4gP}tf6MGRIGDYHtvtnR2j zzCW$X5XsOF8Ia1sE2n&P`3T-^z;T-OSzmIc`f`de4emw3r>bUd5AmE_uAy|w6!~n| zR@_gtHP(LP+w&e3=8-69Z3Pa#pbM~{b1-gL9jy({3QQLKaR$jE+f2PgRi9t%*K#Md zXr~>> z@>srvwxnW*6}93tz^KvgOV7IZ{z3Ua7_=`p@36DCB`pMA#MP z_ObHZ8La6>O=6a6sN%U>%nDYpaFe`K8X8kMoPWI#wNw|ivAyrapJlVp(!oVPj(Vge zSyi|xSr_e315_=UnV`*?(@>`W)TeQ?M&%dHTGZ9~E5*dfpDxo(tQSxj=37b{&0o@6 z%%mc-jycos#H%RVUdxqBI@L~zXo$GzT6cf0GLu!XyY*E61j+jS(-V}$N=D&tv3F4} z`f!&OJOMOFl-o8u!rr0ENnor4kt2ySyKMjLNOZ(|51#0lu>7eO z=Y+5UBQ)BzD4!R>*4JvAMXR&k$%AV<_m_tu%J&l#b4!E4J6B5R@FklgJ+&QBdKpNsHc9lIu~hQn2-+o@#y$Nk0dS~ z4xsEzA<3X%qMLeTg#JPi?73iXDkHe^cK@iIB$!(FXE;|}>TqJBz2-b;knPU$?xxFV zn~LUvS}pBmi4t1>>d5)JW5zvci&VlM{4FlRapJR{5;B=*OJJou$+wdBk9YVo!+C~wMYD%o=?>Eh2H@)4s4cU=y6cN!{f)MtFFeQC@n^!6I2uUz7*I~uI>_ITQ& zV~ZStJNS00Oh|GeQ9KKX;4HAul;_`rTe-u|tdZ8r=cLKNW)+Uvp|BO5!Bf9yDaX)r)s#ktO4zczYWRn|KL=8B zD0Uop5c!2O8>KnjN4@K0@QG+JFJk*(ygWK$Te^sAu(W_LAtj#zCBZt`nCL;~KyB!@ zGs*d6`a|K^P*H%r0F!n3VSvq()}Lw00V$__3FiaJTJsa5v*K|EU0gi6{p&m2^lUc! z{yf=Y6BTxal*;*?W{`Em5f|Lhku%R_Q(wHU7H}ME_D)J2{cdfmvJ>%HdnevFOm`fR z33EfeS`%yaWY839=nOeeX+f&`0lXcaAHSgVGhKgGr-I!dll9jmLppeX#zVjhw7(x% z>ON-M+@R}7ubcU)mt_8*%KRdGoeIbrxsrz#WrJQ*O1h1%Gbi#EGoJY`GLvCfWB z(`xqZZd?DwG+#f3K$I>5cStc_Bm0OqSHt*r*1z2I`n1Bx3x6*eNESvS1lISYgdrWfp{8O8l|2HnxU+dei#! zIqOB`3%>X@tV5ha2MjtkdYNGmF#m&<%6}N!fLQ!IWx_fvIl?m-pD=Vk|4B^1;Arv1 zs|G*5D2{-|_mdi<@ZZ%eb+ni3iIc<_4<+)yIvwZeyN-$X#@;6JV^RAE4iIFTZZ352 zW=XoPbMo8Xeng_&JHNgTaHCP64xylJ$x1ICGR{p8RB2q&aoiX*S%~9%4LdzL9X+#w z=Nx9m$_^PGDe3zL$wWH{L1626T9)z_Xa(uQ{*XnpoC)v~(MX^O1W_a9^!>3KBDh`$OQrd@dOX z*Cqq!H*PO5Et!L-)qiX7q>9f#wvm2luNLEA#ZtXbcU8m*m4u7E%S5ETnJcBozb)%h zioo71fR`uOeEPXr^Gwb~p4zw&K7QoL4_1Q6?%aqm#qPE1^vvhrRP<`aYdU}48FI|= zDm}a2HPe5 zWr<4e+2`p#-F4ZFbgaWL0)4PrmvG@7(>SFSo=U0hSjO1<@by`mrdk0zPevy6;YBC` zxXI`Brx`~b}vW@rc7p7f>c?yDb_c^86#pK}aSy@a->wC(-RV&P^R ziib`jCU&nESJwAdJH*%>hJT|@N4*^qg5#z*_rDAFfZzKg6ih^D$P1@Y^`nQ*8PWK! zT3x)QP1WcRTj1&3?ktFZFx}-I>`-|`^C0Vy>4KFOggND7!rE7gmiHP(V{#gL#pT>5 z-skc;r(4{I>&UN{!zfMtc*mw#`#l}e`^eC*3{CJ}{YTLC4WwL3QhsXZ7K=+|^~9v! z9}DTfCP-|^RGVBP3HGxn%J^0^`9I}Zic3Q)qg&^pYgg}+0(Xw6Cd+&ljpX* zcO1*r!M5Vmxsytx)}1n%P5yLrzt1LYLzyTCg6}Hxm4F{hb0#E2sYS#0US$wFcO@{L zdCHc>@zVL{y~Gtzb7JcUQnvk)xEo`*0_R8Nl=;e64mXQR09a2ws)Yg~p2 zNq?ia{>~H=vNpWOJ54W2 zrmzm-`q}%scbI9w=W39hv*5>liEdQ1xDN*DB-nV&jU#`L?eP9rFi%o1iFY6 zb~@V~Ar9cFV$c8R_CNYmITbG?21nMUk9rFhzT!;u-eSk(cJZ=k4EP@5l|Nl0qA=#G zAlq%x$tcnBCLZIr@>D)9>MmeVr-?FS?{8t%}`6~F`B zYWxNwRpQr|Idx4NzgsTtY`W;#O5_#_<^laTJq+p-x@sJsEruLOp!R^*H35eh+-n^= zGu8!&(@5Le#%`iw_^VZc)1uYU9rY+L>>b3~>Ng5!9Xh@nXxv0maHF4568of1aE+3r zSr^!^SuJll<-tP23vd%gROHX3E~0alfUaRd(fbp6XX%@`&9vi2vCdSFksOxfDR-&; z`|kb)-)c>@GX=CKI_~+iFVz_0Q5(bZW({>4yDP}&d>rSe+oz^qgo<+}0_*=M?mTkx zJNU@rt8+5KhyQ3JtuI~>%P7V&cp#r36_NjwU zE-^3%?%Z)S8C}EVvn{FQ0JM{RaFgezbjlb4SDDwDOweId`k7nB-F9(lrl|{kp>92Q z{aYV0bk*~ep$ob%!X!q?1Z|`nKG%5OGrXyInyVKl*!(fhNtcaUH_YI%E-zA-G`Fo#e%=T0<=&R3r#aCSSmRxZm4wS> zG+Y|Obvc&$1_p8M1((|WqffnM`g$vy{s2O&9%Z(>zU_c*Hp16nICYIE^h5)XzJiG} zI~K(cgA$t0zUVX8;M{p)9H&szYzxvIQs_o3YM1X6Eh1DhgtBzMNIdPATp8JHxeec) zeSwy6`fiT-0buYozU=zqy6#dBe@h6}G_l(T(S z@r|;CEe*xL`incaM3rmf7k7Gri<=Ya(c;a5FYC~-v8RT3Q%Sh!5VCVc{Q&z|KVUN6 zHbdhg3*nT>(u32-r@crrTN&v`$ zd)YF7H&#f`q^qUW7UpVlyp7N@`8m)+Z zww?o!lp2N?oZZ3C0`nH+`u?_EZbD*WFuLbn-Q^`wk1-!zP4vCDW^|R5Zrx@8fx2)1 zuQiNDnm3XERhB$c?bRsE)$2jn|0b~a4*xy`g=khgj#9=n zo564`e98Ehd>hRyFY68XP+7tOCpP^ZO{y1_l!tPW)ZQPTVr_8t9SL@TGWw}M_=;m1 zI{S-di;k8Q2++;a5Qf>L92{%h0Yp+H>~;AQ{i8q_za*;@&XcSIfrq+?AP%1%4#qwb z=B`bS&^=Wy@-sUsxOkpBbVLr@C@~9lNs&IR_#`b+;NbOo(Ks&#e>J=lWy4RY>m=J>up?b=)9oJlU3ov3b4Za!8={&)xE*j9L{ z^JU4>7*o|WX)m9`&hdv$1GK$=i(d0rLyf8`t)fBQ!VBiy8{`zb7vp+6OD#{$O@vWh z2bFnok8rzEP5vjLDLfs+!9ncUIh(AY7L_h#1*qE(Lst>RhrTN#r&p0b8sr8#3RX(P zI;!~PJd#Eormc^p^gxgyFZH@q)sC1xD&EYp$+{?3JIf3TW4qr{wC9@u&q0h1eKS*2 z=#%^U5mNS!8l#b~{pH|}((;0_H|6kawQ5`d_q&n-^vyAPlia1K2hY^KxgGR}%%Sl8(a z%CHAG385|xzI5yeEWJTVdr@f6)+A-PTfE<)j9)k}W-D}9;4xZhlR0$KLfkd3Vz;N3 zo+^Ads$0-OuM+M;*>ia;41eqBA%8XN^mOmcHkx?GPXC;SPF(FR;3(h79?nQ-Jl4fF z(2*2mb4lITbNr+soC|jUv7|OoTcO_1M~c|N6C?%eFk?M#==znLM7eAwHubZ|XbkUj z<~aDt2D)Sqw+8C(9216_Nf%4G6r>N1_6>f&0pJ!-0}OB}{gl})a9L<|wr;dV(hxSn zfJP+Z1p%(mJqs5T(V_Ov|G+l-?GKdPRnEmd%hj7nA4|n5N&6fF6I$pQF^cfwk`hPe zu8OC-i-7N=lRmAF_2pfc&OgH)Ko1#RfUIJmI z`{v;w0Qd9z#lzT#|I%M2(dzcN1LuZcKO*67js`wY@_+=)N=^>CdH;>NLXn8vZh>-A=GzX-8t$-@mT;t*5;z|W&M65j)03OxW{bEb=?=k0^$a zpK}XLfIyR94jJ0KdJ>l=I|lGeClw&N6*G4W>GA62%TPDnu~r)gCnwf#Gaz77>hUWL zgs6tUda}B?$u>9%0xW@+mbUjyYW!-5-rN_kqIWVOVULNN0D%CXe))wKaD973ODjrA zW{w7IFlFOv&sN50+|S3x@wQGSo14ybl72b>B46Lf8XZ+_2CbTZM4xBS0jSVuv~Tdz zRc#E4HH18EIvL)Z0}KLIP{4|co`cm2zZMsIQt~o)e!b+OO->zANWp@oHBfwl#y)Zt zRPbf7S65dr^-GDjRaw+GG}Ngw5S3O{B~gkHE9KN#H~qb6O68k&FR!WsPrZE6`|DRM z9D&&T8hEpdY>%iMz{w&T?IU8zxnEr8FEeDJnPYuxPPMQCE*pegG&p~B&acliAa9d zN_i%goLsrhKbxBaCl%Eoflm=Y6&Uh8ZlL^TsmCXrsF*(yxDA?|f7bu)OJ1zoJezmH z;3S3V)u~>_La`>6rLR}5w6qjZ#sCcG15C6hO9{%2_)!T6dz~<)3{GC_&V)?v0NsYx z)5_{<_bN~N?kcY0TS$?6x??i#VsGER?M_Q#(M$ep>jW?b0&%I(m}`AYly+Ysu?Mip zCV^w`lp2!lYI}Rv@R~JeQa+LbrOCxy5qZ8K8f|23{IvDw%N4F-HR+G+wKL}bOZ_pw z#4iil|CY2e&dim(cM3mYu45h*=jOh2=T*86ml%HtmklxZZZ|5{22!R|@Kil+>IB`X{lo;CsEPYU`=P_oT9?9{rz4zdW^dB(Ng;>j3Wh&DIcIU0re4+15g$ z(iKb^-f!jZ+3sSWSnwS_>&sQU_iMUel_bAa28gL-kRfc%#1owW5uG06Vptay{sE+V z742R_|F)$J6Up^ei*EOmF-ST7d}dQGdjf=6L8ilXE`&!EPE_1`6EY@c`F@QWh*OO> z7eb;E*KF;Kjf^5m5GexBf6&v?4%c}sy9=~R`R#Myk?$m4bO2hH+3Y#sDimob_iM7L znNSQ~s(WBgPD*NbcDP|!=a)X~_YercTzzP2m(vnQ-s@wZVQ{z>z>_-hcr@;pz;zjU zCp}~K)VU&Qyxofpvz{6z${3KM(o8^|xX^v?y8~lantOb(t8464 z56YZcbt2gKGZ2LHCf`}f@)D0%JGq>qM}n*DP>IYy07C--N>G}X(b#>5dn1lnVSlHK zVcFy@539&RXlSU|!3+Rnh`Gwqt!zD2T!kaLy*xZTzSKr4Jx621!|~c%Jb?%MHcx8R zmGq;R++!Hb1)UCiSKabOyuw~jPYn-$jgh_(wG|Gin_-{>=I{1v2a=#b3qqz^G&(^$ zO}~Ddn`Y{LiJYOKp*nT0lNSMPTNS;o;qtH{yOM9q_IF5KWgAyw)U;z=<=p32Pp%Tf zAwNJ3h!K_b+?#jmr9qM`r$h!%3LF(fFIjIl2Yqn^v!ZvSj-s^yNPfWP1~S08ZxoHXyoDyib?&pp57b{#J+CfE;skD=EZk!585E$@ZpIH+o<5n zj0u5!^U$^2Tqa$8{bfiJv_7VEMf%MGN|EKwxqCu3cj6*T-2lI2I)FfcS_ zGb}6&C=5Ig*A4U99pFfUlCHyzsS3%fB*eV<{ah#PH?0+Gp?vjxT$=GH(oF0A=VU{0@%x$16ASo!T3 zs;Jz~J>X1xYznxD(^~=~(;JvNBkTU_k^E6Tj~e%t=u5Jlro;n|zr|T-uu$}bPap|W zM{wzSOaOx0PM)mmxG%$g8$hsnv+%4jG4tFbFPd~$pmqRlIz~as|6R4vdE0Ty=1C_M z4yf!M;SGQ{xirZ1-(siG<8jyc`!B$s_T4o?CBXDGo<~<$Jan8P!T@o?{_R`-LXx*t zsUi0Y1$;xhl(M@#0PkV8eH#7hiJ0&FJr1G$t9CAb0<1#@2r-@2jon0SH5b2&v5=YT z7$7H+l9F~-3mT9CWLN=Y)K+o`8UeuraF*?Zc?fx}CJf{s(dyQ?dT zEagmcKYZ^&0BOJgn_A}FzuI3A8roGLmu5Zxa4Kr@&sA2CkrJ#rUSFT6n4p)P;fI!Y zM!glxplO5hn!xE`G5H4g+n#tc;+Da#z53e|Jf zPai)Dn1&<+E0Gr0ARpQfu3Qg3t0~LI#LI1BZ9-e_5q%RsQ1!Y!GWTzeD-|g5LtbG2hy_B!D0XXaVrf zB>vh1$Z8BTi~RYAsIe2 z0P7&(DS6e%Bhk0_%EbWNj&ykFezLn5X{`)o`hRXch(nCeQu9)>+Y6PQGJyVO4OL(d zAY}ro7gK$X{ApWAeJU26P`s)}u7oNW0+7xp zF!70la(vc0#%Ze4MC{G53*{BVs^*huSYcRE*pJFlqIyc+}GW8q4@VpcVgZ$tJblBVr?BnM07tv35qFN(oR zy1#YaICmig&&K+B5U}rZ$vfQN^Kr)q^njuU0sG>3#inbl^5#S^(QZSw`Cny)5EO3$ zxWl^haXt})KdrSIVh>EkfHDz)mpM$MihRSA;H9WquM7gH*E#t%4Z65I++=HTV^(?O z>KO=j}y zAW&!AsMEBN+NeZa4r`!P*Lr3))OCX>@~4T>Helw-UXXu0kQOuX*$M!ttJ^Qe`C-L`BGd)OgipKlbL8%G(CL>CFFSApUD;m9dpEb-*-hRN8 z@LvDZq`m_n5BsDs8FP$h^Q5=>AZP92HF+OSyMnR%dEzY8jas|J&aI2Zk3g-z$HD#2 z;@y88ZDwYxT@kdcV>D|^^NP;itdvN~xdj^&2=o~uRcoM<_A_FVCs&)y`Py++jWVzJ zu4X#aHYSE?R1tzwGXQEvnVo*0XP^AY4|U)({~&GvD^=-7RJD4F0^vC|A#v&5JCl)v zV&CO`mcIZshOk8gB_dQ}Bf(?UtUTZrfOimSC-rqKYb4?J71J}x_xBC0;8#chOAd$x zR>Ppn`}yy$(6_S?1u7A6d{Qxb>^T7bnEPrlMQJHIG{=|45$6Z!qW*#u^=D? z&F=o3TTAj>SkCPtCqY`+#1F8kkx2Qr}~nMj|#cQ6;SBUhVYx69BRmLUVO zosL#IG4aw4RK@<{mmBH%tJW^qznb;rmwSq>s9YuowbrrFjo94LYsnA$^fLOK3ex_Q zg$ScWt~!Hqnks&$G8}S+i0bE@SV4Z)h&b{cyO#JOaRhx)qE|5CGLG&Rs1?Ej{_`Nz zP1yv~Bo;#hX^&(n9c6>Ax;N$vUVM5EB*4G_7og+Q%j$;&3<*MR*FdSE`Mwed2%gXP zuAqs?^z6mJbQpeyD$-g4Q1sAI;HU&U%q<+3ANbLQ=jTnNg?v0<+hilLo>F5>Q;{Jt zJ9L0K@sYkAiB0Tktnx5&C5@DA%lMf8{zj%*?*JX3z|_#hGED2a3#~n^%Izxz;z$R- z*IF@^@92Qaxh#HZTXt3^WI*hC7Q|vQJc91z4fUSS7hbyTkyS zUBP`(RHD&*9U(~7M7bR_k~n&Ece3eX@V4m<{;&BJW%2Z}s)Io-H1-5;r_Fq4_=}?L z%dU5)K`oasb*T$HZg~ogPFkh|spW7a4PcHCh?WiuH%&ki;jOtW1`Q=anDF~{Vj>!je?YJrBz^<0%zt>kDBa9z;n{Kl| zYZL3$LU`~<9M*vRJfU?$KWI|J{|HoG@Drun8+K1?-pXkjypG`~1R?~LT+E(w z1J;65>wXw7xO>LAX7TK3%erCObOGyaC;f54X)1YbZEd)^RGbt<5P!dKL{`0UxxicZ z7Se)P>-$Ho4`bR;D3qG=0sHipj7jyBQ(a3-jBmEdFpkmx&yVKjX1y8$5F3Y6o_40$yQ9MYVLYQ>{vT2hv*JT5N4NNEZ?;Zco6Y$1U=|L3C6l*$dR$vcc0I?Vj{8MMq3Y&MCJ$*y=O_Y zv{c}4>Pp)Bsep+33@^2r9@6)4z-r7QP;Bj>UPw*b+1dFz&>Ez!t*?(TV*xz(4vXYJ zkDs=&?gzR&f0SvU%;U>*cjK(N1mp1TK=ETvON!_$WWF(+i&lV%N%%su(EHJm$Oq5Ht(A?_c-f zFA1%3=xy`sj2N#5Xfm{bo=-G2mQaRn9cir!w1Gt_U-Gir$>5qljVPb0mtxk+_}-CE zItp7inJP@{gJOS>Y4I|ZmTlLa0pg@g(c6yyt_CuH-2VAd7zVUmk$;+c-&w$9pqAMD z{_uUrFJ8^snSINHIelIYbY}t}&`p+F;#rn>z4-)jDv-~#U(?}NHBiWHgoQ%mEG-)^ zOa=Mz(-V+%#e*MpPQs6X#upWicd73o29y~Ub}JQ`FZa68;U-FLoPbebc)(~$;c6!6 z)wrYZ;>Crj4=Kpi2F$H|b#ET>U}~d!-Rn7kq4g&qK9EmRo@{L;U=v;q)8i=wpw@q` zbfGk@40e2JAs)XO2$~tZ{fNLkV8;4KfsYt72()Fx^iLVkzDs@de0tX9^6?qamM|xo ztmaW0NkkwJ>6;epEyI{(@cSzxFAV}C87}Lpoy^Yt&zgg8RyH6rOujUkqNMjL0K)P~o3AK+Q4S2JLZ#Y@QFS{EcGErDlxdIO zp+5i|_`4I24?H*mCKd)Mfvx$)v~SrAI+YnDHZ(NYH=sT-zXZm-fO{Jdh!4IKxwhzT z*c^m6a(duz2dEll){+c7vF>h}aP%|*AcU()n#OSEsXe3A@mf&sbGgN2{* z1LGUBZ#-WTj20#LMDVVSyq&84Jw2^UW4cyP{C5l0-@uK)cnT=bb*o)MLTc$fkPZq1 z&JIiKw@(2_{q6vQ>l@?4JpL8Y0^>J8IU7?VCS$h1TTQtEcV}~C=s}=aJ@V0&JBfbx z_$+*7UvR<4q~Co@^$_sy4z^cReww8XNhn@?44Dn2Xqk#8t}9759Uq69OgPtAnh}F! z8W*yX2(MVyeK1hrCDgr$_FSq$0)n?YUIql8sXrn#;BS>|f6L$t@hxmowNsyc744aT z&Bo>?GJ=ND%S8XQIi}WyV795ziv9v9B|x)s*yg}TXB0Pwgw()u2Y7r_7#T?PUGvt4 z7w$>6aiyEiP1k4P%lX$ppB&N)A!cSVfe&3KoojaH#6cj`m100QjCoUZ8y}At0{Vmk zPh2U)r$9@Vb&$>+CLUmm;*6^w?EtUTmGNnfXa&JMD_pLTnt3nJ@y(uH8|6| zbd5G-GQZ#2{-#J8ZyQ+kw}*>$mHl53Owd^d;bqQg z3&bj@4S;6BN^MoAJ*hxBFK1Xv6k_Kv<+ZNjlYGURDj&}n5I2v%Rq7t+lM#i~odA*Q z|LN?@Tb zVVk9xvNKxDzT?5OPV}a2C#xUgiQmAVioJ;h*K{i%2O~Ko%!FNil$jBcSRY$ss#l{J za#k#N{Y_luDM?*LH`9rneF$WAqOs&NDCP6HO~B^QaM!A}=y$ldVJV-ciTORu$|MGQ zO6oN46=2FaX`>poKKzS#RaJ|0xe=%f%dI3vz#qpX-$Ch{;b(IG`_}#|dxP;keMnQ& zao@^bBc5?CRd7?2c~y^L`xk zCNAKKi%k@bUBNaIYEucTW~cive6$qPKfrR!2LB|Dv@2L1&2tkO^+7M`>PjiZg7@uH zyFI&U-<}*}^R*FTSq{46_Y`E2&JN7d_+w#8lgpJRkdkNcJokKyFi~9bQiI5uWFqS^ zjj)m*w_$^fvF(^kW8)gQOOf(z$Q87}J&q2SLrA5>wB7!s_$+bRXy3d~U?iozgH=gu z6TsVCNp^O^8PB`uWsBm*8=Rmyn;9||uBeDRSRPlr7qQ15uR*)oe$zmwzlw4lJqa@~ zs0#5@P{$T0Wke^PX;cGuT1@K8j)u7nFD$CCL~3;AD2&pM5$pnz?|-&~*}da;^f>XV zO4J@EVd6m@qk=#*Ek#o35dez&9`sTQxh-}TlT=Um>5WPP*B}D*TSzL;RM7bjo3sxN zqf|=bxRgq#;<(C6Sg4fi9o8E@>Ex3UqXME<{RYzDo6{dH)T#o}nk~!A%gI5}Fp8Ay zAZD*FFxU`67ekM7mqUA|lIRbq&oN0&BG9X>8cVjhG=PfD#_Q@0RD%P{dQ3=XKVZd|1u6Io0fLV+&#<0nu<-!wKtY#MlE74$B<|$F!k8zf#Ub(*66>$8si$W75 zr?5~DR^N0nyZhpVs8Iyw7uGnX>!x{&mGZd*<#E{f=E>Ti0yteRe{^>NJ!VBya_%r> zTI3|lC*@4~L-?75)Xu}>RTty_-;lyr-VY1MdIQmU{=WC3NlyYB67`(Cbf;qIRFD65d|VMb>Hb~Jo*jIM zWnlcTb&XdCL@#wn;9htm$}DaJHj$H}Xr-(2M(&)4#c;(v%|af_0V2zN5(=e|xo-vP ztw{dFoEJ8M&;sS?`Kc!YTYgeF+6J zqD0@Uo?YDx+Y#2%4Q2UEm@)4Uk((zR&04eUkPtz1|IW?gbUra&B9HrWtqIbhOt*mq zbJ|$xJ|m^mHpsI(m7FboYw$v@}jSPv#Mzb$KTr%EX&WkVKwCfR>+x3-%{T! ztjJ=a>X%hlNA(XE4a^;8*)O3G3_!oW9I&I@D^#9ovEM|bt|2=R$CHVDr_~915Jc?6 z`dcLf9-f}g85#}ah5jMYk@Y}GaY<4$r7=V{dEcgod>pknx8uq0M&?I!>c+9A{xk8% zEd?a18ulROV;dVx9sqKqNM%W$X{REvfb@?&sn?5N>pgle)mfoUpmWu+hcK@+!6u zWQ~&+IKbFAJ{tnwHyTBemH0A-`Q+-K^lEc+vruU)JbL&QfKF|;MLHxl(g&(;XKwvw z;dU3s@ub}!H<%e$MFlsTXe(YR8NliCSvoB-6vV`aaRWFL?uM#KOW3E84%UgfEk;r^ ztK$j}ETe*P7?%aKFn7zz{4;_q}*GGq8uI^9E=Ru zZ9)7ZOB2mspFL~#><`@9QR&NmvzUbExx6}~Ub)(WUBNd#*_{iIP(j?eaAxcRZHhpP;T7iWp_i_WTnnv<^(4~i}BX6{>gXQ;Nu@7WCz}tAEhm~)jWQKrbn!G`ubk}Up_{K9UcL!Ml%;Wd-`i0?d{N4lS-cQ$ zH5>tEUBnpyu6|_u87Dz{oIMl3OEmRW^}IN}QU5w8WsvM9NPYZvVD6Ym z3hYf^?npA^=4sDY@Qv7Qtww`(?PwDHn?Xjyy?t4VzJ-vU#ZR=A$Iag3N4`qtZ{zf} z3mO)CU5d4)chX>!Ec=tgF`oumRarb`d?*7QQrgjpYZJzLe-m*6d()8?tkY67D|rbB z`0epR&&XO>j);z7Q(Kd%`ZDiu)b0#ar1(}vHfyBP*5erL(L=*bqu9UbqkdIU$(}v( zi*W3#=RCxn^OD{pvVPgB*&%F)rM5Qjg1*5TDRY8+GfC=)zXeiCY+xmm=(8!>8DL#m zqa&=xx#mfDJ+4nNP#|E00BQ@c3BCPVl+wSXwe>cBPDU*F?E)=nQD{DV?+?Ts7rv}~ zJNwpC>|N0`dK$RYEfWR5@YFhZ-#Sx4aw+gZ8G1+9hEiGCh3;?$*2loPs|e-$UxIBW z?fwgvc2uJ&QQF}LV5@p2Zf9yybyp3&grU^49d_aWDZm9c3Pn@&KKwZhbDu(9NNGH*qTpOcZ${d(R@mTt+ zRL}V$`vUR1rUdL~*gpDBHMr#kmv%r|(5z}d2x1)?H8k$lqK32G26|bVn?kRF)+w}! zl>!vE=CQf7wkwz>sr~{~m+=zR8!Ejw-D=?dwUJD4B(8YdU!nOeb#$7S-60_xB|@`Y zlw@!jaPh*{EfGoX8P}qro6aS#beq8ZO8GK#64f=8T3{?PV*hOzpnKlq?%hLzPIv6C zHLt05l=#u(9@rATztoV7O^?1*Kt1=Ur+DZU>=B5Ve;l>)W1(1U1?Xm6`AXfrGNr7; zpnUX-8=5S}e-p_J>&cuDK8gB0IW;BaUHenBe&V!{Px_b;`zIbEP2%n|CjBSB7E=~B zat8(mdR6l=Wj7cQ8g77l(Ro~O>0h%??;R+ z>wV0ZY8^TZ=~%@94bf88D7^izsqJ?p7;LSr{6MP5nFs+?-Pl@AS^R3F z%{6l@;Li`X^7AfF_17--9th;+)rCA+?I~@l?cwoCXJY)R5S1Syz=_H4-SYN`Q0ZRX zJj(vEVkdW60Bg{ zTofyRq#EmH(9ZU1%}&5#&=Kjy&G{OFL(rDjY_m*e+|OpEnz*1Xx&vqm;vjZFg#Xzj z5)>IA%eKC?I?}weHL~-3E6`z^UbCGC)k-C2hmLlLd?32s_!B#us#6{)5Ssr88yutT zP=3a&k4Njh=69@_pgII8)NF6KuD9;|Ze`4L)^6``hnF2l*GfC^?!Hyv&K7WEnjxs2 zJDW8-+TgMJ-7bQyThLWB30DX3HFfEF4}e%+1K;PZuM>{`;G5Ok{=L+ycY0mDrXDn; zilLjc&5z^A-_ABa87hOe+2IPjOM7FLipPUa)mY@&6E)S|89RYFgwSoHe z0dMaS?u3{972ZL|?!j9enkQ9CT5N96Q8Tlu4%j@ta@E#$ZlqJdAfn&!aao|c;}aMAxBkse zg+2)!-Ty+eH9t~AAkn2(`{W6jKjQ~%|1RrSF@%zXnD@-#YI=E?rhmR3t*{9YQln5a z%+qR}B=xA*kX_}`A-fhkNp<#vSHn*3NK_-C#xee=75uh*c)L9 z28g>md2*33x%WP-XcD*-LxcU~E-yRIp0gR%nL2h#kaxI|qX?8mJF1y(PN4kdx?PCX z@#4k7Oxl8#Pr^cXW>4BHN_Fe9s1Yeoo= zbmlUzif5oq2U41F(XMsT#g1!R<9gZ+%ImU|1x78{&-L@!Hq!P-S5*!owx7G2JnMaX z^`Zt$G;a#)6~}C7%4$P;=k6qRvAU@BV72TaGIdRtqGRe-@}-P}Cu=KkXW?4l%~mTt zWc1yXZrbK;VLe*9QAgBtP*eaD8%@R~H9ucY=1V`_Rrmfk&H(a!fM6O(iNW--xmfI|sgWDu|zJ*)^&0r8E*r@ri11 z)&&~b$6i3gT<2VJepfI1#`?%I3OVJZqwWx%;RSSTwB;dnNt_(Iezb4wf>~#d2(pjj zb2AVNxC1ei1r*!=TtZR*JCZQFU|*luGsA{l;fwllG%dLrLiPc0Z?U{?6Q2{e4O|^X zE2qx54fyRBM|QakSi`aD|7Hn!NbQs)vj$33PAPz#6!>lz;we>`MiMaiGtVR6>5gCQ z`S%GNr}Zz8Cr*Q!`Nlf0Pzid{0^xb$P9(|>UUr;1;|99>)j#TSC4ymE4=tn)244qZ}@_SueNKUk-q!_FsOt7xBtd`JGacbQfxx_bo3L=SQ5}vtU_YMvdZ1 z49jS?nFCe$Z7fN+_?lvft7r(??S0bu>-SBibUL%)Wy&nq!VA6VLpS$SfS#w%A$BUO zAHKD%Uou)gZGOks+I6&1e2You=x?B@E!}yU|$9q4DwUxzJAF zQBb_7{uQ9Nl4&~ZR4AjauFfhz`Thqb`-}7joeC2MckM>j5}>LEfMrL08X#Nr)+S$S zKu5GRl!wPQz16Qu(T=)X<*Va&59(!3VxdpsCy*@`c%QP<)WTSqYt>Q`MmH)CRCj{O zu|7DIvK#X(2eT_OT6H|x41|Wp+tMBH-7YE;imvPM*w%;dCMh}h`jJQG57vnPS0|9J zNM%nT{i*8}p=cpp96*zf{q5fZ_5!jMvAdmgD$aIZl;@r7%(f`cV+t-(4ZW8rbSnrd za9~AK_S%-w8q#7(_2Tw&{Hiv3BGgZXYA%d8`U|vgJ2eeh{h8G(zKn&hjxD%`Q|$#^ zYyO|L`{?~h%=Me&B^m84rL(^0KorSVzT-IWUGeDv1<;&CB0vzoiuJ7+W0kXC7a1Ef+)&l@>WjVHiPn_-1+n8 zXLcdV7`>>z&=Ci}!Q+u1BEOhp+&SHYGmhMn33))Pi);$2imNerg#8@*Qtfx&xC1xs zqU=qiOqhDVsU}ciaJx{ELzo&OlD<{%P*Gm)m#`}rnh?VTllGg)N~bpI3D@t=X@wEN zBt$=sfmw{-kc9+>d=4D|GK`V|d|on2W$=0YH>3Lyni+t@Gk*RbPWZXQ#K!mKm*+#v Qc*Y25BlCYq296>B15xox&j0`b literal 33130 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,oWJe~9hbxB!BaU};hKDFkPfc3) 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 Date: Fri, 9 Dec 2022 13:41:04 +0100 Subject: [PATCH 213/225] Fixing spelling --- DESCRIPTION | 2 +- shiny.benchmark.Rproj | 2 +- vignettes/tutorial/how-to-measure-apps-performance.Rmd | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 0e1ac0d..5d8d10f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -14,7 +14,7 @@ SystemRequirements: yarn 1.22.17 or higher, Node 12 or higher Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.2.2 +RoxygenNote: 7.2.3 VignetteBuilder: knitr Depends: R (>= 3.1.0) diff --git a/shiny.benchmark.Rproj b/shiny.benchmark.Rproj index 69fafd4..6ff5a50 100644 --- a/shiny.benchmark.Rproj +++ b/shiny.benchmark.Rproj @@ -19,4 +19,4 @@ LineEndingConversion: Posix BuildType: Package PackageUseDevtools: Yes PackageInstallArgs: --no-multiarch --with-keep.source -PackageRoxygenize: rd,collate,namespace +PackageRoxygenize: rd,collate,namespace,vignette diff --git a/vignettes/tutorial/how-to-measure-apps-performance.Rmd b/vignettes/tutorial/how-to-measure-apps-performance.Rmd index 5b26b2e..44ea100 100644 --- a/vignettes/tutorial/how-to-measure-apps-performance.Rmd +++ b/vignettes/tutorial/how-to-measure-apps-performance.Rmd @@ -30,7 +30,7 @@ remotes::install_github("Appsilon/shiny.benchmark") Let's start creating an application that will serve us as a guide through the `shiny.benchmark` functionalities. -Save the following code as `ui.R`. It is a simple UI containing three columns with one action button in each. Also each column has an output which will be created in the server file later. +Save the following code as `ui.R`. It is a simple user interface containing three columns with one action button in each. Also each column has an output which will be created in the server file later. ```r function() { @@ -223,7 +223,7 @@ git add server.R git commit -m "improving performance" ``` -To play with `renv` let's downgrade `shiny` version and snapshoot it: +To play with `renv` let's downgrade `shiny` version and snapshot it: ```git git checkout -b feature2 @@ -419,4 +419,4 @@ plot(shinytest2_out) ---- -**Congratulations! You are now able to apply your knoledge to check the performance improvments in your own projects!** +**Congratulations! You are now able to apply your knowledge to check the performance improvements in your own projects!** From 68848de8013741a0a1f6f50f7e1ff25230a46d54 Mon Sep 17 00:00:00 2001 From: douglas Date: Fri, 9 Dec 2022 14:13:50 +0100 Subject: [PATCH 214/225] Small fixes --- .gitignore | 1 + DESCRIPTION | 1 + vignettes/.gitignore | 2 ++ vignettes/tutorial/how-to-measure-apps-performance.Rmd | 4 +++- 4 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 vignettes/.gitignore diff --git a/.gitignore b/.gitignore index c7b3280..7b46709 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ vignettes/*.pdf # documentation page docs +inst/doc diff --git a/DESCRIPTION b/DESCRIPTION index 5d8d10f..fea6e9a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -22,6 +22,7 @@ Suggests: knitr, lintr, rcmdcheck, + rmarkdown, mockr, spelling Imports: diff --git a/vignettes/.gitignore b/vignettes/.gitignore new file mode 100644 index 0000000..097b241 --- /dev/null +++ b/vignettes/.gitignore @@ -0,0 +1,2 @@ +*.html +*.R diff --git a/vignettes/tutorial/how-to-measure-apps-performance.Rmd b/vignettes/tutorial/how-to-measure-apps-performance.Rmd index 44ea100..0dc8bbc 100644 --- a/vignettes/tutorial/how-to-measure-apps-performance.Rmd +++ b/vignettes/tutorial/how-to-measure-apps-performance.Rmd @@ -1,6 +1,8 @@ --- title: "Tutorial: Compare performance of different versions of a shiny application" -output: rmarkdown::html_vignette +output: + rmarkdown::html_vignette: + self_contained: true vignette: > %\VignetteIndexEntry{Tutorial: Compare performance of different versions of a shiny application} %\VignetteEngine{knitr::rmarkdown} From 7f5dc4ed4958330d7513a8ec19685e38bcbb7a8d Mon Sep 17 00:00:00 2001 From: douglas Date: Fri, 9 Dec 2022 14:19:57 +0100 Subject: [PATCH 215/225] adding vignettes to build ignore --- .Rbuildignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.Rbuildignore b/.Rbuildignore index 6694056..03026e5 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -5,3 +5,4 @@ tests/end2end pkgdown docs +vignettes From 4052f69ab0cb847818495bcbe910fdba5c22f382 Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Tue, 13 Dec 2022 12:51:15 +0100 Subject: [PATCH 216/225] docs: Add tutorial to the documentation page. --- pkgdown/_pkgdown.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index b6d8b23..d01650d 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -23,6 +23,9 @@ navbar: - icon: fa-home href: index.html text: "Start" + - icon: fa-university + href: articles/tutorial/how-to-measure-apps-performance.html + text: "Tutorial" - icon: fa-file-code-o text: "Reference" href: reference/index.html From 64e810b4511b6ce831a737fe47fac20f3020aa24 Mon Sep 17 00:00:00 2001 From: "Douglas R. Mesquita Azevedo" Date: Mon, 2 Jan 2023 16:52:29 +0100 Subject: [PATCH 217/225] Fixing english Co-authored-by: Jakub Nowicki --- vignettes/tutorial/how-to-measure-apps-performance.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/tutorial/how-to-measure-apps-performance.Rmd b/vignettes/tutorial/how-to-measure-apps-performance.Rmd index 0dc8bbc..f56fc6d 100644 --- a/vignettes/tutorial/how-to-measure-apps-performance.Rmd +++ b/vignettes/tutorial/how-to-measure-apps-performance.Rmd @@ -30,7 +30,7 @@ remotes::install_github("Appsilon/shiny.benchmark") # Create an initial application -Let's start creating an application that will serve us as a guide through the `shiny.benchmark` functionalities. +Let's start by creating an application that will serve us as a guide through the `shiny.benchmark` functionalities. Save the following code as `ui.R`. It is a simple user interface containing three columns with one action button in each. Also each column has an output which will be created in the server file later. From 0d5b6059fb00aeb5efb43e4af736b9c68f4a9943 Mon Sep 17 00:00:00 2001 From: "Douglas R. Mesquita Azevedo" Date: Mon, 2 Jan 2023 16:53:28 +0100 Subject: [PATCH 218/225] Linking RStudio and Posit Co-authored-by: Jakub Nowicki --- vignettes/tutorial/how-to-measure-apps-performance.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/tutorial/how-to-measure-apps-performance.Rmd b/vignettes/tutorial/how-to-measure-apps-performance.Rmd index f56fc6d..8502278 100644 --- a/vignettes/tutorial/how-to-measure-apps-performance.Rmd +++ b/vignettes/tutorial/how-to-measure-apps-performance.Rmd @@ -149,7 +149,7 @@ This code is simulating clicks in the three buttons we have in our application. ## shinytest2 -`shinytest2` is an R package maintained by [Posit](https://posit.co/). It is handy for R users since all tests can be done using R only (differently than Cypress). To set up it easily, run `shinytest2::use_shinytest2()`. It will create configuration files which you do not need to change for this tutorial. +`shinytest2` is an R package maintained by [Posit](https://posit.co/) (formerly RStudio). It is handy for R users since all tests can be done using R only (differently than Cypress). To set up it easily, run `shinytest2::use_shinytest2()`. It will create configuration files which you do not need to change for this tutorial. Save the following code as `tests/testthat/test-set1.R`: From 390010e12e019140d346082008823058d769cbf4 Mon Sep 17 00:00:00 2001 From: "Douglas R. Mesquita Azevedo" Date: Mon, 2 Jan 2023 16:54:11 +0100 Subject: [PATCH 219/225] English improvment Co-authored-by: Jakub Nowicki --- vignettes/tutorial/how-to-measure-apps-performance.Rmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vignettes/tutorial/how-to-measure-apps-performance.Rmd b/vignettes/tutorial/how-to-measure-apps-performance.Rmd index 8502278..9e02458 100644 --- a/vignettes/tutorial/how-to-measure-apps-performance.Rmd +++ b/vignettes/tutorial/how-to-measure-apps-performance.Rmd @@ -194,7 +194,7 @@ renv::snapshot(prompt = FALSE) # Simulating app versions -In a regular project, you may use `git` to maintain the code. In this case, it is natural to have different app's versions in different branches/commits/releases. `shiny.benchmark` take advantage of these different `git` refs to run tests under different code versions. Add the following code to `.gitignore` to avoid problems with uncommitted files later: +In a regular project, you use `git` to maintain the code versioning. In this case, it is natural to have different app's versions in different branches/commits/releases. `shiny.benchmark` take advantage of these different `git` refs to run tests under different code versions. Add the following code to `.gitignore` to avoid problems with uncommitted files later: ```git .Rhistory From e10e71e9e162ff18765b9b2a4a5ea349dcda7d76 Mon Sep 17 00:00:00 2001 From: douglas Date: Fri, 6 Jan 2023 22:13:36 +0100 Subject: [PATCH 220/225] New images --- vignettes/tutorial/images/console_basic.png | Bin 43064 -> 36990 bytes vignettes/tutorial/images/plot.png | Bin 10007 -> 10255 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/vignettes/tutorial/images/console_basic.png b/vignettes/tutorial/images/console_basic.png index c86fb694b7ede452407a8e547197f2fee64e677f..17c715211384af55568d0041da44d55e70231da4 100644 GIT binary patch literal 36990 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/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
    AiPVk&k-;_8-MR3Y%sZse!+|lLUgvyxH3Nu`k?p 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
  • >`DZ957(m! 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 b(Am4zx*Jf<)RiWN1H6eZ{1l$6)5U8AC+ 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 literal 10007 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 Date: Fri, 6 Jan 2023 22:14:12 +0100 Subject: [PATCH 221/225] Small changes requested by the reviewer --- .../how-to-measure-apps-performance.Rmd | 204 +++++++++++------- 1 file changed, 126 insertions(+), 78 deletions(-) diff --git a/vignettes/tutorial/how-to-measure-apps-performance.Rmd b/vignettes/tutorial/how-to-measure-apps-performance.Rmd index 9e02458..10b1a95 100644 --- a/vignettes/tutorial/how-to-measure-apps-performance.Rmd +++ b/vignettes/tutorial/how-to-measure-apps-performance.Rmd @@ -26,6 +26,8 @@ To install `shiny.benchmark` use the following command: remotes::install_github("Appsilon/shiny.benchmark") ``` +`shiny.benchmark` will handle `Cypress` installation. If you face any inconvenience using `Cypress`, please try to use `shinytest2` in the rest of this tutorial. + ---- # Create an initial application @@ -66,7 +68,7 @@ function(input, output, session) { # Sys.sleep react1 <- eventReactive(input$run1, { out <- system.time( - Sys.sleep(times[1] + rexp(n = 1, rate = 10)) # we will play with the time here + Sys.sleep(times[1] + rexp(n = 1, rate = 1)) # we will play with the time here ) return(out[3]) @@ -74,7 +76,7 @@ function(input, output, session) { react2 <- eventReactive(input$run2, { out <- system.time( - Sys.sleep(times[2] + rexp(n = 1, rate = 10)) # we will play with the time here + Sys.sleep(times[2] + rexp(n = 1, rate = 1)) # we will play with the time here ) return(out[3]) @@ -82,7 +84,7 @@ function(input, output, session) { react3 <- eventReactive(input$run3, { out <- system.time( - Sys.sleep(times[3] + rexp(n = 1, rate = 10)) # we will play with the time here + Sys.sleep(times[3] + rexp(n = 1, rate = 1)) # we will play with the time here ) return(out[1]) @@ -90,15 +92,15 @@ function(input, output, session) { # outputs output$out1 <- renderUI({ - tags$span(round(react1()), style = "font-size: 500px;") + tags$span(round(react1()), style = "font-size: 5vw;") }) output$out2 <- renderUI({ - tags$span(round(react2()), style = "font-size: 500px;") + tags$span(round(react2()), style = "font-size: 5vw;") }) output$out3 <- renderUI({ - tags$span(round(react3()), style = "font-size: 500px;") + tags$span(round(react3()), style = "font-size: 5vw;") }) } ``` @@ -117,6 +119,54 @@ shiny::runApp() `shiny.benchmark` works under two different engines: `Cypress` and `shinytest2`. +## shinytest2 + +`shinytest2` is an R package maintained by [Posit](https://posit.co/) (formerly RStudio). It is handy for R users since all tests can be done using R only (differently than Cypress). To set up it easily, run `shinytest2::use_shinytest2()`. It will create configuration files which you do not need to change for this tutorial. + +Save the following code as `tests/testthat/test-set1.R`: + +```r +test_that("Out1 time elapsed - set1", { + app <- AppDriver$new(name = "test1", height = 975, width = 1619) + app$click("run1") + app$expect_values(output = "out1") +}) + +test_that("Out2 time elapsed - set1", { + app <- AppDriver$new(name = "test2", height = 975, width = 1619) + app$click("run2") + app$expect_values(output = "out2") +}) + +test_that("Out3 time elapsed - set1", { + app <- AppDriver$new(name = "test3", height = 975, width = 1619) + app$click("run3") + app$expect_values(output = "out3") +}) +``` + +This code is simulating clicks in the three buttons we have in our application. Also it waits for the outputs to appear. In a new file, replace `set1` by `set2` in the code and save it as `tests/testthat/test-set2.R` as well. It will be useful to present some functionalities later. + +```r +test_that("Out1 time elapsed - set2", { + app <- AppDriver$new(name = "test1", height = 975, width = 1619) + app$click("run1") + app$expect_values(output = "out1") +}) + +test_that("Out2 time elapsed - set2", { + app <- AppDriver$new(name = "test2", height = 975, width = 1619) + app$click("run2") + app$expect_values(output = "out2") +}) + +test_that("Out3 time elapsed - set2", { + app <- AppDriver$new(name = "test3", height = 975, width = 1619) + app$click("run3") + app$expect_values(output = "out3") +}) +``` + ## Cypress Cypress is a widely used end to end testing JavaScript library. Because its broader usage, this engine allows the user to take advantage of a huge number of functionalities in order to test its applications. Also, the community is active and therefore it is easier to find solution for bugs you may encounter while coding. @@ -125,56 +175,50 @@ Save the following code as `tests/cypress/test-set1.js`: ```r describe('Cypress test', () => { - it('Out1 time elapsed - set1', () => { // replace set1 by set2 + it('Out1 time elapsed - set1', () => { cy.visit('/'); cy.get('#run1').click(); cy.get('#out1', {timeout: 10000}).should('be.visible'); }); // Test how long it takes to wait for out2 - it('Out2 time elapsed - set1', () => { // replace set1 by set2 + it('Out2 time elapsed - set1', () => { cy.get('#run2').click(); cy.get('#out2', {timeout: 10000}).should('be.visible'); }); // Test how long it takes to wait for out3 - it('Out3 time elapsed - set1', () => { // replace set1 by set2 + it('Out3 time elapsed - set1', () => { cy.get('#run3').click(); cy.get('#out3', {timeout: 10000}).should('be.visible'); }); }); ``` -This code is simulating clicks in the three buttons we have in our application. Also it waits for the outputs to appear. In a new file, replace `set1` by `set2` in the code and save it as `tests/cypress/test-set2.js` as well. It will be useful to present some functionalities later. - -## shinytest2 - -`shinytest2` is an R package maintained by [Posit](https://posit.co/) (formerly RStudio). It is handy for R users since all tests can be done using R only (differently than Cypress). To set up it easily, run `shinytest2::use_shinytest2()`. It will create configuration files which you do not need to change for this tutorial. - -Save the following code as `tests/testthat/test-set1.R`: +Again, replace `set1` by `set2` in the code and save it as `tests/cypress/test-set2.R` as well. ```r -test_that("Out1 time elapsed - set1", { - app <- AppDriver$new(name = "test1", height = 975, width = 1619) - app$click("run1") - app$expect_values(output = "out1") -}) +describe('Cypress test', () => { + it('Out1 time elapsed - set2', () => { + cy.visit('/'); + cy.get('#run1').click(); + cy.get('#out1', {timeout: 10000}).should('be.visible'); + }); -test_that("Out2 time elapsed - set1", { - app <- AppDriver$new(name = "test2", height = 975, width = 1619) - app$click("run2") - app$expect_values(output = "out2") -}) + // Test how long it takes to wait for out2 + it('Out2 time elapsed - set2', () => { + cy.get('#run2').click(); + cy.get('#out2', {timeout: 10000}).should('be.visible'); + }); -test_that("Out3 time elapsed - set1", { - app <- AppDriver$new(name = "test3", height = 975, width = 1619) - app$click("run3") - app$expect_values(output = "out3") -}) + // Test how long it takes to wait for out3 + it('Out3 time elapsed - set2', () => { + cy.get('#run3').click(); + cy.get('#out3', {timeout: 10000}).should('be.visible'); + }); +}); ``` -Again, replace `set1` by `set2` in the code and save it as `tests/testthat/test-set2.R` as well. - ---- @@ -218,7 +262,7 @@ Also, let's create a new branch called `feature1`: git checkout -b feature1 ``` -At this point, we can simulate improvement in our application. To do so, let's change `Sys.sleep` time in the server function. Replace `times <- c(10, 5, 2)` by `times <- c(5, 2.5, 1)` in `server.R` and then commit the changes. +At this point, we can simulate improvement in our application. To do so, let's change `Sys.sleep` time in the server function. Replace `times <- c(10, 5, 2)` by `times <- c(5, 2.5, 1)` in first row of `server.R` and then commit the changes. ```git git add server.R @@ -231,10 +275,10 @@ To play with `renv` let's downgrade `shiny` version and snapshot it: git checkout -b feature2 ``` -Replace `times <- c(5, 2.5, 1)` by `times <- c(2.5, 1.25, 0.5)` in `server.R`. Also, run the following code to downgrade `shiny`: +Replace `times <- c(5, 2.5, 1)` by `times <- c(2.5, 1.25, 0.5)` in first row of `server.R`. Also, run the following code to downgrade `shiny`: ```r -renv::install("shiny@1.7.0") +renv::install("shiny@1.0.0") renv::snapshot(prompt = FALSE) ``` @@ -257,13 +301,13 @@ Now we have all ingredients needed: An application, a set of tests and different - `commit_list`: a named list of `git` refs (commit hashes, branch names, tags, ...) - `cypress_dir` or `shinytest2_dir`: path to `Cypress` or `shinytest2` tests -By default, `shiny.benchmark` uses `renv`. To turn `renv` off just set `use_renv = FALSE`. +By default, `shiny.benchmark` uses `renv`. To turn `renv` off just set `use_renv = FALSE` in the `benchmark` call. Be aware that this function will take a while to run since the application will be started and tested 3 times (`develop`, `feature1` and `using_renv` branches). ```r library(shiny.benchmark) commits <- list( - "develop" = "710fce371b3bf25c9223a11c70d5b27e5d16448e", # develop + "develop" = "develop", "feature1" = "feature1", "using_renv" = "feature2" ) @@ -284,7 +328,7 @@ shinytest2_out <- benchmark( ) ``` -For the sake of illustration, we are using the hash code in the develop branch (which is not needed). The console should display something similar to: +Instead of a branch name, you can also use the hash code of a desired commit. The console should display something similar to: @@ -295,24 +339,24 @@ cypress_out$performance ``` ```{r echo = FALSE, eval = TRUE} -list(develop = list(structure(list(date = structure(c(1670530838, -1670530838, 1670530838, 1670530838, 1670530838, 1670530838), class = c("POSIXct", +list(develop = list(structure(list(date = structure(c(1673034247, +1673034247, 1673034247, 1673034247, 1673034247, 1673034247), class = c("POSIXct", "POSIXt"), tzone = ""), rep_id = c(1L, 1L, 1L, 1L, 1L, 1L), test_name = c("Out1 time elapsed - set1", "Out2 time elapsed - set1", "Out3 time elapsed - set1", "Out1 time elapsed - set2", -"Out2 time elapsed - set2", "Out3 time elapsed - set2"), duration_ms = c(10743L, -5215L, 2309L, 10526L, 5354L, 2267L)), class = "data.frame", row.names = c(NA, --6L))), feature1 = list(structure(list(date = structure(c(1670530879, -1670530879, 1670530879, 1670530879, 1670530879, 1670530879), class = c("POSIXct", +"Out2 time elapsed - set2", "Out3 time elapsed - set2"), duration_ms = c(10782L, +6091L, 2804L, 10591L, 6768L, 3944L)), class = "data.frame", row.names = c(NA, +-6L))), feature1 = list(structure(list(date = structure(c(1673034279, +1673034279, 1673034279, 1673034279, 1673034279, 1673034279), class = c("POSIXct", "POSIXt"), tzone = ""), rep_id = c(1L, 1L, 1L, 1L, 1L, 1L), test_name = c("Out1 time elapsed - set1", "Out2 time elapsed - set1", "Out3 time elapsed - set1", "Out1 time elapsed - set2", -"Out2 time elapsed - set2", "Out3 time elapsed - set2"), duration_ms = c(5764L, -2798L, 1321L, 5316L, 2713L, 1169L)), class = "data.frame", row.names = c(NA, --6L))), using_renv = list(structure(list(date = structure(c(1670530923, -1670530923, 1670530923, 1670530923, 1670530923, 1670530923), class = c("POSIXct", +"Out2 time elapsed - set2", "Out3 time elapsed - set2"), duration_ms = c(6471L, +6442L, 1422L, 5613L, 3593L, 1272L)), class = "data.frame", row.names = c(NA, +-6L))), using_renv = list(structure(list(date = structure(c(1673034314, +1673034314, 1673034314, 1673034314, 1673034314, 1673034314), class = c("POSIXct", "POSIXt"), tzone = ""), rep_id = c(1L, 1L, 1L, 1L, 1L, 1L), test_name = c("Out1 time elapsed - set1", "Out2 time elapsed - set1", "Out3 time elapsed - set1", "Out1 time elapsed - set2", -"Out2 time elapsed - set2", "Out3 time elapsed - set2"), duration_ms = c(3435L, -1509L, 719L, 2978L, 1431L, 763L)), class = "data.frame", row.names = c(NA, +"Out2 time elapsed - set2", "Out3 time elapsed - set2"), duration_ms = c(3941L, +3010L, 995L, 3082L, 2130L, 2458L)), class = "data.frame", row.names = c(NA, -6L)))) ``` @@ -348,28 +392,29 @@ shinytest2_out$performance ``` ```{r echo=FALSE} -list(develop = list(structure(list(date = structure(c(1670530838, -1670530838, 1670530838, 1670530838, 1670530838, 1670530838), class = c("POSIXct", +list(develop = list(structure(list(date = structure(c(1673034247, +1673034247, 1673034247, 1673034247, 1673034247, 1673034247), class = c("POSIXct", "POSIXt"), tzone = ""), rep_id = c(1L, 1L, 1L, 1L, 1L, 1L), test_name = c("Out1 time elapsed - set1", "Out2 time elapsed - set1", "Out3 time elapsed - set1", "Out1 time elapsed - set2", -"Out2 time elapsed - set2", "Out3 time elapsed - set2"), duration_ms = c(12.649, -7.88999999999999, 4.51400000000001, 12.3340000000001, 7.47699999999998, -4.11500000000001)), class = "data.frame", row.names = c(NA, -6L -))), feature1 = list(structure(list(date = structure(c(1670530879, -1670530879, 1670530879), class = c("POSIXct", "POSIXt"), tzone = ""), +"Out2 time elapsed - set2", "Out3 time elapsed - set2"), duration_ms = c(12.0559999999996, +8.16600000000017, 6.94699999999921, 13.3299999999999, 7.09899999999925, +6.84699999999975)), class = "data.frame", row.names = c(NA, -6L +))), feature1 = list(structure(list(date = structure(c(1673034279, +1673034279, 1673034279), class = c("POSIXct", "POSIXt"), tzone = ""), rep_id = c(1L, 1L, 1L), test_name = c("Out1 time elapsed - set1", "Out2 time elapsed - set1", "Out3 time elapsed - set1"), - duration_ms = c(7.07800000000009, 5.447, 3.49199999999996 + duration_ms = c(6.96900000000005, 5.58899999999994, 3.23300000000017 )), class = "data.frame", row.names = c(NA, -3L))), using_renv = list( - structure(list(date = structure(c(1670530923, 1670530923, - 1670530923), class = c("POSIXct", "POSIXt"), tzone = ""), + structure(list(date = structure(c(1673034314, 1673034314, + 1673034314), class = c("POSIXct", "POSIXt"), tzone = ""), rep_id = c(1L, 1L, 1L), test_name = c("Out1 time elapsed - set2", "Out2 time elapsed - set2", "Out3 time elapsed - set2" - ), duration_ms = c(4.79999999999995, 3.63, 3.1339999999999 - )), class = "data.frame", row.names = c(NA, -3L)))) + ), duration_ms = c(4.59799999999996, 4.01999999999953, + 3.41100000000006)), class = "data.frame", row.names = c(NA, + -3L)))) ``` -Now the output is sightly different. For `develop` branch both files are in use (they match the pattern). For `feature1` and `feature2` only one file is in use. It can be useful when new tests are added during the development process and you need to run different tests for different versions. +Now the output is sightly different. For `develop` branch both files (`test-set1` and `test-set2`) are in use since they match the `test-set[0-9]` pattern. For `feature1` and `feature2` only one file is in use since we directly requested `test-set1` and `test-set2` files respectively. It can be useful when new tests are added during the development process and you need to run different tests for different versions. ## Repetitions @@ -381,10 +426,12 @@ shinytest2_out <- benchmark( shinytest2_dir = testthat_dir, use_renv = FALSE, tests_pattern = "set1", - n_rep = 10 + n_rep = 5 ) ``` +It is faster than running the benchmark several times since the test structure is created only once internally saving some execution time. + Some methods are implemented to make it easy to explore the results. `summary` brings summarized statistics as mean, median, minimum and maximum while `plot` shows a plot with the average times for each `git` ref and test. Also it presents maximum and minimum range. ```r @@ -397,19 +444,20 @@ structure(list(commit = c("develop", "develop", "develop", "feature1", ), test_name = c("Out1 time elapsed - set1", "Out2 time elapsed - set1", "Out3 time elapsed - set1", "Out1 time elapsed - set1", "Out2 time elapsed - set1", "Out3 time elapsed - set1", "Out1 time elapsed - set1", "Out2 time elapsed - set1", -"Out3 time elapsed - set1"), n = c(10L, 10L, 10L, 10L, 10L, 10L, -10L, 10L, 10L), mean = c(12.2756, 7.64179999999999, 4.70679999999998, -7.18020000000001, 5.22919999999999, 3.46759999999995, 5.05980000000004, -3.75569999999996, 2.82280000000001), median = c(12.238, 7.62050000000005, -4.77099999999996, 7.14600000000007, 5.1925, 3.40049999999985, -5.06850000000009, 3.60749999999996, 2.79049999999997), sd = c(0.212946106901357, -0.145640195916746, 0.210462876114088, 0.253890527590115, 0.191039728270784, -0.215106278641769, 0.340268456108157, 0.484944910960702, 0.259766904059041 -), min = c(11.963, 7.46699999999998, 4.35799999999995, 6.89200000000005, -4.89699999999993, 3.19999999999982, 4.57500000000005, 3.19899999999984, -2.49700000000007), max = c(12.602, 7.95299999999997, 4.97799999999984, -7.61400000000003, 5.56999999999994, 3.81999999999994, 5.75900000000001, -4.97299999999996, 3.21599999999989)), class = c("tbl_df", "tbl", +"Out3 time elapsed - set1"), n = c(5L, 5L, 5L, 5L, 5L, 5L, 5L, +5L, 5L), mean = c(12.3748, 7.37940000000017, 5.71140000000014, +7.72180000000008, 5.35640000000003, 4.28839999999982, 5.36419999999998, +4.74899999999998, 4.52899999999991), median = c(12.2960000000003, +7.1279999999997, 6.1220000000003, 7.32099999999991, 5.4320000000007, +4.39900000000034, 5.34699999999975, 4.79299999999967, 4.5019999999995 +), sd = c(0.473558549706366, 0.628408147624124, 1.21808653223053, +1.07820856052986, 0.603523653223451, 0.775558379491765, 0.455512019599966, +0.748540246078935, 0.982901826227147), min = c(11.9110000000001, +6.91499999999996, 4.26799999999912, 7.01000000000022, 4.76000000000022, +3.1279999999997, 4.9340000000002, 3.70100000000002, 3.65899999999965 +), max = c(13.1099999999997, 8.45000000000073, 7.15100000000075, +9.61200000000008, 6.27800000000025, 5.26899999999932, 6.10800000000017, +5.50500000000011, 6.11400000000049)), class = c("tbl_df", "tbl", "data.frame"), row.names = c(NA, -9L)) ``` From 1e828d01f3ec190a83a95095024e2e49ca649b4e Mon Sep 17 00:00:00 2001 From: douglas Date: Fri, 6 Jan 2023 22:37:06 +0100 Subject: [PATCH 222/225] update WORDLIST --- inst/WORDLIST | 1 + 1 file changed, 1 insertion(+) diff --git a/inst/WORDLIST b/inst/WORDLIST index f3ffef9..23160e7 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -2,6 +2,7 @@ CMD JS POSIXct Posit +RStudio appsilon dir js From 0be27d01feb3da2a6ad6f73bf0d4dbe33f2c0c4e Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 9 Jan 2023 13:49:15 +0100 Subject: [PATCH 223/225] docs: Update readme. --- README.md | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 9a85724..c976450 100644 --- a/README.md +++ b/README.md @@ -3,23 +3,21 @@ > _Tools to measure performance improvements in Shiny apps._ +[![CRAN status](https://www.r-pkg.org/badges/version/shiny.benchmark)](https://cran.r-project.org/package=shiny.benchmark) [![R-CMD-check](https://github.com/Appsilon/shiny.benchmark/workflows/R-CMD-check/badge.svg)](https://github.com/Appsilon/shiny.benchmark/actions?workflow=R-CMD-check) -[![codecov](https://codecov.io/github/Appsilon/shiny.benchmark/branch/develop/graph/badge.svg?token=JBEL2P5GIO)](https://codecov.io/github/Appsilon/shiny.benchmark) `shiny.benchmark` is a tool aimed to measure and compare the performance of different versions of a `shiny` application. Based on a list of different application versions, accessible by a git repo by its refs (commit hash or branch name), the user can write instructions to be executed using Cypress or `shinytest2`. These instructions are then evaluated by the different versions of your `shiny` application and therefore the performance's improvement/deterioration (time elapsed) are be recorded. The package is flexible enough to allow different sets of tests for the different refs as well as different package versions (via `renv`). Also, the user can replicate the tests to have more accurate measures of performance. -How to install? ---------------- +## How to install? ```r remotes::install_github("Appsilon/shiny.benchmark") ``` -Dependencies ------------- +## Dependencies `shiny.benchmark` can use two different engines to test the change in the performance of your application: [shinytest2](https://rstudio.github.io/shinytest2/) and [Cypress](https://www.cypress.io/). The latter requires `Node` (version 12 or higher) and `yarn` (version 1.22.17 or higher) to be available. @@ -31,8 +29,7 @@ To install them on your computer, follow the guidelines on the documentation pag Besides that, on Linux, it might be required to install other `Cypress` dependencies. Check the [documentation](https://docs.cypress.io/guides/getting-started/installing-cypress#Linux-Prerequisites) to find out more. -How to use it? --------------- +## How to use it? The best way to start using `shiny.benchmark` is through an example. If you want a start point, you can use the `load_example` function. In order to use this, create a new folder in your computer and use the following code to generate an application to serve us as example for our performance checks: @@ -160,21 +157,19 @@ summary(out) plot(out) ``` -How to contribute? ------------------- +## How to contribute? If you want to contribute to this project please submit a regular PR, once you're done with new feature or bug fix. Reporting a bug is also helpful - please use [GitHub issues](https://github.com/Appsilon/shiny.benchmark/issues) and describe your problem as detailed as possible. -Appsilon -======== +## Appsilon -Appsilon is the **Full Service Certified Posit Partner**. Learn more +Appsilon is a **Posit (formerly RStudio) Full Service Certified Partner**. Learn more at [appsilon.com](https://appsilon.com). -Get in touch [opensource@appsilon.com](opensource@appsilon.com) +Get in touch [opensource@appsilon.com](mailto:opensource@appsilon.com) We are hiring! From 4f5e5193dca84becf3187664eaeab3207ef5e7a2 Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 9 Jan 2023 13:49:32 +0100 Subject: [PATCH 224/225] chore: Update documentation website. --- pkgdown/_pkgdown.yml | 20 ++++++++++++++++++++ pkgdown/extra.css | 11 +++++++++++ 2 files changed, 31 insertions(+) diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index d01650d..a7b789c 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -14,6 +14,9 @@ template: gtag('js', new Date()); gtag('config', 'G-RS06EY8KNQ'); + + before_navbar: | + url: https://github.com/Appsilon/shiny.benchmark/ @@ -34,6 +37,17 @@ navbar: href: https://github.com/Appsilon/shiny.benchmark - icon: fa-twitter fa-lg href: https://twitter.com/Appsilon + - icon: fab fa-mastodon fa-lg + href: https://fosstodon.org/@appsilon + +home: + sidebar: + structure: [star, links, license, community, citation, authors, dev] + components: + star: + title: GitHub + text: | + Star reference: - title: Performance tests @@ -52,3 +66,9 @@ reference: - title: Other contents: - '`load_example`' + +footer: + structure: + left: developed + components: + developed: "Developed with :heart: by [Appsilon](https://appsilon.com)." diff --git a/pkgdown/extra.css b/pkgdown/extra.css index 7f9c6f3..482904a 100644 --- a/pkgdown/extra.css +++ b/pkgdown/extra.css @@ -33,6 +33,17 @@ button.btn.btn-primary.btn-copy-ex { border-color: rgb(178, 9, 41); } +.home { + left: 0px; + position: absolute; + padding: 8px 30px; + color: rgba(255,255,255,0.55); +} + +.home:hover { + color: rgba(255,255,255,0.9); +} + .app-preview { margin: 1.5em 0.75em; padding: 0.25em; From e26a22e4b892f42cf84cb9116468c78f010ac6d0 Mon Sep 17 00:00:00 2001 From: Jakub Nowicki Date: Mon, 9 Jan 2023 13:50:08 +0100 Subject: [PATCH 225/225] chore: Build documentation page on push to main. --- .github/workflows/pkgdown.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pkgdown.yml b/.github/workflows/pkgdown.yml index 352ce54..a8edc0a 100644 --- a/.github/workflows/pkgdown.yml +++ b/.github/workflows/pkgdown.yml @@ -3,7 +3,7 @@ on: push: branches: - - develop + - main workflow_dispatch: name: pkgdown