Skip to content

Commit 4117d33

Browse files
authored
Merge pull request #128 from rage/bundle-r-testrunner
bundled tmc-r-testrunner for tests to speed up CI
2 parents 9d9d4ff + b084603 commit 4117d33

File tree

39 files changed

+1115
-2
lines changed

39 files changed

+1115
-2
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ jobs:
2727
dotnet-version: "5.0.x"
2828
- name: Install tmc-r-tester
2929
run: |
30-
Rscript -e 'install.packages(c("devtools","testthat", "httr", "curl"),repos="https://ftp.eenet.ee/pub/cran/")'
31-
Rscript -e 'devtools::install_github("RTMC/tmc-r-tester/tmcRtestrunner")'
30+
Rscript -e 'install.packages(c("testthat", "jsonlite", "R.utils"))'
31+
Rscript -e 'install.packages("plugins/r/tests/tmcRtestrunner", repos=NULL, type="source")'
3232
- name: Build test binary
3333
run: cargo test --no-run --verbose
3434
- name: Run tests
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
^.*\.Rproj$
2+
^\.Rproj\.user$
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
linters: with_defaults(
2+
absolute_paths_linter = NULL, # 21
3+
closed_curly_linter = NULL, # 2
4+
open_curly_linter = NULL, # 2
5+
single_quotes_linter = NULL, # 1
6+
NULL,
7+
line_length_linter(120),
8+
object_length_linter(80)
9+
)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Package: tmcRtestrunner
2+
Type: Package
3+
Title: Runs TMC R projects
4+
Version: 0.1.0
5+
Author: RTMC
6+
Maintainer: The package maintainer <yourself@somewhere.net>
7+
Description: More about what it does (maybe more than one line)
8+
Use four spaces when indenting paragraphs within the Description.
9+
License: What license is it under?
10+
Encoding: UTF-8
11+
LazyData: true
12+
Depends:
13+
testthat,
14+
jsonlite,
15+
R.utils
16+
Imports:
17+
testthat,
18+
jsonlite,
19+
R.utils
20+
Suggests:
21+
lintr
22+
Collate:
23+
'ResultsJsonParser.R'
24+
'ResultsCreator.R'
25+
'RunTests.R'
26+
'GetAvailablePoints.R'
27+
'TestEnvironment.R'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
exportPattern("^[[:alpha:]]+")
2+
import(testthat)
3+
import(jsonlite)
4+
import(R.utils)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
2+
.get_available_points <- function(project_path) {
3+
.init_global_vars()
4+
test_files <- list.files(path = paste0(project_path, "/tests/testthat"), pattern = "test.*\\.R",
5+
full.names = T, recursive = FALSE)
6+
for (test_file in test_files) {
7+
.GlobalEnv$map_to_desc[[.GlobalEnv$counter]] <- list()
8+
.GlobalEnv$file_points[[.GlobalEnv$counter]] <- list()
9+
test_file(test_file, reporter = "silent", env = .create_counter_env(project_path))
10+
.GlobalEnv$counter <- .GlobalEnv$counter + 1
11+
}
12+
return (.add_points(.GlobalEnv$test_available_points, .GlobalEnv$file_points, .GlobalEnv$map_to_desc))
13+
}
14+
15+
.init_global_vars <- function() {
16+
.GlobalEnv$test_available_points <- list()
17+
.GlobalEnv$file_points <- list()
18+
.GlobalEnv$map_to_desc <- list()
19+
.GlobalEnv$counter <- 1
20+
}
21+
22+
.add_points <- function(test_available_points, file_points, map_to_desc) {
23+
all_available_points <- list()
24+
for (i in (1:unlist(.GlobalEnv$counter - 1))) {
25+
for (desc in map_to_desc[[i]]) {
26+
all_available_points[[desc]] <- c(file_points[[i]], test_available_points[[desc]])
27+
}
28+
}
29+
return (all_available_points)
30+
}
31+
32+
.create_counter_env <- function(project_path) {
33+
test_env <- new.env()
34+
.define_counter_functions(test_env, project_path)
35+
return (test_env)
36+
}
37+
38+
.define_counter_functions <- function(test_env, project_path) {
39+
.source_files(test_env, project_path)
40+
test_env$test <- function(desc, point, code){
41+
if (!(desc %in% .GlobalEnv$test_available_points)) {
42+
.GlobalEnv$test_available_points[[desc]] <- list()
43+
}
44+
.GlobalEnv$test_available_points[[desc]] <- c(point)
45+
.GlobalEnv$map_to_desc[[.GlobalEnv$counter]] <- c(.GlobalEnv$map_to_desc[[.GlobalEnv$counter]], desc)
46+
}
47+
test_env$points_for_all_tests <- function(points){
48+
.GlobalEnv$file_points[[.GlobalEnv$counter]] <- c(points)
49+
}
50+
}
51+
52+
# Checks the available points for all test in the project without running test. Creates
53+
# file .available_points.json in the project root.
54+
run_available_points <- function(project_path = getwd()) {
55+
available_points <- .get_available_points(project_path)
56+
57+
json_results <- .create_available_points_json_results(available_points)
58+
.write_json(json_results, paste0(project_path, "/.available_points.json"))
59+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
.create_file_results <- function(testthat_file_output,
2+
tests_points,
3+
file_points) {
4+
5+
results <- list()
6+
for (test in testthat_file_output) {
7+
name <- test$test
8+
status <- .get_status_for_test(test)
9+
message <- .create_message_for_test(test, status)
10+
backtrace <- .create_backtrace_for_test(test, status)
11+
points <- .get_points_for_test(name,
12+
tests_points,
13+
file_points)
14+
15+
test_result <- list("name" = name,
16+
"status" = status,
17+
"points" = points,
18+
"message" = message,
19+
"backtrace" = backtrace)
20+
21+
results[[length(results) + 1]] <- test_result
22+
}
23+
return(results)
24+
}
25+
26+
.get_points_for_test <- function(test_name, tests_points, file_points) {
27+
if (is.null(tests_points[[test_name]])) {
28+
test_points <- vector()
29+
} else {
30+
test_points <- tests_points[[test_name]]
31+
}
32+
test_points <- c(file_points, test_points)
33+
return(test_points)
34+
}
35+
36+
.get_status_for_test <- function(test) {
37+
if (.check_if_test_passed(test)) {
38+
status <- "pass"
39+
} else {
40+
status <- "fail"
41+
}
42+
return(status)
43+
}
44+
45+
#Checks if a single test passed
46+
.check_if_test_passed <- function(test) {
47+
ret <- TRUE
48+
for (result in test$results) {
49+
if (!.check_if_result_passed(result)) {
50+
ret <- FALSE
51+
break
52+
}
53+
}
54+
return(ret)
55+
}
56+
57+
#Check if a single result passed
58+
.check_if_result_passed <- function(result) {
59+
return(format(result) == "As expected")
60+
}
61+
62+
.message_from_failed_result <- function(result) {
63+
message_rows <- strsplit(result$message, "\n")[[1]]
64+
return(paste(message_rows, collapse = "\n"))
65+
}
66+
67+
.create_message_for_test <- function(test, status) {
68+
if (status == "pass") return("")
69+
70+
for (result in test$results) {
71+
if (format(result) != "As expected") {
72+
return(.message_from_failed_result(result))
73+
}
74+
}
75+
return("")
76+
}
77+
78+
.create_backtrace_for_test <- function(testthat_test_result, status) {
79+
if (status == "pass") return(list())
80+
81+
for (result in testthat_test_result$results) {
82+
if (format(result) != "As expected") {
83+
backtrace <- list()
84+
i <- 1;
85+
for (call in result$call) {
86+
backtrace <- append(backtrace, paste0(i, ": ", .create_call_message(call)))
87+
i <- i + 1
88+
}
89+
return(backtrace)
90+
}
91+
}
92+
return(list())
93+
94+
}
95+
96+
.create_call_message <- function(call) {
97+
call_str <- format(call)
98+
call_srcref <- attributes(call)$srcref
99+
srcref_data <- c(call_srcref)
100+
srcfile_filename <- attributes(call_srcref)$srcfile$filename
101+
102+
if (is.null(call_srcref)) {
103+
message <- paste0(call_str)
104+
} else {
105+
message <- paste0(call_str, " in ", srcfile_filename, "#", srcref_data[[1]])
106+
}
107+
108+
return(message)
109+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#Creates JSON containing test names and points availble from them, based on the test file.
2+
.create_available_points_json_results <- function(available_points) {
3+
results <- list()
4+
for (desc in names(available_points)) {
5+
results[[desc]] <- available_points[[desc]]
6+
}
7+
return (results)
8+
}
9+
10+
.create_json_run_results <- function(run_results) {
11+
json_test_results <- list()
12+
for (test_result in run_results$test_results) {
13+
json_test_results[[length(json_test_results) + 1]] <- .create_json_test_result(test_result)
14+
}
15+
json_run_results <- list("runStatus" = unbox(run_results$run_status),
16+
"backtrace" = lapply(run_results$backtrace, unbox), "testResults" = json_test_results)
17+
return(json_run_results)
18+
}
19+
20+
#Creates JSON for each different test case.
21+
.create_json_test_result <- function(test_result) {
22+
test_result <- list(status = unbox(test_result$status),
23+
name = unbox(format(test_result$name)),
24+
message = unbox(test_result$message),
25+
backtrace = lapply(test_result$backtrace, unbox),
26+
points = test_result$points)
27+
return(test_result)
28+
}
29+
30+
#Writes JSON based on the whole test result.
31+
.write_json <- function(results, file) {
32+
#json utf-8 coded:
33+
json <- enc2utf8(toJSON(results, pretty = FALSE))
34+
json <- prettify(json)
35+
#encode json to utf-8 and write file
36+
write(json, file)
37+
}
38+
39+
#Prints results.
40+
.print_results_from_json <- function(json_result) {
41+
for (test in json_result$testResults) {
42+
cat(sep = "", test$name, ": ", test$status, "\n")
43+
if (test$message != "") {
44+
cat(sep = "", "\n", test$message, "\n")
45+
}
46+
}
47+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Runs the tests from project directory and writes results JSON to the root of the project
2+
# as .results.json.
3+
#
4+
# Args:
5+
# project_path: The absolute path to the root of the project being tested.
6+
# print: If TRUE, prints results; if not, not. DEFAULT is FALSE.
7+
#
8+
# Returns:
9+
# Run results list containing: runStatus (string), backtrace (list), test_results (list)
10+
run_tests <- function(project_path = getwd(), print = FALSE) {
11+
#Runs tests for project and returns the results.
12+
#If sourcing_error occurs, .sourcing_error_run_results returns the results.
13+
run_results <- tryCatch({.run_tests_project(project_path)},
14+
sourcing_error = .sourcing_error_run_result,
15+
run_error = .run_error_run_result)
16+
17+
json_run_results <- .create_json_run_results(run_results)
18+
.write_json(json_run_results, file.path(project_path, ".results.json"))
19+
20+
if (print) {
21+
.print_results_from_json(json_run_results)
22+
}
23+
24+
invisible(run_results)
25+
}
26+
27+
.run_tests_project <- function(project_path) {
28+
test_results <- list()
29+
#Lists all the files in the path beginning with "test" and ending in ".R"
30+
test_files <- list.files(path = file.path(project_path, "tests", "testthat"), pattern = "test.*\\.R",
31+
full.names = TRUE, recursive = FALSE)
32+
33+
for (test_file in test_files) {
34+
file_results <- .run_tests_file(test_file, project_path)
35+
test_results <- c(test_results, file_results)
36+
}
37+
return(list("run_status" = "success", "backtrace" = list(), "test_results" = test_results))
38+
}
39+
40+
.run_tests_file <- function(file_path, project_path) {
41+
.GlobalEnv$points <- list()
42+
.GlobalEnv$points_for_all_tests <- list()
43+
44+
test_env = .create_test_env(project_path)
45+
test_file_output <- tryCatch({test_file(file_path, reporter = "silent", env = test_env)},
46+
error = .signal_run_error)
47+
48+
test_file_results <- .create_file_results(test_file_output, points, .GlobalEnv$points_for_all_tests)
49+
50+
return(test_file_results)
51+
}
52+
53+
.signal_sourcing_error <- function(error) {
54+
sourcing_error <- simpleError(message = error$message, call = error$call)
55+
class(sourcing_error) <- c("sourcing_error", class(sourcing_error))
56+
signalCondition(sourcing_error)
57+
}
58+
59+
.sourcing_error_run_result <- function(sourcing_error) {
60+
split_message <- strsplit(sourcing_error$message, split = "\n")
61+
backtrace <- lapply(split_message[[1]], unbox)
62+
return(list("run_status" = "sourcing_failed", "backtrace" = backtrace, "test_results" = list()))
63+
}
64+
65+
.signal_run_error <- function(error) {
66+
run_error <- simpleError(message = error$message, call = error$call)
67+
class(run_error) <- c("run_error", class(run_error))
68+
signalCondition(run_error)
69+
}
70+
71+
.run_error_run_result <- function(run_error) {
72+
split_message <- strsplit(run_error$message, split = "\n")
73+
backtrace <- lapply(split_message[[1]], unbox)
74+
return(list("run_status" = "run_failed", "backtrace" = backtrace, "test_results" = list()))
75+
}

0 commit comments

Comments
 (0)