├── .Rbuildignore ├── .gitignore ├── CHANGELOG.md ├── DESCRIPTION ├── LICENSE ├── NAMESPACE ├── R ├── aaa-s3-definitions.R ├── check-code.R ├── check-control.R ├── check-error.R ├── check-expr.R ├── check-fun-def.R ├── check-function-result.R ├── check-function.R ├── check-ggplot.R ├── check-library-function.R ├── check-logic.R ├── check-mc.R ├── check-object.R ├── check-output.R ├── check-predefined-objects.R ├── check-rmd-utils.R ├── check-rmd.R ├── check-that.R ├── check-wd.R ├── comparison.R ├── diff.R ├── is-equal.R ├── messages-utils.R ├── messages.R ├── parse-status.R ├── parse-token.R ├── parse-tokenizer.R ├── parse-utils.R ├── parse.R ├── reporter.R ├── rstudio-test-chunk-options.R ├── rstudio-test-pipe.R ├── rstudio-test-props.r ├── rstudio-test-rmd-group.R ├── rstudio-test-text.R ├── rstudio-test-yaml-header.R ├── state.R ├── success-msg.R ├── test-exercise.R ├── test_an_object.R ├── utils-control.R ├── utils-function.R ├── utils-learnr.R ├── utils-pd.R ├── utils-rmd.R ├── utils-state.R └── utils.R ├── README.md ├── _pkgdown.yml ├── inst └── experiment.R ├── man ├── build_pd.Rd ├── check_chunk.Rd ├── check_code.Rd ├── check_control.Rd ├── check_equal.MarkdownChunkOptionState.Rd ├── check_equal.MarkdownYamlOptionState.Rd ├── check_error.Rd ├── check_fun_def.Rd ├── check_function.Rd ├── check_function_result.Rd ├── check_ggplot.Rd ├── check_header.Rd ├── check_library.Rd ├── check_logic.Rd ├── check_mc.Rd ├── check_option.MarkdownChunkState.Rd ├── check_option.MarkdownYamlState.Rd ├── check_output.Rd ├── check_predefined_objects.Rd ├── check_rmd.Rd ├── check_that.Rd ├── check_title.Rd ├── check_wd.Rd ├── check_yaml.Rd ├── compare.Rd ├── disable_highlighting.Rd ├── ex.Rd ├── get_num_hits.Rd ├── is_equal.Rd ├── override.Rd ├── reexports.Rd ├── run_until_fail.Rd ├── s3defs.Rd ├── setup_state.Rd ├── state.Rd ├── success_msg.Rd ├── test_chunk_options.Rd ├── test_exercise.Rd ├── test_expr.Rd ├── test_object.Rd ├── test_pipe.Rd ├── test_props.Rd ├── test_rmd_group.Rd ├── test_text.Rd ├── test_yaml_header.Rd ├── testwhat_learnr.Rd └── tw.Rd ├── tests ├── testthat.R └── testthat │ ├── .Rapp.history │ ├── checking-function-calls.R │ ├── helper.R │ ├── test-check-code.R │ ├── test-check-control.R │ ├── test-check-error.R │ ├── test-check-expr.R │ ├── test-check-fun-def.R │ ├── test-check-function-result.R │ ├── test-check-function.R │ ├── test-check-ggplot.R │ ├── test-check-library-function.R │ ├── test-check-logic.R │ ├── test-check-mc.R │ ├── test-check-object.R │ ├── test-check-output.R │ ├── test-check-rmd.R │ ├── test-check-that.R │ ├── test-check-wd.R │ ├── test-content-examples.R │ ├── test-diff.R │ ├── test-highlighting.R │ ├── test-message-utils.R │ ├── test-messages.R │ ├── test-parse.R │ ├── test-rstudio-test-chunk-options.R │ ├── test-rstudio-test-pipe.R │ ├── test-rstudio-test-props.R │ ├── test-rstudio-test-rmd-file.R │ ├── test-rstudio-test-rmd-group.R │ ├── test-rstudio-test-text.R │ ├── test-rstudio-test-yaml-header.R │ ├── test-state.R │ ├── test-success-msg.R │ ├── test-test-an-object.R │ ├── test-test-exercise.R │ ├── test-test-predefined-objects.R │ ├── test-utils-state.R │ └── test-utils.R ├── testwhat.Rproj └── vignettes ├── checking-control-flow.Rmd ├── checking-function-calls.Rmd ├── checking-function-definitions.Rmd ├── checking-markdown-documents.Rmd ├── checking-objects.Rmd ├── checking-output.Rmd ├── checking-through-string-matching.Rmd ├── combining-sets-of-SCTs.Rmd ├── electives.Rmd ├── extensions.Rmd ├── glossary.Rmd ├── syntax.Rmd └── test-to-check.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^\.git$ 4 | ^inst/web$ 5 | ^docs$ 6 | ^_pkgdown\.yml$ 7 | ^README\.Rmd$ 8 | ^README-.*\.png$ 9 | ^pkgdown$ 10 | ^CHANGELOG\.md$ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .DS_Store 5 | .tmp.RData 6 | *.orig 7 | inst/doc 8 | docs/* 9 | vignettes/*.R 10 | vignettes/*.html 11 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: testwhat 2 | Type: Package 3 | Title: Easily write submission correctness tests for R exercises 4 | Version: 4.11.1 5 | Date: 2014-07-31 6 | Description: This package makes it easy for teachers to test students' 7 | code submissions for R exercises, e.g., for interactive R courses on DataCamp. 8 | Depends: 9 | R (>= 3.0.0) 10 | Imports: 11 | evaluate, 12 | markdown, 13 | R6, 14 | stringdist, 15 | magrittr, 16 | praise 17 | Suggests: 18 | car, 19 | covr, 20 | dplyr, 21 | ggvis, 22 | ggplot2, 23 | jsonlite, 24 | knitr, 25 | lazyeval, 26 | pkgdown, 27 | rmarkdown, 28 | rpart, 29 | stats, 30 | testthat, 31 | utils, 32 | withr 33 | License: GPL-3 + file LICENSE 34 | LazyData: true 35 | URL: https://github.com/datacamp/testwhat 36 | BugReports: https://github.com/datacamp/testwhat/issues 37 | Authors@R: c( 38 | person("Andreas", "Alfons", role = "aut"), 39 | person("Filip", "Schouwenaars", ,"filip@datacamp.com", role = c("aut", "cre")), 40 | person("Vincent", "Vankrunkelsven", ,"vincent@datacamp.com", role = c("aut")) 41 | ) 42 | RoxygenNote: 6.1.1 43 | VignetteBuilder: knitr 44 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(check_body,ControlState) 4 | S3method(check_body,FunDefState) 5 | S3method(check_body,default) 6 | S3method(check_equal,ArgumentState) 7 | S3method(check_equal,ExprErrorState) 8 | S3method(check_equal,ExprOutputState) 9 | S3method(check_equal,ExprResultState) 10 | S3method(check_equal,FunctionResultState) 11 | S3method(check_equal,MarkdownChunkOptionState) 12 | S3method(check_equal,MarkdownYamlOptionState) 13 | S3method(check_equal,ObjectColumnState) 14 | S3method(check_equal,ObjectElementState) 15 | S3method(check_equal,ObjectState) 16 | S3method(check_equal,OperationResultState) 17 | S3method(check_equal,default) 18 | S3method(check_error,ExprState) 19 | S3method(check_error,default) 20 | S3method(check_option,MarkdownChunkState) 21 | S3method(check_option,MarkdownYamlState) 22 | S3method(check_option,default) 23 | S3method(check_output,ExprState) 24 | S3method(check_output,default) 25 | S3method(check_result,ExprState) 26 | S3method(check_result,FunctionState) 27 | S3method(check_result,OperationState) 28 | S3method(check_result,default) 29 | S3method(is_equal,default) 30 | S3method(is_equal,formula) 31 | S3method(print,State) 32 | export("%>%") 33 | export(ChildState) 34 | export(RootState) 35 | export(State) 36 | export(build_pd) 37 | export(check_arg) 38 | export(check_arguments) 39 | export(check_body) 40 | export(check_call) 41 | export(check_chunk) 42 | export(check_code) 43 | export(check_column) 44 | export(check_cond) 45 | export(check_correct) 46 | export(check_element) 47 | export(check_else) 48 | export(check_equal) 49 | export(check_error) 50 | export(check_expr) 51 | export(check_for) 52 | export(check_fun_def) 53 | export(check_function) 54 | export(check_header) 55 | export(check_if) 56 | export(check_if_else) 57 | export(check_library) 58 | export(check_mc) 59 | export(check_object) 60 | export(check_operator) 61 | export(check_option) 62 | export(check_or) 63 | export(check_output) 64 | export(check_output_expr) 65 | export(check_predefined_objects) 66 | export(check_result) 67 | export(check_rmd) 68 | export(check_that) 69 | export(check_title) 70 | export(check_wd) 71 | export(check_while) 72 | export(check_yaml) 73 | export(disable_highlighting) 74 | export(ex) 75 | export(get_num_hits) 76 | export(is_equal) 77 | export(is_false) 78 | export(is_gte) 79 | export(is_lt) 80 | export(is_true) 81 | export(override_solution) 82 | export(override_solution_code) 83 | export(override_solution_env) 84 | export(run_until_fail) 85 | export(setup_state) 86 | export(success_msg) 87 | export(test_exercise) 88 | export(test_what) 89 | export(testwhat_learnr) 90 | export(tw) 91 | importFrom(R6,R6Class) 92 | importFrom(evaluate,evaluate) 93 | importFrom(magrittr,"%>%") 94 | importFrom(markdown,markdownToHTML) 95 | importFrom(praise,praise) 96 | importFrom(stats,na.omit) 97 | importFrom(stringdist,stringdist) 98 | importFrom(utils,argsAnywhere) 99 | importFrom(utils,capture.output) 100 | importFrom(utils,getAnywhere) 101 | importFrom(utils,getParseData) 102 | importFrom(utils,getParseText) 103 | importFrom(utils,methods) 104 | importFrom(utils,tail) 105 | -------------------------------------------------------------------------------- /R/aaa-s3-definitions.R: -------------------------------------------------------------------------------- 1 | #' S3 definitions 2 | #' 3 | #' @param state State to start from 4 | #' @param ... Additional arguments passed to implementations of the S3 method 5 | #' 6 | #' @name s3defs 7 | 8 | #' @rdname s3defs 9 | #' @export 10 | check_equal <- function(state, ...) { 11 | UseMethod("check_equal", state) 12 | } 13 | 14 | #' @rdname s3defs 15 | #' @export 16 | check_equal.default <- function(state, ...) { 17 | stop("Can't run check_equal() with a ", class(state)[1], " as input state.", call. = FALSE) 18 | } 19 | 20 | #' @rdname s3defs 21 | #' @export 22 | check_result <- function(state, ...) { 23 | UseMethod("check_result", state) 24 | } 25 | 26 | #' @rdname s3defs 27 | #' @export 28 | check_result.default <- function(state, ...) { 29 | stop("Can't run check_result() with a ", class(state)[1], " as input state.", call. = FALSE) 30 | } 31 | 32 | #' @rdname s3defs 33 | #' @export 34 | check_body <- function(state, ...) { 35 | UseMethod("check_body", state) 36 | } 37 | 38 | #' @rdname s3defs 39 | #' @export 40 | check_body.default <- function(state, ...) { 41 | stop("Can't run check_body() with a ", class(state)[1], " as input state.", call. = FALSE) 42 | } 43 | 44 | #' @rdname s3defs 45 | #' @export 46 | check_option <- function(state, ...) { 47 | UseMethod("check_option", state) 48 | } 49 | 50 | #' @rdname s3defs 51 | #' @export 52 | check_option.default <- function(state, ...) { 53 | stop("Can't run check_option() with a ", class(state)[1], " as input state.", call. = FALSE) 54 | } 55 | -------------------------------------------------------------------------------- /R/check-code.R: -------------------------------------------------------------------------------- 1 | #' Test the student's code as text 2 | #' 3 | #' Some rudimentary string cleaning is performed to allow for different ways of 4 | #' saying the same things (removing spaces, changing single quotes to double 5 | #' quotes, changing TRUE to T ...). 6 | #' 7 | #' Using these function should be a last resort, as there are myriad ways of 8 | #' solving the same problem with R! 9 | #' 10 | #' @param regex A set of strings/regexes that should be in the student code. 11 | #' @param fixed if TRUE, strings are treated literally. If FALSE, strings are 12 | #' treated as regex patterns. 13 | #' @param times how often should any of the strings be matched? 14 | #' @param state the state to start from 15 | #' @param missing_msg Custom feedback in case the pattern is not contained often 16 | #' enough in the student's submission. 17 | #' @param append Whether or not to append the feedback to feedback built in 18 | #' previous states 19 | #' @param drop_comments Logical value indicating whether or not to remove 20 | #' comments from these student code before looking for the pattern. Defaults 21 | #' to FALSE for backwards compatibility reasons. 22 | #' @examples 23 | #' \dontrun{ 24 | #' # Example 1 25 | #' TRUE & FALSE 26 | #' 27 | #' # SCT 28 | #' ex() %>% check_code(c("TRUE & FALSE", "FALSE & TRUE"), fixed = TRUE) 29 | #' 30 | #' # Example 2: 31 | #' "Hello, world!" 32 | #' 33 | #' # SCT, robust to small typos 34 | #' ex() %>% check_code("[H|h]ello,*\\s*[W|w]orld\\!*") 35 | #' } 36 | #' 37 | #' @export 38 | check_code <- function(state, regex, 39 | fixed = FALSE, times = 1, 40 | missing_msg = NULL, append = TRUE, 41 | drop_comments = FALSE) { 42 | assert_state(state) 43 | regex_state <- RegexState$new(state) 44 | regex_state$add_details(type = "typed", 45 | regex = regex, 46 | fixed = fixed, 47 | times = times, 48 | message = missing_msg, 49 | append = append, 50 | pd = NULL) 51 | student_code <- state$get("student_code") 52 | if (isTRUE(drop_comments)) { 53 | student_code <- remove_comments(student_code) 54 | } 55 | if (isTRUE(fixed)) { 56 | student_code <- clean_up(student_code) 57 | regex <- clean_up(regex) 58 | } 59 | num_hits <- get_num_hits(regex = regex, x = student_code, fixed = fixed) 60 | check_that(is_gte(num_hits, times), feedback = regex_state$details) 61 | return(state) 62 | } 63 | 64 | # deprecated test_student_typed 65 | test_student_typed <- function(strings, 66 | fixed = TRUE, 67 | times = 1, 68 | not_typed_msg = NULL) { 69 | fail_if_v2_only() 70 | ex() %>% check_code(strings, fixed = fixed, times = times, missing_msg = not_typed_msg, append = is.null(not_typed_msg)) 71 | } 72 | 73 | -------------------------------------------------------------------------------- /R/check-error.R: -------------------------------------------------------------------------------- 1 | #' Explicitly check whether the student's submission threw an error. 2 | #' 3 | #' With information gathered from the R Backend, testwhat can detect whether the 4 | #' student's submission generated an error. 5 | #' 6 | #' If all SCTs for an exercise pass, before marking the submission as correct 7 | #' testwhat will automatically check whether the student submission generated an 8 | #' error, unless the exercise explicitly allows for errors. This means it is not 9 | #' needed to use \code{check_error} explicitly. However, in some cases, 10 | #' using \code{check_error} explicitly somewhere 11 | #' throughout your SCT execution can be helpful: 12 | #' 13 | #' \itemize{ 14 | #' \item{If you want to make sure people didn't write typos when 15 | #' writing a long function name.} 16 | #' \item{If you want to first verify whether a 17 | #' function call actually runs,before checking whether the arguments were 18 | #' specified correctly.} 19 | #' \item{More generally, if, because of the content, it's 20 | #' instrumental that the script runs without errors before doing any other 21 | #' verifications.} 22 | #' } 23 | #' 24 | #' @param state State to start from (for \code{check_error}) 25 | #' @param incorrect_msg additional message that is appended to the automatically 26 | #' generated feedback message. 27 | #' @param ... S3 stuff 28 | #' 29 | #' @examples 30 | #' \dontrun{ 31 | #' # Example student code: x <- 4 + "a" 32 | #' 33 | #' # SCT that explicitly checks for an error first 34 | #' ex() %>% check_error() 35 | #' ex() %>% check_object('x') %>% check_equal() 36 | #' 37 | #' # SCT that does not have to check for an error 38 | #' # testwhat will verify for an error implicitly 39 | #' ex() %>% check_object('x') %>% check_equal() 40 | #' } 41 | #' 42 | #' @name check_error 43 | 44 | #' @rdname check_error 45 | #' @export 46 | check_error <- function(state, ...) { 47 | UseMethod("check_error", state) 48 | } 49 | 50 | #' @rdname check_error 51 | #' @export 52 | check_error.default <- function(state, incorrect_msg = NULL, ...) { 53 | assert_state(state) 54 | output_list <- state$get("output_list") 55 | student_pd <- state$get("student_pd") 56 | 57 | error_state <- ErrorState$new(state) 58 | 59 | error_indices <- which(sapply(output_list, `[[`, "type") == "r-error") 60 | if(length(error_indices) == 0) { 61 | fb_msg <- "ok" 62 | pd <- NULL 63 | } else { 64 | error_index <- error_indices[1] 65 | error <- output_list[[error_index]]$payload 66 | fb_msg <- paste("Your code contains an error that you should fix:", 67 | "```", 68 | error, 69 | "```", 70 | ifelse(is.null(incorrect_msg), "", incorrect_msg), 71 | sep = "\n") 72 | 73 | line_info <- NULL 74 | call_index <- error_indices[1] - 1 75 | if(call_index > 0) { 76 | call <- output_list[[call_index]]$payload 77 | hits <- student_pd$text == call 78 | if(!any(hits)) { 79 | pd <- NULL 80 | } else if (sum(hits) == 1) { 81 | pd <- student_pd[hits, ] 82 | } else { 83 | # more than 1 hit - select the code that actually generated the error 84 | hit_indices <- which(sapply(output_list, function(x) { 85 | x$type == "code" && x$payload == call 86 | })) 87 | pd <- student_pd[which(hits)[call_index == hit_indices], ] 88 | } 89 | } else { 90 | pd <- NULL 91 | } 92 | } 93 | error_state$add_details(type = "error", message = fb_msg, pd = pd, append = TRUE) 94 | check_that(is_true(length(error_indices) == 0), feedback = error_state$details) 95 | return(error_state) 96 | } 97 | 98 | # Deprecated 99 | test_error <- function(incorrect_msg = NULL) { 100 | fail_if_v2_only() 101 | ex() %>% check_error(incorrect_msg = incorrect_msg) 102 | } 103 | -------------------------------------------------------------------------------- /R/check-library-function.R: -------------------------------------------------------------------------------- 1 | #' Check whether the library function was called correctly 2 | #' 3 | #' Convenience function to test in a very hacky way whether 4 | #' the library function was called correctly in its most simple form. 5 | #' There is support for the different ways to call the library function 6 | #' 7 | #' @param state state to start from 8 | #' @param package package name for which the library() 9 | #' function should've been called 10 | #' @param not_called_msg optional feedback message in case the library 11 | #' function wasn't called a single time 12 | #' @param incorrect_msg optional feedback message in case the library 13 | #' function wasn't called for the specified package. 14 | #' @inheritParams test_function 15 | #' 16 | #' @examples 17 | #' \dontrun{ 18 | #' # example solution 19 | #' library(ggvis) 20 | #' 21 | #' # sct to test whether ggvis was loaded 22 | #' ex() %>% check_library("ggvis") 23 | #' } 24 | #' 25 | #' @export 26 | check_library <- function(state, package, not_called_msg = NULL, incorrect_msg = NULL) { 27 | assert_state(state) 28 | if(is.null(not_called_msg)) { 29 | not_called_msg <- sprintf("Make sure to call the `library()` function to load the `%s` package.", package) 30 | } 31 | 32 | if(is.null(incorrect_msg)) { 33 | incorrect_msg <- sprintf("Have you correctly called the `library()` function to load the `%s` package?", package) 34 | } 35 | 36 | state %>% check_code(regex = "(library|require)\\(", missing_msg = not_called_msg, 37 | append = is.null(not_called_msg), drop_comments = TRUE) 38 | state %>% check_code(regex = sprintf("(library|require)\\s*\\(\\s*[\"']?%s[\"']?\\s*\\)",package), 39 | missing_msg = incorrect_msg, append = is.null(incorrect_msg), drop_comments = TRUE) 40 | return(state) 41 | } 42 | 43 | test_library_function <- function(package, 44 | not_called_msg = NULL, 45 | incorrect_msg = NULL) { 46 | fail_if_v2_only() 47 | ex() %>% check_library(package, not_called_msg = not_called_msg, incorrect_msg = incorrect_msg) 48 | } 49 | -------------------------------------------------------------------------------- /R/check-mc.R: -------------------------------------------------------------------------------- 1 | #' Test a multiple choice exercise 2 | #' 3 | #' This code expects the DM.result variable to be defined by the DataCamp frontend. 4 | #' There is need to define the success_msg seperately, since it is defined inside the function. 5 | #' 6 | #' @param state the state passed to it. Use \code{ex()} at all times. 7 | #' @param correct number of the correct answer (or vector of numbers, if several options are fine) 8 | #' @param no_selection_msg feedback message in case the student did not select an answer. 9 | #' @param feedback_msgs vector of feedback messages for both the incorrect exercises as the correct exercise. 10 | #' Order the messages according to how they are listed in the instructions. For example, if there are four options, 11 | #' the second of which is correct, a vector of four feedback messages should be provided. The first message corresponds 12 | #' to feedback on the incorrect selection of the first option, the second message corresponds to the feedback message for 13 | #' the correct collection. The third and fourth messages correspond to feedback on the incorrect selection of the third and 14 | #' fourth option. 15 | #' 16 | #' @examples 17 | #' \dontrun{ 18 | #' # Example solution: second instruction correct out of three options. 19 | #' 20 | #' # Corresponding SCT: 21 | #' msg1 <- "Not good, try again!" 22 | #' msg2 <- "Nice one!" 23 | #' msg3 <- "Not quite, give it another shot." 24 | #' ex() %>% check_mc(2, feedback_msgs = c(msg1, msg2, msg3)) 25 | #' } 26 | #' 27 | #' @export 28 | check_mc <- function(state, correct, no_selection_msg = NULL, feedback_msgs = NULL) { 29 | assert_state(state) 30 | # see if DM.result exists 31 | if (is.null(no_selection_msg)) { 32 | no_selection_msg <- "Please select one of the options!" 33 | } 34 | 35 | check_that(is_true(exists("DM.result", envir = state$get("student_env"))), feedback = no_selection_msg) 36 | result <- get("DM.result", envir = state$get("student_env")) 37 | 38 | # see if result is correct 39 | if (!is.null(feedback_msgs) && is.na(feedback_msgs[result])) { 40 | stop("There is no feedback message available for this user input! Make sure you define enough feedback messages.") 41 | } 42 | 43 | check_that(is_true(result %in% correct), 44 | feedback = ifelse(is.null(feedback_msgs), "Your answer is incorrect. Try again.", feedback_msgs[result])) 45 | 46 | success_msg(ifelse(is.null(feedback_msgs), "Good job! Continue to the next exercise.", feedback_msgs[correct])) 47 | } 48 | 49 | test_mc <- function(correct, no_selection_msg = NULL, feedback_msgs = NULL) { 50 | fail_if_v2_only() 51 | ex() %>% check_mc(correct, no_selection_msg = no_selection_msg, feedback_msgs = feedback_msgs) 52 | } 53 | -------------------------------------------------------------------------------- /R/check-predefined-objects.R: -------------------------------------------------------------------------------- 1 | #' Test predefined R objects 2 | #' 3 | #' At the start of your SCT, you typically want to check whether some predefined 4 | #' variables are still correct. \code{test_predefined_object} allows you to 5 | #' specify a vector of object names, together with a vector of equivalence 6 | #' conditions, evaluation specifications, undefined an incorrect messages. 7 | #' 8 | #' @param state the state to start from 9 | #' @param name vector of names of the objects to check 10 | #' @param eq_condition character vector indicating how to compare. See 11 | #' \code{\link{is_equal}}. 12 | #' @param eval logical vector indicating whether or not you want to check only 13 | #' the objects' existence or also whether their values match the solution. 14 | #' @param undefined_msg vector version of \code{undefined_msg} of 15 | #' \code{\link{check_object}} 16 | #' @param incorrect_msg vector version of \code{incorrect_msg} of 17 | #' \code{\link{check_object}} 18 | #' 19 | #' @examples 20 | #' \dontrun{ 21 | #' # Suppose the sample code specifies the variables a, b and c, 22 | #' # and you want to check that a, b and c haven't changed. 23 | #' ex() %>% check_predefined_objects(c("a", "b", "c")) 24 | #' } 25 | #' 26 | #' @export 27 | check_predefined_objects <- function(state, 28 | name, 29 | eq_condition = "equivalent", 30 | eval = TRUE, 31 | undefined_msg = NULL, 32 | incorrect_msg = NULL) { 33 | assert_state(state) 34 | n_names <- length(name) 35 | eq_condition <- rep(eq_condition, n_names, length.out = n_names) 36 | eval <- rep(eval, n_names, length.out = n_names) 37 | 38 | already <- "it has already been coded for you! You can use the arrow to the left of 'Run Code' to reset your code." 39 | 40 | if (is.null(undefined_msg)) { 41 | undefined_msg <- sprintf("Don't remove the predefined variable `%s`; %s", name, already) 42 | } 43 | 44 | if (is.null(incorrect_msg)) { 45 | incorrect_msg <- sprintf("Don't change the contents of the predefined variable `%s`; %s", name, already) 46 | } 47 | 48 | undefined_msg <- rep(undefined_msg, n_names, length.out = n_names) 49 | incorrect_msg <- rep(incorrect_msg, n_names, length.out = n_names) 50 | 51 | for (i in 1:n_names) { 52 | obj <- ex() %>% check_object(name[i], undefined_msg = undefined_msg[i]) 53 | if (isTRUE(eval[i])) { 54 | obj %>% check_equal(eq_condition = eq_condition[i], incorrect_msg = incorrect_msg[i]) 55 | } 56 | } 57 | } 58 | 59 | test_predefined_objects <- function(name, 60 | eq_condition = "equivalent", 61 | eval = TRUE, 62 | undefined_msg = NULL, 63 | incorrect_msg = NULL) { 64 | fail_if_v2_only() 65 | ex() %>% check_predefined_objects(name = name, 66 | eq_condition = eq_condition, 67 | eval = eval, 68 | undefined_msg = undefined_msg, 69 | incorrect_msg = incorrect_msg) 70 | } 71 | -------------------------------------------------------------------------------- /R/check-rmd-utils.R: -------------------------------------------------------------------------------- 1 | find_same_line <- function(lines, patt) { 2 | hits <- which(grepl(patt, lines)) 3 | return(hits) 4 | } 5 | 6 | find_prev_line <- function(lines, patt) { 7 | candidates <- which(grepl(patt, lines)) - 1 8 | # previous row must contain code 9 | return(candidates[!grepl("^\\s*$", lines[candidates])]) 10 | } 11 | 12 | get_header_hits <- function(lines, level) { 13 | all_hits <- list( 14 | h1 = sort(c(length(lines) + 1, 15 | find_same_line(patt = "^#\\s+.*?", lines = lines), 16 | find_prev_line(patt = "^={5,}$", lines = lines))), 17 | h2 = sort(c(length(lines) + 1, 18 | find_same_line(patt = "^##\\s+.*?", lines = lines), 19 | find_prev_line(patt = "^-{5,}$", lines = lines))), 20 | h3 = sort(c(length(lines) + 1, find_same_line(patt = "^###\\s+.*?", lines = lines))) 21 | ) 22 | 23 | hits <- all_hits[[paste0('h', level)]] 24 | if(is.null(hits)) stop(sprintf("No pattern matching available for level %i.", level)) 25 | return(hits) 26 | } 27 | 28 | get_header_elements <- function(lines, hits, index) { 29 | title <- lines[hits[index]] 30 | contents <- lines[(hits[index] + 1): (hits[index+1] - 1)] 31 | contents <- contents[!grepl("^-{5,}$", contents)] 32 | contents <- contents[!grepl("^={5,}$", contents)] 33 | contents <- paste0(contents, collapse = "\n") 34 | 35 | return(list(contents = contents, title = title)) 36 | } 37 | 38 | get_chunks <- function(code, index = index) { 39 | doc_structure <- build_doc_structure(code) 40 | chunks <- doc_structure[sapply(doc_structure, class) == "block"] 41 | return(chunks) 42 | } 43 | 44 | select_chunk <- function(chunks, index) { 45 | payload = list(params = chunks[[index]]$params, 46 | code = chunks[[index]]$input, 47 | pd = build_pd(chunks[[index]]$input)) 48 | return(payload) 49 | } 50 | 51 | parse_yaml <- function(code) { 52 | rmarkdown:::parse_yaml_front_matter(strsplit(code, split = "\n")[[1]]) 53 | } 54 | 55 | 56 | check_equal_option_helper <- function(state, type, incorrect_msg, append) { 57 | student_option <- state$get("student_option") 58 | solution_option <- state$get("solution_option") 59 | state$add_details(type = type, 60 | case = "equal", 61 | student = student_option, 62 | solution = solution_option, 63 | message = incorrect_msg, 64 | append = append) 65 | 66 | check_that(is_equal(student_option, solution_option, "equal"), 67 | feedback = state$details) 68 | 69 | return(state) 70 | } -------------------------------------------------------------------------------- /R/check-that.R: -------------------------------------------------------------------------------- 1 | #' Expectation wrapper 2 | #' 3 | #' This function wraps around an is_... function. When the expectation fails to 4 | #' be met, the feedback message is sent to the reporter. You can use 5 | #' \code{\link{is_true}}, \code{\link{is_false}}, \code{\link{is_gte}} or 6 | #' \code{\link{is_equal}} 7 | #' 8 | #' @param code The expectation that should be wrapped 9 | #' @param feedback A character string with feedback when the expection is not 10 | #' met OR a list object, containing multiple pieces of information. This list 11 | #' should at least contain an element named \code{message} 12 | #' @param env environment in which the test should be evaluated; defaults to \code{parent.frame()} 13 | #' 14 | #' @examples 15 | #' \dontrun{ 16 | #' check_that(is_true(3 == 3)) 17 | #' check_that(is_false(3 == 4)) 18 | #' check_that(is_gte(4, 3)) 19 | #' check_that(is_equal(4, 4)) 20 | #' } 21 | #' @name check_that 22 | 23 | #' @rdname check_that 24 | #' @export 25 | check_that <- function(code, feedback, env = parent.frame()) { 26 | 27 | # feedback can be a character string 28 | if (is.character(feedback)) { 29 | feedback <- list(list(message = feedback)) 30 | } 31 | 32 | stopifnot(is.list(feedback), is.list(feedback[[1]])) 33 | 34 | res <- try(eval(code, envir = env), silent = TRUE) 35 | if (!isTRUE(res)) { 36 | throw_sct_failure(feedback = feedback, 37 | message = build_feedback_message(feedback)) 38 | } 39 | } 40 | 41 | throw_sct_failure <- function(message, feedback, call = sys.call(-1)) { 42 | sct_failed_msg <- 43 | c <- structure( 44 | class = c("sct_failure", "error", "condition"), 45 | list(message = message, call = call), 46 | feedback = feedback) 47 | stop(c) 48 | } 49 | 50 | #' @rdname check_that 51 | #' @export 52 | test_what <- function(code, feedback) { 53 | fail_if_v2_only() 54 | lut <- list(expect_true = call("is_true"), 55 | expect_false = call("is_false"), 56 | expect_equal = call("is_equal")) 57 | call <- substitute(code) 58 | call[1] <- lut[[as.character(call[[1]])]] 59 | check_that(call, feedback, env = parent.frame()) 60 | } -------------------------------------------------------------------------------- /R/check-wd.R: -------------------------------------------------------------------------------- 1 | #' Check whether a file exists 2 | #' 3 | #' @param path Path to the file you want to check 4 | #' @param missing_msg Custom feedback message in case the file is missing 5 | #' @param state the state to start from 6 | #' 7 | #' @examples 8 | #' \dontrun{ 9 | #' # Example 1 solution code: 10 | #' # write("hello", file = "test.txt") 11 | #' 12 | #' # SCT 13 | #' ex() %>% check_wd("test.txt") 14 | #' } 15 | #' @export 16 | check_wd <- function(state, path, missing_msg = NULL) { 17 | assert_state(state) 18 | file_state <- FileState$new(state) 19 | file_state$add_details(type = 'file', 20 | case = 'available', 21 | file = basename(path), 22 | folder = dirname(path), 23 | message = missing_msg) 24 | check_that(is_true(file.exists(path)), feedback = file_state$details) 25 | return(state) 26 | } 27 | 28 | test_file_exists <- function(path, incorrect_msg = NULL) { 29 | fail_if_v2_only() 30 | ex() %>% check_wd(path = path, missing_msg = incorrect_msg) 31 | } 32 | -------------------------------------------------------------------------------- /R/comparison.R: -------------------------------------------------------------------------------- 1 | #' Check if object is true, false, >= or <. 2 | #' 3 | #' Utility functions to use inside \code{\link{check_that}}. 4 | #' 5 | #' @param x object to test 6 | #' @param y single numeric value to compare 7 | #' @name compare 8 | 9 | #' @rdname compare 10 | #' @export 11 | is_true <- function(x) { 12 | identical(as.vector(x), TRUE) 13 | } 14 | 15 | #' @rdname compare 16 | #' @export 17 | is_false <- function(x) { 18 | identical(as.vector(x), FALSE) 19 | } 20 | 21 | #' @rdname compare 22 | #' @export 23 | is_gte <- function(x, y) { 24 | stopifnot(is.numeric(x), length(x) == 1) 25 | stopifnot(is.numeric(y), length(y) == 1) 26 | x >= y 27 | } 28 | 29 | #' @rdname compare 30 | #' @export 31 | is_lt <- function(x, y) { 32 | stopifnot(is.numeric(x), length(x) == 1) 33 | stopifnot(is.numeric(y), length(y) == 1) 34 | x < y 35 | } 36 | -------------------------------------------------------------------------------- /R/is-equal.R: -------------------------------------------------------------------------------- 1 | invalid_eq_condition <- "eq_condition should be either 'equivalent', 'equal' or 'identical'." 2 | 3 | #' Check equality of two objects 4 | #' 5 | #' Utility function to use inside \code{\link{check_that}}. 6 | #' 7 | #' @param x object to test 8 | #' @param y object to compare 9 | #' @param eq_condition how to compare the objects: \code{"equivalent"} (the default, 10 | #' does not check attributes), \code{"equal"} (checks attributes, but allows for 11 | #' errors in machine precision), or \code{"identical"} (exactly identical). 12 | #' @name is_equal 13 | 14 | #' @rdname is_equal 15 | #' @export 16 | is_equal <- function(x, y, eq_condition = "equivalent") { 17 | UseMethod("is_equal", x) 18 | } 19 | 20 | #' @rdname is_equal 21 | #' @export 22 | is_equal.default <- function(x, y, eq_condition = "equivalent") { 23 | eq_fun <- switch(eq_condition, 24 | equivalent = function(x, y) isTRUE(try(all.equal(x, y, check.attributes = FALSE), silent = TRUE)), 25 | equal = function(x, y) isTRUE(try(all.equal(x, y), silent = TRUE)), 26 | identical = identical, 27 | stop(invalid_eq_condition)) 28 | eq_fun(x, y) 29 | } 30 | 31 | #' @rdname is_equal 32 | #' @export 33 | is_equal.formula <- function(x, y, eq_condition = "equivalent") { 34 | tryCatch({ 35 | xlst <- convert_formula(x) 36 | ylst <- convert_formula(y) 37 | isTRUE(all.equal(xlst$target, ylst$target)) && 38 | isTRUE(all.equal(xlst$explan, ylst$explan)) 39 | }, error = function(e) { 40 | # fallback to default equality 41 | is_equal.default(x, y, eq_condition) 42 | }) 43 | } 44 | 45 | convert_formula <- function(form) { 46 | n <- length(form) 47 | deparsed <- lapply(form, deparse)[2:n] 48 | target <- deparsed[[1]] 49 | explan <- deparsed[[2]] 50 | trm <- function(x) gsub("^\\s+|\\s+$", "", x) 51 | explan <- sort(trm(strsplit(explan, "\\+")[[1]])) 52 | return(list(target = target, explan = explan)) 53 | } 54 | -------------------------------------------------------------------------------- /R/messages-utils.R: -------------------------------------------------------------------------------- 1 | trim <- function(x) { 2 | gsub("^\\s+|\\s+$", "", x) 3 | } 4 | 5 | capitalize <- function(x) { 6 | enders <- c(".", "?", "!") 7 | for (e in enders) { 8 | x <- strsplit(x, split = sprintf("\\%s\\s", e))[[1]] 9 | x <- paste0(toupper(substring(x, 1, 1)), substring(x, 2), collapse = sprintf("%s ", e)) 10 | } 11 | return(x) 12 | } 13 | 14 | # Nicely collapse a character vector 15 | collapse <- function(x, conn = " and ") { 16 | if (length(x) > 1) { 17 | n <- length(x) 18 | last <- c(n - 1, n) 19 | collapsed <- paste(x[last], collapse = conn) 20 | collapsed <- paste(c(x[-last], collapsed), collapse = ", ") 21 | } else collapsed <- x 22 | collapsed 23 | } 24 | 25 | collapse_args <- function(x, conn = " and ") { 26 | collapse(paste0("`",x,"`"), conn) 27 | } 28 | 29 | collapse_props <- collapse_args 30 | 31 | collapse_funs <- function(x, conn = " and ") { 32 | collapse(paste0("`",x,"()`"), conn) 33 | } 34 | 35 | set_language <- function(lang) { 36 | message("Different languages are no longer supported in testwhat") 37 | } 38 | 39 | get_ord <- function(index) { 40 | switch(index, 41 | "1" = "first", "2" = "second", 42 | "3" = "third", "4" = "fourth", 43 | "5" = "fifth", "6" = "sixth", 44 | "7" = "seventh", sprintf("%ith", index)) 45 | } 46 | 47 | get_num <- function(index) { 48 | switch(index, 49 | "1" = "one", "2" = "two", 50 | "3" = "three", "4" = "four", 51 | "5" = "five", sprintf("%i", index)) 52 | } 53 | 54 | get_times <- function(index) { 55 | switch(index, 56 | "1" = "", "2" = " twice", 57 | sprintf(" %s times", get_num(index))) 58 | } 59 | 60 | yaml_option_desc <- function(name) { 61 | paste0("`", paste0(name, collapse = ":"), "`") 62 | } 63 | -------------------------------------------------------------------------------- /R/parse-status.R: -------------------------------------------------------------------------------- 1 | #' @importFrom R6 R6Class 2 | ParseStatus <- R6::R6Class("ParseStatus", 3 | public = list( 4 | initialize = function() {}, 5 | # push_state = function(state) { 6 | # private$states <- c(private$states, list(state)) 7 | # }, 8 | # pop_state = function() { 9 | # n <- length(private$states) 10 | # if (n == 0) stop("no more states") 11 | # tail <- private$states[[n]] 12 | # private$states <- private$states[-n] 13 | # tail 14 | # }, 15 | push_bracket = function(token) { 16 | private$bracket_stack <- c(private$bracket_stack, list(token)) 17 | }, 18 | pop_bracket = function(rhs) { 19 | n <- length(private$bracket_stack) 20 | if (n == 0) { 21 | self$add_lint(rhs, sprintf("This %s wasn't expected.", human_name(rhs$type))) 22 | return(NULL) 23 | } 24 | 25 | lhs <- private$bracket_stack[[n]] 26 | if (complement(lhs$type) != rhs$type) { 27 | self$add_lint(lhs, sprintf("Make sure to correctly close this %s.", human_name(lhs$type))) 28 | return(NULL) 29 | } 30 | 31 | private$bracket_stack <- private$bracket_stack[-n] 32 | }, 33 | 34 | add_lint = function(token, message) { 35 | private$lints <- c(private$lints, 36 | list(c(token$location, message = paste(fail_msg, message)))) 37 | }, 38 | 39 | lint_present = function() { 40 | length(private$lints) > 0 41 | }, 42 | 43 | get_lint = function(index = 1) { 44 | private$lints[[index]] 45 | }, 46 | 47 | get_finish_lint = function() { 48 | n <- length(private$bracket_stack) 49 | if (n > 0) { 50 | token <- private$bracket_stack[[n]] 51 | return(c(token$location, list(message = paste(fail_msg, sprintf("Don't forget to close this %s.", human_name(token$type)))))) 52 | } else { 53 | return(list(message = parse_fallback_msg)) 54 | } 55 | } 56 | ), 57 | private = list( 58 | states = list(), 59 | bracket_stack = list(), 60 | lints = list() 61 | ) 62 | ) -------------------------------------------------------------------------------- /R/parse-token.R: -------------------------------------------------------------------------------- 1 | #' @importFrom R6 R6Class 2 | RToken <- R6::R6Class("RToken", 3 | public = list( 4 | type = character(0), 5 | location = list(), 6 | initialize = function(type, line_start, column_start, line_end, column_end) { 7 | self$type = type 8 | self$location = list(line_start = line_start, 9 | column_start = column_start, 10 | line_end = line_end, 11 | column_end = column_end) 12 | } 13 | ) 14 | ) 15 | 16 | #' @importFrom R6 R6Class 17 | RTokenCursor <- R6::R6Class("RTokenCursor", 18 | public = list( 19 | tokens = NULL, 20 | initialize = function(code) { 21 | tokenizer <- RTokenizer$new(code) 22 | token <- tokenizer$next_token() 23 | tokens <- list() 24 | while (!is.null(token)) { 25 | # do away with comments 26 | if (token$type != "COMMENT") { 27 | tokens <- c(tokens, list(token)) 28 | } 29 | token <- tokenizer$next_token() 30 | } 31 | self$tokens <- tokens 32 | private$offset <- 1 33 | private$n <- length(tokens) 34 | }, 35 | move_up = function() { 36 | if (self$is_at_eod()) { 37 | return(invisible(NULL)) 38 | } else { 39 | private$offset <- private$offset + 1 40 | return(invisible(self$current_token())) 41 | } 42 | }, 43 | move_to_next_token = function() { 44 | current <- self$move_up() 45 | while (current$type %in% c("OTHER", "WHITESPACE", "COMMENT", "STRING") && !self$is_at_eod()) { 46 | current <- self$move_up() 47 | } 48 | }, 49 | is_at_eod = function() { 50 | private$offset == private$n 51 | # TODO make more complete (borrow from RTokenCursor::isAtEndOfDocument) 52 | }, 53 | current_token = function() { 54 | self$tokens[[private$offset]] 55 | }, 56 | is_type = function(types) { 57 | self$current_token()$type %in% types 58 | }, 59 | get_type = function() { 60 | self$current_token()$type 61 | } 62 | ), 63 | private = list( 64 | offset = integer(0), 65 | n = integer(0) 66 | ) 67 | ) -------------------------------------------------------------------------------- /R/parse-utils.R: -------------------------------------------------------------------------------- 1 | is_white_space_or_comment <- function(token) { 2 | token$type == "WHITESPACE" || token$type == "COMMENT" 3 | } 4 | 5 | complement <- function(type) { 6 | switch(type, 7 | LPAREN = "RPAREN", 8 | RPAREN = "LPAREN", 9 | LBRACE = "RBRACE", 10 | RBRACE = "LBRACE", 11 | LDBRACKET = "RDBRACKET", 12 | RDBRACKET = "LDBRACKET", 13 | LBRACKET = "RBRACKET", 14 | RBRACKET = "LBRACKET", 15 | "ERROR") 16 | } 17 | 18 | human_name <- function(type) { 19 | switch(type, 20 | LPAREN = "parenthesis", 21 | RPAREN = "parenthesis", 22 | LBRACE = "curly bracket", 23 | RBRACE = "curly bracket", 24 | LDBRACKET = "double bracket", 25 | RDBRACKET = "double bracket", 26 | LBRACKET = "bracket", 27 | RBRACKET = "bracket", 28 | "ERROR") 29 | } 30 | 31 | fail_msg <- "Have a look at the highlighted code in the editor." 32 | parse_fallback_msg <- "Your code contains a syntax error. Check the console output and try to fix the issue." 33 | -------------------------------------------------------------------------------- /R/parse.R: -------------------------------------------------------------------------------- 1 | do_parse <- function(code) { 2 | cursor <- RTokenCursor$new(code) 3 | status <- ParseStatus$new() 4 | 5 | if (is.null(cursor$tokens)) { 6 | return(status$get_finish_lint()) 7 | } 8 | 9 | start <- function() { 10 | 11 | if (cursor$is_type("INCOMPLETE_STRING")) { 12 | status$add_lint(cursor$current_token(), "Make sure to close the string again!") 13 | } 14 | 15 | if (cursor$is_type("FILL_IN")) { 16 | status$add_lint(cursor$current_token(), "Replace it with valid R code!") 17 | } 18 | 19 | if (cursor$is_type(c("LPAREN", "LBRACE", "LBRACKET", "LDBRACKET"))) { 20 | status$push_bracket(cursor$current_token()) 21 | # type <- cursor$get_type() 22 | # state <- paste0("WITHIN_", substr(type, 2, nchar(type)), "S") 23 | # status$push_state(state) 24 | } 25 | 26 | if (cursor$is_type(c("RPAREN", "RBRACKET", "RDBRACKET", "RBRACE"))) { 27 | # Leave out the state stuff for now 28 | status$pop_bracket(cursor$current_token()) 29 | # status$pop_state() 30 | } 31 | 32 | if (status$lint_present()) { 33 | return(status$get_lint()) 34 | } 35 | 36 | if (cursor$is_at_eod()) { 37 | return(status$get_finish_lint()) 38 | } 39 | 40 | cursor$move_to_next_token() 41 | start() 42 | } 43 | 44 | start() 45 | } 46 | 47 | -------------------------------------------------------------------------------- /R/reporter.R: -------------------------------------------------------------------------------- 1 | get_line_info <- function(feedback) { 2 | 3 | # take 'highest pd' in list of feedback 4 | pd <- NULL 5 | disabled <- sapply(feedback, function(det) isTRUE(det$highlighting_disabled)) 6 | for (i in length(feedback):1) { 7 | if (!is.null(feedback[[i]]$pd)) { 8 | # if something found, check that it's not behind highlighting disabled 9 | if (!any(disabled) || which(disabled) > i) { 10 | pd <- feedback[[i]][["pd"]] 11 | break 12 | } 13 | } 14 | } 15 | 16 | if (!isTRUE(try(is.data.frame(pd), silent = TRUE))) { 17 | return(NULL) 18 | } 19 | 20 | id <- pd$id[!(pd$parent %in% pd$id)] 21 | if (length(id) > 1) { 22 | return(list(line_start = min(pd$line1), 23 | column_start = min(pd$col1), 24 | line_end = max(pd$line2), 25 | column_end = max(pd$col2))) 26 | } 27 | x <- as.list(pd[pd$id == id, c("line1", "col1", "line2", "col2")]) 28 | names(x) <- c("line_start", "column_start", "line_end", "column_end") 29 | x 30 | } 31 | 32 | #' @importFrom markdown markdownToHTML 33 | to_html <- function(x) { 34 | html <- markdownToHTML(text = x, fragment.only = TRUE) 35 | gsub("

(.*?)

", "\\1", html) #remove

tags, coded by front end. 36 | } 37 | -------------------------------------------------------------------------------- /R/rstudio-test-chunk-options.R: -------------------------------------------------------------------------------- 1 | #' Check whether the student defined the correct chunk options (R Markdown 2 | #' exercises) 3 | #' 4 | #' Check whether the student defined the correct chunk options in an R Markdown 5 | #' exercise 6 | #' 7 | #' This test can only be called inside a test_rmd_group() call! 8 | #' 9 | #' @param options Set of options 10 | #' @param not_called_msg feedback message if option was not specified 11 | #' @param incorrect_msg feedback message if option was incorrectly set 12 | #' @keywords internal 13 | test_chunk_options <- function(options = NULL, 14 | not_called_msg = NULL, 15 | incorrect_msg = NULL) { 16 | fail_if_v2_only() 17 | state <- ex() 18 | chunk_number <- state$get("chunk_number") 19 | student_chunk <- state$get("student_ds_part") 20 | solution_chunk <- state$get("solution_ds_part") 21 | 22 | # First, check if both student and solution chunk are 'block' class (i.e. code chunks) 23 | if (class(solution_chunk) != "block") { 24 | stop("The specified rmd group is not of 'block' class.") 25 | } 26 | 27 | check_that(is_equal(class(student_chunk), "block"), "Wrong class student chunk") 28 | 29 | sol_options <- solution_chunk$params 30 | stud_options <- student_chunk$params 31 | 32 | if (is.null(options)) { 33 | options <- names(sol_options) 34 | if (length(options) == 0) { 35 | return(TRUE) 36 | } 37 | } 38 | 39 | # Set up default messages 40 | # message if specified function was not called 41 | if (is.null(not_called_msg)) { 42 | not_called_msg = sprintf("Code chunk %i of your submission should contain the option%s %s.", 43 | chunk_number, if (length(options) == 1) "" else "s", collapse_props(options)) 44 | } 45 | 46 | # message if the properties or not found or set incorrectly 47 | if (is.null(incorrect_msg)) { 48 | incorrect_msg = sprintf("In code chunk %i of your submission, make sure to correctly define the option%s %s.", 49 | chunk_number, if (length(options) == 1) "" else "s", collapse_props(options)) 50 | } 51 | 52 | # select from sol_options and stud_props the ones to check on 53 | # 54 | # reverse the list, because in case options are defined multiple times, 55 | # the last options are the ones that are seen as valid by RMarkdown 56 | sol_options_select = rev(sol_options)[options] 57 | stud_options_select = rev(stud_options)[options] 58 | if (any(is.na(names(sol_options_select)))) { 59 | stop(sprintf("You defined options that are not in code chunk %i of the solution", chunk_number)) 60 | } 61 | 62 | no_nas <- any(is.na(names(stud_options_select))) 63 | # check if all options available 64 | check_that(is_false(no_nas), not_called_msg) 65 | 66 | 67 | # check the equality of stud and solution options. 68 | if (!no_nas) { 69 | check_that(is_equal(sol_options_select, stud_options_select), incorrect_msg) 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /R/rstudio-test-pipe.R: -------------------------------------------------------------------------------- 1 | #' Check whether a student used the pipe operator sufficiently (ggvis and dplyr exercises) 2 | #' 3 | #' Check whether a student used the pipe sufficiently. By default, the function only checks if 4 | #' the pipe was used at least once. The user can also select the minimal 5 | #' number of occurrences of the pipe. 6 | #' 7 | #' @param num minimal number of times the pipe operator has to appear (default = 1) 8 | #' @param absent_msg feedback message in case the student did not use a single pipe. 9 | #' @param insuf_msg feeback message in case the student did not use the pipe operator sufficiently. 10 | #' @keywords internal 11 | test_pipe <- function(num = 1, absent_msg = NULL, insuf_msg = NULL) { 12 | fail_if_v2_only() 13 | if (is.null(insuf_msg)) { 14 | insuf_msg = sprintf("You should use the pipe operator at least %i times in total in your solution code",num) 15 | } 16 | ex() %>% check_code(regex = "%>%", times = num, missing_msg = insuf_msg) 17 | } 18 | -------------------------------------------------------------------------------- /R/rstudio-test-rmd-group.R: -------------------------------------------------------------------------------- 1 | #' Test a single R Markdown file group (R Markdown exercises) 2 | #' 3 | #' Test a single R Markdown file group (R Markdown exercises) with arbitrary testwhat functions. 4 | #' 5 | #' @param group_number Number of the group. 6 | #' @param code SCT code to test the group (in curly braces) 7 | #' @keywords internal 8 | test_rmd_group <- function(group_number, code) { 9 | fail_if_v2_only() 10 | old_state <- ex() 11 | on.exit(tw$set(state = old_state)) 12 | test_env <- old_state$get("test_env") 13 | tw$set(state = get_rmd_group(old_state, group_number)) 14 | eval(substitute(code), envir = test_env) 15 | } 16 | 17 | get_rmd_group <- function(state, group_number) { 18 | student_code <- state$get("student_code") 19 | solution_code <- state$get("solution_code") 20 | 21 | group_state <- MarkdownState$new(state) 22 | group_state <- parse_docs(group_state) 23 | 24 | solution_ds <- group_state$get("solution_ds") 25 | student_ds <- group_state$get("student_ds") 26 | 27 | if (group_number > length(solution_ds)) { 28 | stop(sprintf("Invalid group_number (%s), while solution contains only %s parts", 29 | group_number,length(solution_ds))) 30 | } 31 | 32 | student_ds_part <- student_ds[[group_number]] 33 | solution_ds_part <- solution_ds[[group_number]] 34 | group_state$set(student_ds_part = student_ds_part, 35 | solution_ds_part = solution_ds_part, 36 | student_code = student_ds_part$input, 37 | solution_code = solution_ds_part$input) 38 | 39 | # set numbers, to be used in default messages of tests 40 | if (class(student_ds_part) == "block") { 41 | group_state$set(chunk_number = group_number - sum(sapply(student_ds[1:group_number],class) == "inline"), 42 | student_pd = build_pd(student_ds_part$input), 43 | solution_pd = build_pd(solution_ds_part$input)) 44 | } else if (class(student_ds_part) == "inline") { 45 | group_state$set(inline_number = group_number - sum(sapply(student_ds[1:group_number],class) == "block")) 46 | } 47 | return(group_state) 48 | } 49 | 50 | -------------------------------------------------------------------------------- /R/rstudio-test-text.R: -------------------------------------------------------------------------------- 1 | #' Test inline text and formatting (Markdown) 2 | #' 3 | #' Test inline text and its formatting for R Markdown exercises. 4 | #' This test can only be called inside a test_rmd_group() call! 5 | #' 6 | #' @param text Text to match (can be a regular expression!) 7 | #' @param format the format of the text that the text should be in ("any", "italics", "bold", "code", "inline_code", "brackets", "parentheses", "list"). 8 | #' If none of the above, the format string is appended to text in front and in the back and used as a regexp. 9 | #' @param freq How often the text should appear with this formatting 10 | #' @param not_called_msg feedback message if the text was not there 11 | #' @param incorrect_msg feedback message if the text was not properly formatted 12 | #' 13 | #' @keywords internal 14 | test_text <- function(text, 15 | format = "any", 16 | freq = 1, 17 | not_called_msg = NULL, 18 | incorrect_msg = NULL) { 19 | state <- ex() 20 | inline_number <- state$get("inline_number") 21 | student_inline <- state$get("student_ds_part") 22 | solution_inline <- state$get("solution_ds_part") 23 | 24 | # First, check if both student and solution chunk are 'inline' class 25 | if(class(solution_inline) != "inline") { 26 | stop("The specified rmd group is not of 'inline' class.") 27 | } 28 | check_that(is_equal(class(student_inline), "inline"), 29 | feedback = "The student rmd group is not inline!") 30 | 31 | # Set up default messages 32 | # message if text was not found 33 | if(is.null(not_called_msg)) { 34 | not_called_msg = sprintf("Inline group %i of your submission should contain the text \"%s\".", 35 | inline_number, text) 36 | } 37 | 38 | # message if the properties or not found or set incorrectly 39 | if(is.null(incorrect_msg)) { 40 | incorrect_msg = sprintf("In inline group %i of your submission, make sure to correctly format the text \"%s\".", 41 | inline_number, text) 42 | } 43 | 44 | # first, check if text is found in student_inline 45 | hits <- gregexpr(pattern = text, student_inline$input)[[1]] 46 | check_that(is_false(hits[1] == -1), feedback = not_called_msg) 47 | check_that(is_gte(length(hits), freq), feedback = not_called_msg) 48 | 49 | if(hits[1] == -1 || length(hits) < freq) { 50 | return(FALSE) 51 | } 52 | 53 | patt <- text; 54 | for(i in seq_along(format)) { 55 | if(format[i] == "any") return(TRUE) 56 | else if(format[i] == "italics") patt <- paste0("((\\*",patt,"\\*)|(_",patt,"_))") 57 | else if(format[i] == "bold") patt <- paste0("((\\*{2}",patt,"\\*{2})|(_{2}",patt,"_{2}))") 58 | else if(format[i] == "code") patt <- paste0("`",patt,"`") 59 | else if(format[i] == "inline_code") patt <- paste0("`r\\s*",patt,"`") 60 | else if(format[i] == "brackets") patt <- paste0("\\[",patt,"\\]") 61 | else if(format[i] == "parentheses") patt <- paste0("\\(",patt,"\\)") 62 | else if(format[i] == "list") patt <- paste0("[-\\*]\\s*",patt) 63 | else patt <- paste0(format[i],patt,format[i]) 64 | } 65 | 66 | hits = gregexpr(pattern = patt, student_inline$input)[[1]] 67 | check_that(is_false(hits[1] == -1), feedback = incorrect_msg) 68 | check_that(is_gte(length(hits), freq), feedback = not_called_msg) 69 | } 70 | -------------------------------------------------------------------------------- /R/rstudio-test-yaml-header.R: -------------------------------------------------------------------------------- 1 | #' Check yaml header (Markdown) 2 | #' 3 | #' Check whether the student specified the correct options in the yaml header (for 4 | #' R Markdown exercises). This test should be called outside an test_rmd_group call. 5 | #' 6 | #' @param options Set of options. Embedded options have to be specified using the dot notation. 7 | #' @param check_equality whether or not to actually check the value assigned to the option (default TRUE) 8 | #' @param not_called_msg feedback message if option was not specified (optional but recommended) 9 | #' @param incorrect_msg feedback message if option was incorrectly set (optional but recommended) 10 | #' @keywords internal 11 | test_yaml_header <- function(options = NULL, 12 | check_equality = TRUE, 13 | not_called_msg = NULL, 14 | incorrect_msg = NULL) { 15 | fail_if_v2_only() 16 | state <- ex() 17 | chunk_number <- state$get("chunk_number") 18 | student_code <- state$get("student_code") 19 | solution_code <- state$get("solution_code") 20 | 21 | yaml_solution <- try(unlist(rmarkdown:::parse_yaml_front_matter(strsplit(solution_code, split = "\n")[[1]])), silent = TRUE) 22 | if(inherits(yaml_solution, "try-error")) { 23 | stop("Something wrong with yaml header of solution code!") 24 | } 25 | 26 | yaml_student <- try(unlist(rmarkdown:::parse_yaml_front_matter(strsplit(student_code, split = "\n")[[1]])), silent = TRUE) 27 | check_that(is_false(inherits(yaml_student, "try-error")), 28 | feedback = "Make sure the YAML header contains no errors. Beware of erroneous indentation.") 29 | 30 | if(is.null(options)) { 31 | options <- names(yaml_solution) 32 | if(length(options) == 0) { 33 | return(TRUE) 34 | } 35 | } 36 | 37 | # Set up default messages 38 | # message if specified function was not called 39 | if(is.null(not_called_msg)) { 40 | not_called_msg <- sprintf("The YAML header of your submission should contain the option%s %s.", 41 | if(length(options) == 1) "" else "s", collapse_props(options)) 42 | } 43 | 44 | # message if the properties are not found or set incorrectly 45 | if(is.null(incorrect_msg)) { 46 | incorrect_msg = sprintf("In your YAML header, correctly define the option%s %s.", 47 | if(length(options) == 1) "" else "s", collapse_props(options)) 48 | } 49 | 50 | # select from sol_options and stud_props the ones to check on 51 | sol_options_select = yaml_solution[options] 52 | stud_options_select = yaml_student[options] 53 | if(any(is.na(names(sol_options_select)))) { 54 | stop(sprintf("You want to test on yaml options that are not in the solution's yaml header", chunk_number)) 55 | } 56 | 57 | no_nas = any(is.na(names(stud_options_select))) 58 | # check if all options available 59 | check_that(is_false(no_nas), feedback = not_called_msg) 60 | 61 | if(!no_nas && check_equality) { 62 | check_that(is_equal(sol_options_select, stud_options_select), feedback = incorrect_msg) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /R/success-msg.R: -------------------------------------------------------------------------------- 1 | #' Define the success message 2 | #' 3 | #' If all tests in an SCT pass, the student is presented with a 4 | #' congratulatory message. You can specify this message with 5 | #' /code{success_msg()}. It does not matter where in the SCT 6 | #' you specify this message, but at the end makes most sense. 7 | #' 8 | #' For multiple choice exercises, the success message is specified 9 | #' inside \code{\link{check_mc}}, so an additional call of \code{success_msg} 10 | #' is not necessary. 11 | #' 12 | #' @param msg The success message as a character string. 13 | #' @param praise Whether or not to prepend a message of praise from the \code{praise} package. 14 | #' 15 | #' @export 16 | #' @importFrom praise praise 17 | success_msg <- function(msg, praise = FALSE) { 18 | if (isTRUE(praise)) { 19 | msg <- paste(praise::praise(), msg) 20 | } 21 | tw$set(success_msg = capitalize(trim(msg))) 22 | } 23 | -------------------------------------------------------------------------------- /R/test-exercise.R: -------------------------------------------------------------------------------- 1 | #' Run all tests for an exercise 2 | #' 3 | #' Run all tests for an exercise and report the results (including feedback). 4 | #' This function is run by R Backend and should not be used by course creators. 5 | #' 6 | #' @param sct Submission correctness tests as a character string. 7 | #' @param ex_type Type of the exercise 8 | #' @param pec pre-exercise-code 9 | #' @param student_code character string representing the student code 10 | #' @param solution_code character string representing the solution code 11 | #' @param student_env environment containing the objects defined by the student. 12 | #' @param solution_env environment containing the objects defined by solution code 13 | #' @param output_list the output structure that is generated by RBackend 14 | #' @param allow_errors whether or not errors are allowed by RBackend (FALSE by default) 15 | #' @param force_diagnose whether diagnose tests have to pass even if the checks pass (FALSE by default) 16 | #' @param seed random seed that is used for SCTs that run expressions (42 by default). 17 | #' 18 | #' @return A list with components \code{passed} that indicates whether all 19 | #' tests were sucessful, and \code{feedback} that contains a feedback message. 20 | #' 21 | #' @export 22 | test_exercise <- function(sct, 23 | ex_type, 24 | pec, 25 | student_code, 26 | solution_code, 27 | student_env, 28 | solution_env, 29 | output_list, 30 | allow_errors = FALSE, 31 | force_diagnose = FALSE, 32 | seed = 42) { 33 | # backwards compatibility with older versions of RBackend 34 | if (missing(student_env)) { 35 | student_env <- globalenv() 36 | } 37 | 38 | # First check if parsing worked out 39 | if (any(sapply(output_list, `[[`, "type") == "parse-error")) { 40 | report <- tryCatch(do_parse(student_code), 41 | error = function(e) { 42 | list(message = parse_fallback_msg) 43 | }) 44 | return(c(list(correct = FALSE), report)) 45 | } else { 46 | # Store everything that's needed locally (initialize does a full reset) 47 | tw$clear() 48 | tw$set(success_msg = sample(c("Good Job!", "Well done!", "Great work!"), 1)) 49 | state <- RootState$new(pec = pec, 50 | student_code = student_code, 51 | student_pd = build_pd(student_code), 52 | student_env = student_env, 53 | solution_code = solution_code, 54 | solution_pd = build_pd(solution_code), 55 | solution_env = solution_env, 56 | output_list = output_list, 57 | test_env = new.env(parent = environment()), 58 | force_diagnose = force_diagnose) 59 | tw$set(state = state, 60 | stack = TRUE, 61 | seed = seed) 62 | on.exit(tw$clear()) 63 | 64 | # Execute sct with the DataCamp reporter such that it collects test results 65 | res <- run_until_fail(parse(text = sct)) 66 | 67 | # If the SCT passed, check whether there is an error as the last step 68 | if (isTRUE(res$correct) && !allow_errors) { 69 | res <- run_until_fail(ex() %>% check_error()) 70 | } 71 | 72 | return(post_process(res, ex_type)) 73 | } 74 | } 75 | 76 | #' Run SCT until it fails 77 | #' 78 | #' @param code the SCT script to run as an expression 79 | #' 80 | #' @export 81 | run_until_fail <- function(code) { 82 | tryCatch({ 83 | # Run the SCT 84 | eval(code, envir = tw$get("state")$get("test_env")) 85 | # If it got here, the SCT passed 86 | return(list(correct = TRUE, message = tw$get("success_msg"))) 87 | }, sct_failure = function(e) { 88 | return(list(correct = FALSE, 89 | message = e$message, 90 | feedback = attr(e, "feedback"))) 91 | }) 92 | } 93 | 94 | 95 | post_process <- function(res, ex_type) { 96 | # convert to HTML 97 | res$message <- to_html(res$message) 98 | 99 | # Only add line info if: 100 | # - message is incorrect, and 101 | # - exercise is not markdown / rcpp (post-processing of code chunks gives strange things) 102 | if (!res$correct && !(ex_type %in% c("MarkdownExercise", "RCppExercise"))) { 103 | line_info <- get_line_info(res$feedback) 104 | if (!is.null(line_info)) { 105 | res <- c(res, line_info) 106 | } 107 | } 108 | res$feedback <- NULL 109 | return(res) 110 | } -------------------------------------------------------------------------------- /R/test_an_object.R: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | test_an_object <- function(name, 4 | undefined_msg = NULL, 5 | eq_condition = "equivalent") { 6 | fail_if_v2_only() 7 | 8 | # Get needed elements from tw 9 | student_env <- ex()$get("student_env") 10 | solution_env <- ex()$get("solution_env") 11 | 12 | if (is.null(undefined_msg)) { 13 | # Avoid returning this message, always set undefined_msg 14 | undefined_msg <- "There is some object missing in your code." 15 | } 16 | 17 | check_defined(name, solution_env) 18 | solution <- get(name, envir = solution_env, inherits = FALSE) 19 | 20 | valid_values <- list() 21 | length(valid_values) <- length(ls(student_env)) 22 | 23 | counter <- 1 24 | for (student_var in ls(student_env)) { 25 | student_value <- get(student_var, envir = student_env, inherits = FALSE) 26 | if (identical(class(student_value), class(solution))) { 27 | valid_values[[counter]] <- student_value 28 | counter <- counter + 1 29 | } 30 | } 31 | 32 | if (counter > 1) { 33 | correct <- vapply(valid_values[1:counter-1], function(x) { is_equal(x, solution, eq_condition) }, logical(1)) 34 | } else { 35 | correct <- FALSE 36 | } 37 | 38 | check_that(is_true(any(correct)), feedback = undefined_msg) 39 | } -------------------------------------------------------------------------------- /R/utils-control.R: -------------------------------------------------------------------------------- 1 | #' @importFrom utils getParseText 2 | extract_control <- function(pd, keyword, elnames) { 3 | if(any(pd$token == keyword)) { 4 | # Intersection of parent of ids with the correct keyword WITH 5 | # ids that are top-level (whose parent is not in pd) OR 6 | # ids of children of top-level parents that have curly brackets 7 | parents <- pd$parent[pd$token == keyword] 8 | top_level_ids <- pd$id[!(pd$parent %in% pd$id)] 9 | top_level_ids_with_curly_brackets <- pd$id[grepl("^\\s*\\{.*?\\}\\s*$", pd$text) & pd$id %in% top_level_ids] 10 | children_of_curly_brackets <- pd$id[pd$parent %in% top_level_ids_with_curly_brackets] 11 | ids <- base::intersect(parents, c(top_level_ids, children_of_curly_brackets)) 12 | 13 | chop_up_pd <- function(id, elnames) { 14 | expr_ids <- pd$id[pd$parent == id & pd$token %in% c("expr", "forcond")] 15 | sub_codes <- lapply(expr_ids, getParseText, parseData = pd) 16 | sub_pds <- lapply(expr_ids, get_sub_pd, pd = pd) 17 | out <- mapply(function(code, pd) list(code = code, pd = pd), sub_codes, sub_pds, SIMPLIFY = FALSE) 18 | names(out) <- elnames[1:length(out)] 19 | out 20 | } 21 | 22 | lapply(ids, chop_up_pd, elnames = elnames) 23 | } else { 24 | return(list()) 25 | } 26 | } 27 | 28 | extract_if <- function(pd) { 29 | extract_control(pd, keyword = "IF", elnames = c("cond_part", "if_part", "else_part")) 30 | } 31 | 32 | extract_for <- function(pd) { 33 | extract_control(pd, keyword = "FOR", elnames = c("cond_part", "expr_part")) 34 | } 35 | 36 | extract_while <- function(pd) { 37 | extract_control(pd, keyword = "WHILE", elnames = c("cond_part", "expr_part")) 38 | } 39 | 40 | 41 | -------------------------------------------------------------------------------- /R/utils-learnr.R: -------------------------------------------------------------------------------- 1 | #' A checker function to use with learnr 2 | #' 3 | #' For exercise checking, learnr tutorials require a function that learnr can 4 | #' use in the background to run the code in each "-check" chunk and to format 5 | #' the results into a format that learnr can display. The function must accept a 6 | #' specific set of inputs and return a specific type of output. Users are not 7 | #' intended to use the function themselves, but to pass it to the 8 | #' \code{exercise.checker} knitr chunk option within the setup chunk of the 9 | #' tutorial. 10 | #' 11 | #' Similar to grader's \code{grade_learnr()}, testwhat provides 12 | #' \code{testwhat_learnr()} for this purpose. To enable exercise checking in 13 | #' your learnr tutorial, set \code{tutorial_options(exercise.checker = 14 | #' testwhat_learnr)} in the setup chunk of your tutorial. 15 | #' 16 | #' @param label Label for exercise chunk 17 | #' @param solution_code R code submitted by the user 18 | #' @param user_code Code provided within the "-solution" chunk for the 19 | #' exercise. 20 | #' @param check_code Code provided within the "-check" chunk for the exercise. 21 | #' @param envir_result The R environment after the execution of the chunk. 22 | #' @param evaluate_result The return value from the \code{evaluate::evaluate} 23 | #' function. 24 | #' @param ... Unused (include for compatibility with parameters to be added in 25 | #' the future) 26 | #' 27 | #' @return An R list which contains several fields indicating the result of the 28 | #' check. 29 | #' @export 30 | testwhat_learnr <- function(label = NULL, 31 | solution_code = NULL, 32 | user_code = NULL, 33 | check_code = NULL, 34 | envir_result = NULL, 35 | evaluate_result = NULL, 36 | ...) { 37 | 38 | ######### START COPY FROM grade_learnr ################## 39 | # Sometimes no user code is provided, but 40 | # that means there is nothing to check. Also, 41 | # you do not want to parse NULL 42 | if (is.null(user_code)) { 43 | return(list( 44 | message = "I didn't receive your code. Did you write any?", 45 | correct = FALSE, 46 | type = "error", 47 | location = "append" 48 | )) 49 | } 50 | 51 | # Sometimes no solution is provided, but that 52 | # means there is nothing to check against. Also, 53 | # you do not want to parse NULL 54 | if (is.null(solution_code)) { 55 | return(list( 56 | message = "No solution is provided for this exercise.", 57 | correct = TRUE, 58 | type = "info", 59 | location = "append" 60 | )) 61 | } 62 | ######### END COPY FROM grade_learnr ################## 63 | 64 | setup_state(sol_code = solution_code, 65 | stu_code = user_code, 66 | sol_env = NULL, 67 | stu_env = envir_result, 68 | stu_result = evaluate_result) 69 | 70 | res <- run_until_fail(parse(text = check_code)) 71 | return(list(message = res$message, 72 | correct = res$correct, 73 | location = "append", 74 | type = if(res$correct) "success" else "error")) 75 | } 76 | -------------------------------------------------------------------------------- /R/utils-pd.R: -------------------------------------------------------------------------------- 1 | #' Build ParseData from string representing code 2 | #' 3 | #' @param code character string representing code 4 | #' @param silent if TRUE, parsing errors are caugt and NULL is returned. 5 | #' @importFrom utils getParseData 6 | #' @export 7 | build_pd <- function(code, silent=TRUE) { 8 | tryCatch(getParseData(parse(text = code, keep.source = TRUE), includeText = TRUE), 9 | error = function(e) { 10 | if (silent) { 11 | return(NULL) 12 | } else { 13 | stop(e) 14 | } 15 | }) 16 | } 17 | 18 | get_children <- function(pd, ids) { 19 | all_childs <- c() 20 | childs <- function(index){ 21 | kids <- pd$id[ pd$parent %in% index ] 22 | if( length(kids) ){ 23 | all_childs <<- c(all_childs, kids ) 24 | childs( kids ) 25 | } 26 | } 27 | sapply(ids, childs) 28 | return(all_childs) 29 | } 30 | 31 | get_sub_pd <- function(pd, ids) { 32 | children <- get_children(pd, ids) 33 | pd[pd$id %in% c(children, ids), ] 34 | } 35 | 36 | extract_assignments <- function(pd, name) { 37 | symbols <- pd[pd$token == "SYMBOL" & pd$text == name, ] 38 | assigns <- pd[pd$token %in% c("LEFT_ASSIGN", "RIGHT_ASSIGN", "EQ_ASSIGN"), ] 39 | 40 | if(nrow(assigns) == 0) return(NULL) 41 | 42 | sub_pds <- list() 43 | for(i in 1:nrow(assigns)) { 44 | assign <- assigns[i, ] 45 | valid_ids <- get_valid_ids(pd, assign, symbols) 46 | if(is.null(valid_ids)) next 47 | sub_pd <- get_sub_pd(pd, ids = valid_ids) 48 | sub_pds <- c(sub_pds, list(sub_pd)) 49 | } 50 | 51 | return(sub_pds) 52 | } 53 | 54 | get_valid_ids <- function(pd, assign, symbols) { 55 | if(assign$token == "EQ_ASSIGN") { 56 | siblings <- pd$id[pd$parent == assign$parent] 57 | close_siblings <- siblings[which(siblings == assign$id) + c(-1, 1)] 58 | symbol_children <- intersect(union(get_children(pd, close_siblings), close_siblings), symbols$id) 59 | } else { 60 | symbol_children <- intersect(get_children(pd, assign$parent), symbols$id) 61 | } 62 | assign_row <- which(pd$id == assign$id) 63 | children_rows <- which(pd$id %in% symbol_children) 64 | if(assign$token == "LEFT_ASSIGN") { 65 | if(any(children_rows < assign_row)) { 66 | return(assign$parent) 67 | } else { 68 | return(NULL) 69 | } 70 | } else if (assign$token == "RIGHT_ASSIGN") { 71 | if(any(children_rows > assign_row)) { 72 | return(assign$parent) 73 | } else { 74 | return(NULL) 75 | } 76 | } else if (assign$token == "EQ_ASSIGN") { 77 | if(any(children_rows < assign_row)) { 78 | return(c(close_siblings, assign$id)) 79 | } else { 80 | return(NULL) 81 | } 82 | } else { 83 | stop("token not supported") 84 | } 85 | } 86 | 87 | 88 | extract_object_assignment <- function(pd, name) { 89 | if (is.null(pd)) { 90 | return(NA) 91 | } 92 | sub_pds <- extract_assignments(pd, name) 93 | if(length(sub_pds) == 1) { 94 | return(sub_pds[[1]]) 95 | } else { 96 | return(NA) 97 | } 98 | } 99 | 100 | #' @importFrom utils tail getParseText 101 | extract_function_definition <- function(pd, name) { 102 | # body of the function is the last brother of the function keyword 103 | sub_pds <- extract_assignments(pd, name) 104 | if(length(sub_pds) == 1) { 105 | pd <- sub_pds[[1]] 106 | function_parents <- pd$parent[pd$token == "FUNCTION"] 107 | if (length(function_parents) == 0) { 108 | return(NULL) 109 | } 110 | fundefs <- lapply(function_parents, function(function_parent) { 111 | last_brother <- tail(pd$id[pd$parent == function_parent], 1) 112 | code <- getParseText(pd, last_brother) 113 | sub_pd <- get_sub_pd(pd, last_brother) 114 | return(list(code = code, pd = sub_pd)) 115 | }) 116 | # only the first parent (if there are embbedded definitions) 117 | return(fundefs[[1]]) 118 | } else { 119 | return(NULL) 120 | } 121 | 122 | } -------------------------------------------------------------------------------- /R/utils-rmd.R: -------------------------------------------------------------------------------- 1 | build_doc_structure <- function(text) { 2 | 3 | # Fix markdown format 4 | old.format <- knitr::opts_knit$get() 5 | knitr::opts_knit$set(out.format = "markdown") 6 | 7 | # Fix pattern business 8 | apat <- knitr::all_patterns 9 | opat <- knitr::knit_patterns$get() 10 | on.exit({ 11 | knitr::knit_patterns$restore(opat) 12 | knitr:::chunk_counter(reset = TRUE) 13 | knitr:::knit_code$restore(list()) 14 | knitr::opts_knit$set(old.format) 15 | }) 16 | knitr::pat_md() 17 | 18 | # split the file 19 | content = knitr:::split_file(lines = knitr:::split_lines(text)) 20 | code_chunks <- knitr:::knit_code$get() 21 | 22 | for(i in seq_along(content)) { 23 | if(class(content[[i]]) == "block") { 24 | label <- content[[i]]$params$label 25 | content[[i]]$input <- paste(code_chunks[[label]],collapse = "\n") 26 | } 27 | } 28 | 29 | # remove the inline blocks that contain nothing or only spaces: 30 | content[sapply(content, function(part) { 31 | all(grepl(pattern = "^\\s*$", x = part$input.src)) && class(part) == "inline" 32 | })] <- NULL 33 | 34 | return(content) 35 | } 36 | 37 | parse_docs <- function(state) { 38 | student_code <- state$get("student_code") 39 | solution_code <- state$get("solution_code") 40 | 41 | student_ds <- build_doc_structure(student_code) #list(list(input = "")) 42 | solution_ds <- build_doc_structure(solution_code) #list(list(input = "")) 43 | 44 | n_student <- length(student_ds) 45 | n_solution <- length(solution_ds) 46 | n_inline_student <- sum(sapply(student_ds, class) == "inline") 47 | n_inline_solution <- sum(sapply(solution_ds, class) == "inline") 48 | n_block_student <- sum(sapply(student_ds, class) == "block") 49 | n_block_solution <- sum(sapply(solution_ds, class) == "block") 50 | 51 | check_that(is_equal(n_student, n_solution), 52 | feedback = sprintf("Make sure the structure of your document is OK. The solution expects %i inline (text) blocks and %i code chunks.", n_inline_solution, n_block_solution)) 53 | 54 | check_that(is_equal(n_inline_student, n_inline_solution), 55 | feedback = sprintf("Make sure you have the correct amount of inline (text) blocks in your R markdown document. The solution expects %i.",n_inline_solution)) 56 | 57 | check_that(is_equal(n_block_student, n_block_solution), 58 | feedback = sprintf("Make sure you have the correct amount of code blocks in your R markdown document. The solution expects %i.", n_block_solution)) 59 | 60 | check_that(is_true(all.equal(sapply(student_ds, class), sapply(solution_ds, class))), 61 | feedback = sprintf("Make sure the overall code structure of your document is OK. The soltion expects the following setup: %s.", 62 | collapse_props(sapply(solution_ds, class), conn = ", "))) 63 | 64 | if(n_student != n_solution) return(FALSE) 65 | if(n_inline_student != n_inline_solution) return(FALSE) 66 | if(n_block_student != n_block_solution) return(FALSE) 67 | if(!isTRUE(all.equal(sapply(student_ds, class), sapply(solution_ds, class)))) return(FALSE) 68 | 69 | state$set(student_ds = student_ds) 70 | state$set(solution_ds = solution_ds) 71 | return(state) 72 | } -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | #' Get the number of hits for a series of regexes 2 | #' 3 | #' @param regex vector of regular expressions against which to match 4 | #' @param x character vector where matches are sought 5 | #' @param fixed logical. If \code{TRUE}, \code{regex} are strings to be matched as is. 6 | #' 7 | #' @export 8 | get_num_hits <- function(regex, x, fixed) { 9 | if (length(regex) == 0 || (length(regex) == 1 && nchar(regex) == 0)) { 10 | return(0) 11 | } else { 12 | counts <- sapply(regex, function(patt) { 13 | if (!fixed) { 14 | patt = paste0('(?s)', patt) 15 | } 16 | res <- gregexpr(patt, text = x, perl = !fixed, fixed = fixed)[[1]] 17 | if (any(res == -1)) { 18 | return(0L) 19 | } else { 20 | return(length(res)) 21 | } 22 | }, USE.NAMES = FALSE) 23 | if (length(counts) == 0) { 24 | return(0) 25 | } else { 26 | return(sum(counts)) 27 | } 28 | } 29 | } 30 | 31 | remove_comments <- function(code) { 32 | lines <- strsplit(code, "\\n")[[1]] 33 | return(paste0(lines[!grepl("^#", lines)], collapse = "\n")) 34 | } 35 | 36 | check_defined <- function(name, sol_env) { 37 | if (!exists(name, sol_env, inherits = FALSE)) { 38 | stop(paste(name, "is not defined in your solution environment.", 39 | "Specify the name of an object that is actually defined in the solution code")) 40 | } 41 | } 42 | 43 | check_sufficient <- function(calls, index, name) { 44 | if (index > length(calls)) { 45 | stop(sprintf("Fix either the index argument or the solution code; currently, there aren't %s calls of %s available in the solution.", index, name)) 46 | } 47 | } 48 | 49 | v2_only <- function() { 50 | env_var <- "TESTWHAT_V2_ONLY" 51 | Sys.getenv(env_var) != "" && Sys.getenv(env_var) == "1" 52 | } 53 | 54 | fail_if_v2_only <- function(errmsg = sprintf("%s() can no longer be used in SCTs. Use its check equivalent instead.", deparse(sys.call(-1)[[1]]))) { 55 | if (v2_only()) { 56 | stop(errmsg) 57 | } else { 58 | return(invisible(NULL)) 59 | } 60 | } 61 | 62 | failure <- function() { 63 | FALSE 64 | } 65 | 66 | get_solution_code <- function() { ex()$get("solution_code") } 67 | 68 | #' @importFrom magrittr %>% 69 | #' @export 70 | magrittr::`%>%` 71 | 72 | assert_is_string <- function(x, sct_name) { 73 | if (!is.character(x)) 74 | stop(x, paste0(sys.call(1)[1], " requires a string, but received the class", typeof(x), '.')) 75 | } 76 | 77 | tw_accessors <- function() { 78 | tw_data <- list() 79 | 80 | get = function(name) { 81 | if(missing(name)) { 82 | tw_data 83 | } else { 84 | tw_data[[name]] 85 | } 86 | } 87 | 88 | set = function(...) { 89 | tw_data <<- merge(list(...)) 90 | invisible(NULL) 91 | } 92 | 93 | clear = function() { 94 | tw_data <<- list() 95 | invisible(NULL) 96 | } 97 | 98 | initialize = function(data) { 99 | tw_data <<- data 100 | invisible(NULL) 101 | } 102 | 103 | merge = function(values) merge_list(tw_data, values) 104 | list(get = get, set = set, clear = clear, initialize = initialize) 105 | } 106 | 107 | merge_list <- function(x, y) { 108 | x[names(y)] = y 109 | x 110 | } 111 | 112 | #' tw singleton object to access data across SCT chains. 113 | #' 114 | #' @export 115 | tw <- tw_accessors() 116 | 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > :warning: **This repo had outdated tokens in its travisci config** 2 | > To make new releases for this project it needs to be moved to circleci 3 | 4 | # testwhat 5 | 6 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fdatacamp%2Ftestwhat.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fdatacamp%2Ftestwhat?ref=badge_shield) 7 | 8 | Verify R code submissions and auto-generate meaningful feedback messages. 9 | Originally developed for R exercises on DataCamp for so-called Submission Correctness Tests, but can also be used independently. 10 | 11 | - If you are new to teaching on DataCamp, check out https://instructor-support.datacamp.com. 12 | - If you want to learn what SCTs are and how they work, visit [this article](https://instructor-support.datacamp.com/courses/course-development/submission-correctness-tests) specifically. 13 | - For a complete overview of all functionality inside `testwhat` and articles about what to use when, consult https://datacamp.github.io/testwhat. 14 | 15 | For details, questions and suggestions, [contact us](mailto:content-engineering@datacamp.com). 16 | 17 | 18 | ## Installation 19 | 20 | ```R 21 | library("remotes") 22 | install_github("datacamp/testwhat") 23 | ``` 24 | 25 | ## Demo 26 | 27 | Experimenting locally: 28 | 29 | ```R 30 | library(testwhat) 31 | setup_state(sol_code = "x <- 5", 32 | stu_code = "x <- 4") 33 | 34 | ex() %>% check_object("x") 35 | # No error: x is defined in both student and solution code 36 | 37 | ex() %>% check_object("x") %>% check_equal() 38 | # Error: The contents of the variable `x` aren't correct. 39 | 40 | # Debugging state 41 | s <- ex() %>% check_object() 42 | s # only prints out state class 43 | str(s) # full overview of state 44 | s$get("student_code") # access student code in state 45 | ``` 46 | 47 | To include an SCT in a DataCamp course, visit https://instructor-support.datacamp.com. 48 | 49 | ## Tests 50 | 51 | `testwhat` currently depends on the proprietary `RBackend` and `RCompletion` packages to run tests. Tests run automatically on every branch that is updated through travis. 52 | 53 | ```R 54 | devtools::test() 55 | ``` 56 | 57 | ## Documentation 58 | 59 | Whenever a push is done to the `master` branch, this repo will automatically build a `pkgdown` website (containing reference documentation and vignettes), push it to the `gh-pages` branch, which in turn is served by GitHub at https://datacamp.github.io/testwhat. 60 | 61 | 62 | 63 | ## License 64 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fdatacamp%2Ftestwhat.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fdatacamp%2Ftestwhat?ref=badge_large) 65 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://datacamp.github.io/testwhat/ 2 | 3 | template: 4 | params: 5 | bootswatch: cosmo 6 | ganalytics: "UA-120925561-1" 7 | docsearch: 8 | api_key: cba84df952b7606852b92617801244e5 9 | index_name: datacamp_testwhat 10 | 11 | reference: 12 | - title: Commonly used functions 13 | contents: 14 | - starts_with("check_") 15 | - success_msg 16 | - override 17 | - disable_highlighting 18 | - title: Checking markdown 19 | contents: 20 | - check_rmd 21 | - check_header 22 | - check_chunk 23 | - check_option.MarkdownChunkState 24 | - check_equal.MarkdownChunkOptionState 25 | - check_yaml 26 | - check_option.MarkdownYamlState 27 | - check_equal.MarkdownYamlOptionState 28 | - title: Internals 29 | contents: 30 | - build_pd 31 | - compare 32 | - ex 33 | - get_num_hits 34 | - is_equal 35 | - run_until_fail 36 | - setup_state 37 | - state 38 | - test_exercise 39 | - tw 40 | - testwhat_learnr 41 | 42 | navbar: 43 | title: ~ 44 | type: default 45 | left: 46 | - text: Syntax 47 | href: articles/syntax.html 48 | - text: Glossary 49 | href: articles/glossary.html 50 | - text: Guides 51 | menu: 52 | - text: Checking objects 53 | href: articles/checking-objects.html 54 | - text: Checking function calls 55 | href: articles/checking-function-calls.html 56 | - text: Checking output 57 | href: articles/checking-output.html 58 | - text: Checking control flow 59 | href: articles/checking-control-flow.html 60 | - text: Checking function definitions 61 | href: articles/checking-function-definitions.html 62 | - text: Checking through string matching 63 | href: articles/checking-through-string-matching.html 64 | - text: Combining sets of SCTs 65 | href: articles/combining-sets-of-SCTs.html 66 | - text: Checking markdown documents 67 | href: articles/checking-markdown-documents.html 68 | - text: Electives 69 | href: articles/electives.html 70 | - text: Advanced 71 | menu: 72 | - text: Test to Check 73 | href: articles/test-to-check.html 74 | - text: Extensions 75 | href: articles/extensions.html 76 | 77 | - text: Reference 78 | href: reference/index.html 79 | -------------------------------------------------------------------------------- /inst/experiment.R: -------------------------------------------------------------------------------- 1 | pec <- ' 2 | # pec here 3 | ' 4 | 5 | sol_code <- ' 6 | x <- mean(1:3) 7 | ' 8 | 9 | stu_code <- ' 10 | x <- mean(1:3) 11 | ' 12 | 13 | library(testwhat) 14 | setup_state(pec = pec, sol_code = sol_code, stu_code = stu_code) 15 | 16 | ex() %>% check_function('mean') %>% disable_highlighting() %>% check_arg('x') %>% check_equal() 17 | -------------------------------------------------------------------------------- /man/build_pd.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils-pd.R 3 | \name{build_pd} 4 | \alias{build_pd} 5 | \title{Build ParseData from string representing code} 6 | \usage{ 7 | build_pd(code, silent = TRUE) 8 | } 9 | \arguments{ 10 | \item{code}{character string representing code} 11 | 12 | \item{silent}{if TRUE, parsing errors are caugt and NULL is returned.} 13 | } 14 | \description{ 15 | Build ParseData from string representing code 16 | } 17 | -------------------------------------------------------------------------------- /man/check_chunk.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-rmd.R 3 | \name{check_chunk} 4 | \alias{check_chunk} 5 | \title{Check markdown code chunk} 6 | \usage{ 7 | check_chunk(state, index = 1, not_found_msg = NULL, append = TRUE) 8 | } 9 | \arguments{ 10 | \item{state}{the state to start from. This state can be produced by 11 | \code{\link{check_rmd}}, but can also follow on \code{\link{check_header}} 12 | to look for a chunk in a specific header section.} 13 | 14 | \item{index}{number that specifies which code chunk to check in the student 15 | and solution code that is zoomed in on.} 16 | 17 | \item{not_found_msg}{If specified, this overrides the automatically generated 18 | message in case no index'th chunk was found.} 19 | 20 | \item{append}{Whether or not to append the feedback to feedback built in 21 | previous states.} 22 | } 23 | \value{ 24 | A state that zooms in on the code chunk. 25 | } 26 | \description{ 27 | Checks if a code chunk was specified. If not, generates a feedback message. 28 | If yes, zooms in on the code chunk so you can use functions like 29 | \code{\link{check_function}} as for any regular R exercise. 30 | } 31 | -------------------------------------------------------------------------------- /man/check_code.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-code.R 3 | \name{check_code} 4 | \alias{check_code} 5 | \title{Test the student's code as text} 6 | \usage{ 7 | check_code(state, regex, fixed = FALSE, times = 1, 8 | missing_msg = NULL, append = TRUE, drop_comments = FALSE) 9 | } 10 | \arguments{ 11 | \item{state}{the state to start from} 12 | 13 | \item{regex}{A set of strings/regexes that should be in the student code.} 14 | 15 | \item{fixed}{if TRUE, strings are treated literally. If FALSE, strings are 16 | treated as regex patterns.} 17 | 18 | \item{times}{how often should any of the strings be matched?} 19 | 20 | \item{missing_msg}{Custom feedback in case the pattern is not contained often 21 | enough in the student's submission.} 22 | 23 | \item{append}{Whether or not to append the feedback to feedback built in 24 | previous states} 25 | 26 | \item{drop_comments}{Logical value indicating whether or not to remove 27 | comments from these student code before looking for the pattern. Defaults 28 | to FALSE for backwards compatibility reasons.} 29 | } 30 | \description{ 31 | Some rudimentary string cleaning is performed to allow for different ways of 32 | saying the same things (removing spaces, changing single quotes to double 33 | quotes, changing TRUE to T ...). 34 | } 35 | \details{ 36 | Using these function should be a last resort, as there are myriad ways of 37 | solving the same problem with R! 38 | } 39 | \examples{ 40 | \dontrun{ 41 | # Example 1 42 | TRUE & FALSE 43 | 44 | # SCT 45 | ex() \%>\% check_code(c("TRUE & FALSE", "FALSE & TRUE"), fixed = TRUE) 46 | 47 | # Example 2: 48 | "Hello, world!" 49 | 50 | # SCT, robust to small typos 51 | ex() \%>\% check_code("[H|h]ello,*\\\\s*[W|w]orld\\\\!*") 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /man/check_control.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-control.R 3 | \name{check_control} 4 | \alias{check_control} 5 | \alias{check_if_else} 6 | \alias{check_while} 7 | \alias{check_for} 8 | \alias{check_cond} 9 | \alias{check_body.ControlState} 10 | \alias{check_if} 11 | \alias{check_else} 12 | \title{Check whether student coded a control statement correctly} 13 | \usage{ 14 | check_if_else(state, index = 1, not_found_msg = NULL, append = TRUE) 15 | 16 | check_while(state, index = 1, not_found_msg = NULL, append = TRUE) 17 | 18 | check_for(state, index = 1, not_found_msg = NULL, append = TRUE) 19 | 20 | check_cond(state) 21 | 22 | \method{check_body}{ControlState}(state, ...) 23 | 24 | check_if(state) 25 | 26 | check_else(state, not_found_msg = NULL, append = TRUE) 27 | } 28 | \arguments{ 29 | \item{state}{state to start from (for \code{check_} functions)} 30 | 31 | \item{index}{Number of that particular control statement to check} 32 | 33 | \item{not_found_msg}{Custom message in case the control statement was not found} 34 | 35 | \item{append}{Whether or not to append the feedback to feedback built in previous states} 36 | 37 | \item{...}{S3 stuff} 38 | } 39 | \description{ 40 | Check whether student coded a control statement correctly 41 | } 42 | \examples{ 43 | \dontrun{ 44 | # Example 1: if else 45 | vec <- c("a", "b", "c") 46 | if("a" \%in\% vec) { 47 | print("a in here") 48 | } else if(any("b" > vec)) { 49 | cat("b not smallest") 50 | } else { 51 | str(vec) 52 | } 53 | 54 | # SCT 55 | check_if_else(1) \%>\% { 56 | check_cond(.) \%>\% { 57 | check_code(., "\%in\%") 58 | check_code(., "vec") 59 | } 60 | check_if(.) \%>\% check_function(., "print") 61 | check_else(.) \%>\% check_if_else() \%>\% { 62 | check_cond(.) \%>\% check_code(">") 63 | check_if(.) \%>\% check_function("cat") 64 | check_else(.) \%>\% check_function("str") 65 | } 66 | } 67 | 68 | # Example 2: while loop 69 | while(x < 18) { 70 | x <- x + 5 71 | print(x) 72 | } 73 | 74 | # SCT 75 | check_while(1) \%>\% { 76 | check_cond(.) \%>\% check_code(c("< 18", "18 >")) 77 | check_body(.) \%>\% { 78 | check_code(., c("x + 5", "5 + x")) 79 | check_function(., "print") \%>\% test_arg("x") 80 | } 81 | } 82 | 83 | # Example 3: for loop 84 | for(i in 1:5) { 85 | print("hurray!") 86 | } 87 | 88 | # SCT 89 | ex() \%>\% check_for() \%>\% { 90 | check_cond(.) \%>\% { 91 | check_code(., "in") 92 | check_code(., "1") 93 | check_code(., "5") 94 | } 95 | check_body(.) \%>\% check_function("print") \%>\% check_arg("x") \%>\% check_equal() 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /man/check_equal.MarkdownChunkOptionState.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-rmd.R 3 | \name{check_equal.MarkdownChunkOptionState} 4 | \alias{check_equal.MarkdownChunkOptionState} 5 | \title{Check equality of markdown code chunk option} 6 | \usage{ 7 | \method{check_equal}{MarkdownChunkOptionState}(state, 8 | incorrect_msg = NULL, append = FALSE, ...) 9 | } 10 | \arguments{ 11 | \item{state}{the state to start from. Should be a state produced by \code{\link{check_option}}.} 12 | 13 | \item{incorrect_msg}{If specified, this overrides the automatically generated message in case the options don't match between student and solution.} 14 | 15 | \item{append}{Whether or not to append the feedback to feedback built in previous states.} 16 | 17 | \item{...}{S3 stuff} 18 | } 19 | \description{ 20 | Check equality of markdown code chunk option 21 | } 22 | -------------------------------------------------------------------------------- /man/check_equal.MarkdownYamlOptionState.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-rmd.R 3 | \name{check_equal.MarkdownYamlOptionState} 4 | \alias{check_equal.MarkdownYamlOptionState} 5 | \title{Check equality of markdown YAML header option} 6 | \usage{ 7 | \method{check_equal}{MarkdownYamlOptionState}(state, 8 | incorrect_msg = NULL, append = TRUE, ...) 9 | } 10 | \arguments{ 11 | \item{state}{the state to start from. Should be a state produced by 12 | \code{\link{check_option}}.} 13 | 14 | \item{incorrect_msg}{If specified, this overrides the automatically generated 15 | message in case the options don't match between student and solution.} 16 | 17 | \item{append}{Whether or not to append the feedback to feedback built in 18 | previous states.} 19 | 20 | \item{...}{S3 stuff} 21 | } 22 | \description{ 23 | Check equality of markdown YAML header option 24 | } 25 | -------------------------------------------------------------------------------- /man/check_error.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-error.R 3 | \name{check_error} 4 | \alias{check_error} 5 | \alias{check_error.default} 6 | \title{Explicitly check whether the student's submission threw an error.} 7 | \usage{ 8 | check_error(state, ...) 9 | 10 | \method{check_error}{default}(state, incorrect_msg = NULL, ...) 11 | } 12 | \arguments{ 13 | \item{state}{State to start from (for \code{check_error})} 14 | 15 | \item{...}{S3 stuff} 16 | 17 | \item{incorrect_msg}{additional message that is appended to the automatically 18 | generated feedback message.} 19 | } 20 | \description{ 21 | With information gathered from the R Backend, testwhat can detect whether the 22 | student's submission generated an error. 23 | } 24 | \details{ 25 | If all SCTs for an exercise pass, before marking the submission as correct 26 | testwhat will automatically check whether the student submission generated an 27 | error, unless the exercise explicitly allows for errors. This means it is not 28 | needed to use \code{check_error} explicitly. However, in some cases, 29 | using \code{check_error} explicitly somewhere 30 | throughout your SCT execution can be helpful: 31 | 32 | \itemize{ 33 | \item{If you want to make sure people didn't write typos when 34 | writing a long function name.} 35 | \item{If you want to first verify whether a 36 | function call actually runs,before checking whether the arguments were 37 | specified correctly.} 38 | \item{More generally, if, because of the content, it's 39 | instrumental that the script runs without errors before doing any other 40 | verifications.} 41 | } 42 | } 43 | \examples{ 44 | \dontrun{ 45 | # Example student code: x <- 4 + "a" 46 | 47 | # SCT that explicitly checks for an error first 48 | ex() \%>\% check_error() 49 | ex() \%>\% check_object('x') \%>\% check_equal() 50 | 51 | # SCT that does not have to check for an error 52 | # testwhat will verify for an error implicitly 53 | ex() \%>\% check_object('x') \%>\% check_equal() 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /man/check_fun_def.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-fun-def.R 3 | \name{check_fun_def} 4 | \alias{check_fun_def} 5 | \alias{check_arguments} 6 | \alias{check_body.FunDefState} 7 | \alias{check_call} 8 | \title{Check whether the student defined a function correctly} 9 | \usage{ 10 | check_fun_def(state, name, undefined_msg = NULL, no_fundef_msg = NULL, 11 | append = TRUE) 12 | 13 | check_arguments(state, incorrect_number_arguments_msg = NULL, 14 | append = TRUE) 15 | 16 | \method{check_body}{FunDefState}(state, not_found_msg = NULL, 17 | append = TRUE, ...) 18 | 19 | check_call(state, ...) 20 | } 21 | \arguments{ 22 | \item{state}{the state to start from} 23 | 24 | \item{name}{The name of the function to test} 25 | 26 | \item{undefined_msg}{Custom message in case the specified function 27 | was not defined} 28 | 29 | \item{no_fundef_msg}{Custom message in case the function specified in \code{name} is not a function.} 30 | 31 | \item{append}{Whether or not to append the feedback to feedback built in previous states} 32 | 33 | \item{incorrect_number_arguments_msg}{Optional feedback message in case the 34 | function does not have the correct number of arguments.} 35 | 36 | \item{not_found_msg}{Custom feedback message if function definition was not 37 | found.} 38 | 39 | \item{...}{arguments to pass to the user-defined function to test result, output or error in a later stage} 40 | } 41 | \description{ 42 | \code{check_fun_def} checks whether an object is defined in the student enviornment, and returns a state that can be piped to: 43 | \itemize{ 44 | \item{\code{check_arguments}, to check whether the correct arguments where specified.} 45 | \item{\code{check_call}, to call the function with the provided arguments, and produces a state that can be piped to \code{check_output}, \code{check_result} and \code{check_error} to compare the output, result or error from calling the function between student and solution.} 46 | \item{\code{check_body}, that returns a state that focuses on the body that defines the function. Note that you cannot use \code{\link{check_object}} to compare variables that are limited to the function scope.} 47 | } 48 | } 49 | \examples{ 50 | \dontrun{ 51 | # Example: 52 | my_op <- function(a, b) { 53 | stopifnot(length(a) == length(b)) 54 | return(abs(a) + abs(b)) 55 | } 56 | 57 | # Robust SCT 58 | ex() \%>\% check_fun_def('my_op') \%>\% check_correct( 59 | { 60 | check_call(., c(1, 2), c(3, 4)) \%>\% check_result() \%>\% check_equal() 61 | check_call(., c(1, -2), c(3, -4)) \%>\% check_result() \%>\% check_equal() 62 | check_call(., c(-1, 2), c(-3, 4)) \%>\% check_result() \%>\% check_equal() 63 | check_call(., 1, c(3, 4)) \%>\% check_error() 64 | check_call(., c(1, -2), 3) \%>\% check_error() 65 | }, 66 | { 67 | check_arguments(.) 68 | check_body(.) \%>\% { 69 | check_function(., 'stopifnot') \%>\% check_arg('...') \%>\% { 70 | check_function(., 'length', index = 1) \%>\% check_arg('x') \%>\% check_equal(eval = FALSE) 71 | check_function(., 'length', index = 2) \%>\% check_arg('x') \%>\% check_equal(eval = FALSE) 72 | check_code(., '==') 73 | } 74 | check_function(., 'abs', index = 1) \%>\% check_arg('x') \%>\% check_equal(eval = FALSE) 75 | check_function(., 'abs', index = 2) \%>\% check_arg('x') \%>\% check_equal(eval = FALSE) 76 | } 77 | } 78 | ) 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /man/check_function.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-function.R 3 | \name{check_function} 4 | \alias{check_function} 5 | \alias{check_operator} 6 | \alias{check_arg} 7 | \alias{check_equal.ArgumentState} 8 | \title{Check whether a student correctly called a function/operator} 9 | \usage{ 10 | check_function(state, name, index = 1, not_called_msg = NULL, 11 | append = TRUE) 12 | 13 | check_operator(state, name, index = 1, append = TRUE, 14 | not_called_msg = NULL) 15 | 16 | check_arg(state, arg, arg_not_specified_msg = NULL, append = TRUE) 17 | 18 | \method{check_equal}{ArgumentState}(state, incorrect_msg = NULL, 19 | eval = TRUE, eq_condition = "equivalent", eq_fun = NULL, 20 | append = TRUE, ...) 21 | } 22 | \arguments{ 23 | \item{state}{state to start from} 24 | 25 | \item{name}{name of the function/operator as a string, e.g. \code{"mean"} or 26 | \code{"+"}} 27 | 28 | \item{index}{integer that specifies which call of \code{name} in the 29 | solution code will be checked.} 30 | 31 | \item{not_called_msg}{custom feedback message in case the student did not 32 | call the function often enough.} 33 | 34 | \item{append}{Whether or not to append the feedback to feedback built in 35 | previous states} 36 | 37 | \item{arg}{name or position of argument to specify 38 | ... Arguments can be accessed using '..' (see example 5) (for \code{check_arg})} 39 | 40 | \item{arg_not_specified_msg}{custom message in case argument was not 41 | specified (for \code{check_arg})} 42 | 43 | \item{incorrect_msg}{custom feedback message in case the student did not call 44 | the function with the same argument values as in the sample solution. You 45 | can specify a vector of arguments with the same length as \code{args}, to 46 | have argument-specific custom feedback.} 47 | 48 | \item{eval}{logical vector indicating whether and how to compare arguments. 49 | If \code{eval} is \code{NA}, student and solution argument are not 50 | compared. If \code{eval} is \code{FALSE}, the string versions of the 51 | arguments are compared. If \code{eval} is \code{TRUE}, the argument in the 52 | student code is evaluated in the student environment and the argument in 53 | the solution code is evaluated in the solution environment, and their 54 | results are compared. Setting this to \code{FALSE} can be useful, e.g., to 55 | check whether the student supplied a large predefined object, or when 56 | you're in a sub-SCT where the environments are not unambiguously available.} 57 | 58 | \item{eq_condition}{character vector indicating how to perform the 59 | comparison for each argument. See \code{\link{is_equal}}.} 60 | 61 | \item{eq_fun}{optional argument to specify a custom equality function. The 62 | function should take two arguments and always return a single boolean 63 | value: \code{TRUE} or \code{FALSE}.} 64 | 65 | \item{...}{S3 stuff} 66 | } 67 | \description{ 68 | Check whether a student called a function correctly. Note: 69 | \code{test_function} and \code{test_function_v2} are now identical and either 70 | can be used. 71 | } 72 | \examples{ 73 | \dontrun{ 74 | # Example 1 75 | mean(1:3) 76 | 77 | # SCT 78 | ex() \%>\% check_function("mean") \%>\% check_arg("x") \%>\% check_equal() 79 | 80 | # Example 2 81 | mean(c(NA, 1, 2), na.rm = TRUE) 82 | 83 | # SCT 84 | ex() \%>\% check_function("mean") \%>\% { 85 | check_arg(., "x") \%>\% check_equal() 86 | check_arg(., "na.rm") \%>\% check_equal() 87 | } 88 | 89 | # Example 3 90 | 5 + 4 91 | 92 | # SCT 93 | ex() \%>\% check_operator("+") \%>\% check_result() \%>\% check_equal() 94 | 95 | # Example 4: Positional argument check 96 | cor(rnorm(10), rnorm(10)) 97 | 98 | # SCT 99 | ex() \%>\% check_function("cor") \%>\% { 100 | check_arg(., 1) \%>\% check_equal() 101 | check_arg(., 2) \%>\% check_equal() 102 | } 103 | 104 | # Example 5: ... in check_args 105 | 106 | soln <- "std_dev <- purrr::compose(sqrt, var, .dir='forward')" 107 | state <- setup_state(soln, soln) 108 | state \%>\% check_function(., "compose") \%>\% { 109 | check_arg(., '..1') \%>\% check_equal() # sqrt 110 | check_arg(., '..2') \%>\% check_equal() # var 111 | check_arg(., '.dir') \%>\% check_equal() 112 | } 113 | 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /man/check_function_result.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-function-result.R 3 | \name{check_function_result} 4 | \alias{check_function_result} 5 | \alias{check_result.OperationState} 6 | \alias{check_result.FunctionState} 7 | \alias{check_equal.FunctionResultState} 8 | \alias{check_equal.OperationResultState} 9 | \title{Check the result of a function call/operation} 10 | \usage{ 11 | \method{check_result}{OperationState}(state, error_msg = NULL, 12 | append = TRUE, ...) 13 | 14 | \method{check_result}{FunctionState}(state, error_msg = NULL, 15 | append = TRUE, ...) 16 | 17 | \method{check_equal}{FunctionResultState}(state, 18 | eq_condition = "equivalent", eq_fun = NULL, incorrect_msg = NULL, 19 | append = TRUE, ...) 20 | 21 | \method{check_equal}{OperationResultState}(state, 22 | eq_condition = "equivalent", eq_fun = NULL, incorrect_msg = NULL, 23 | append = TRUE, ...) 24 | } 25 | \arguments{ 26 | \item{state}{the state to start from (for \code{check_} functions)} 27 | 28 | \item{error_msg}{feedback message in case the student function call at the 29 | mentioned index generated an error.} 30 | 31 | \item{append}{Whether or not to append the feedback to feedback built in 32 | previous states} 33 | 34 | \item{...}{S3 stuff} 35 | 36 | \item{eq_condition}{character string indicating how to compare. See 37 | \code{\link{is_equal}}.} 38 | 39 | \item{eq_fun}{optional argument to specify a custom equality function. The 40 | function should take two arguments and always return a single boolean 41 | value: \code{TRUE} or \code{FALSE}.} 42 | 43 | \item{incorrect_msg}{feedback message in case the evaluation was not the 44 | same as in the solution.} 45 | } 46 | \description{ 47 | Check the result of a function call/operation 48 | } 49 | \examples{ 50 | \dontrun{ 51 | # Example 1 52 | mean(1:3) 53 | 54 | # SCT 55 | ex() \%>\% check_function("mean") \%>\% check_result() \%>\% check_equal() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /man/check_ggplot.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-ggplot.R 3 | \name{check_ggplot} 4 | \alias{check_ggplot} 5 | \title{Test ggplot call} 6 | \usage{ 7 | check_ggplot(state, index = 1, all_fail_msg = NULL, 8 | check_data = TRUE, data_fail_msg = NULL, check_aes = TRUE, 9 | aes_fail_msg = NULL, exact_aes = FALSE, check_geom = TRUE, 10 | geom_fail_msg = NULL, exact_geom = FALSE, check_geom_params = NULL, 11 | check_facet = TRUE, facet_fail_msg = NULL, check_scale = TRUE, 12 | scale_fail_msg = NULL, exact_scale = FALSE, check_coord = TRUE, 13 | coord_fail_msg = NULL, exact_coord = FALSE, check_stat = TRUE, 14 | stat_fail_msg = NULL, exact_stat = FALSE, check_extra = NULL, 15 | extra_fail_msg = NULL, exact_extra = NULL, check = NULL) 16 | } 17 | \arguments{ 18 | \item{state}{the state to start from} 19 | 20 | \item{index}{which call to check} 21 | 22 | \item{all_fail_msg}{Message if all fails} 23 | 24 | \item{check_data}{Whether or not to check data layer} 25 | 26 | \item{data_fail_msg}{Message in case data layer fails} 27 | 28 | \item{check_aes}{Whether or not to check aes layer} 29 | 30 | \item{aes_fail_msg}{Message in case aes layer fails} 31 | 32 | \item{exact_aes}{Should the aesthetics be exact?} 33 | 34 | \item{check_geom}{Whether or not to check geom layer} 35 | 36 | \item{geom_fail_msg}{Message in case geom layer fails} 37 | 38 | \item{exact_geom}{Should the geoms be exact?} 39 | 40 | \item{check_geom_params}{Should the geom parameters be checked?} 41 | 42 | \item{check_facet}{Whether or not to check facet layer} 43 | 44 | \item{facet_fail_msg}{Message in case facet layer fails} 45 | 46 | \item{check_scale}{Whether or not to check scale layer} 47 | 48 | \item{scale_fail_msg}{Message in case scale layer fails} 49 | 50 | \item{exact_scale}{Whether or not scales should be defined exactly} 51 | 52 | \item{check_coord}{Whether or not to check coord layer} 53 | 54 | \item{coord_fail_msg}{Message in case coord layer fails} 55 | 56 | \item{exact_coord}{Whether or not coords should be defined exactly} 57 | 58 | \item{check_stat}{Whether or not to check stat layer} 59 | 60 | \item{stat_fail_msg}{Message in case stat layer fails} 61 | 62 | \item{exact_stat}{Whether or not stats should be defined exactly} 63 | 64 | \item{check_extra}{Whether to check extra stuff} 65 | 66 | \item{extra_fail_msg}{Message in case extra stuff fails} 67 | 68 | \item{exact_extra}{Whether or not extra info should be exactly specified.} 69 | 70 | \item{check}{Which layers to check} 71 | } 72 | \description{ 73 | Test ggplot call 74 | } 75 | \keyword{internal} 76 | -------------------------------------------------------------------------------- /man/check_header.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-rmd.R 3 | \name{check_header} 4 | \alias{check_header} 5 | \title{Check markdown header} 6 | \usage{ 7 | check_header(state, level, index = 1, not_found_msg = NULL, 8 | append = TRUE) 9 | } 10 | \arguments{ 11 | \item{state}{the state to start from.} 12 | 13 | \item{level}{the level of the header to check} 14 | 15 | \item{index}{which h header to check} 16 | 17 | \item{not_found_msg}{If specified, this overrides the automatically generated 18 | message in case not enough headers of the specified level were found.} 19 | 20 | \item{append}{Whether or not to append the feedback to feedback built in 21 | previous states} 22 | } 23 | \value{ 24 | A state that zooms in on the section under the header until the next 25 | same-level header. 26 | } 27 | \description{ 28 | Checks if a markdown header with a certain level exists. If not, generates a 29 | feedback message. If yes, zooms in on the entire header section. 30 | } 31 | -------------------------------------------------------------------------------- /man/check_library.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-library-function.R 3 | \name{check_library} 4 | \alias{check_library} 5 | \title{Check whether the library function was called correctly} 6 | \usage{ 7 | check_library(state, package, not_called_msg = NULL, 8 | incorrect_msg = NULL) 9 | } 10 | \arguments{ 11 | \item{state}{state to start from} 12 | 13 | \item{package}{package name for which the library() 14 | function should've been called} 15 | 16 | \item{not_called_msg}{optional feedback message in case the library 17 | function wasn't called a single time} 18 | 19 | \item{incorrect_msg}{optional feedback message in case the library 20 | function wasn't called for the specified package.} 21 | } 22 | \description{ 23 | Convenience function to test in a very hacky way whether 24 | the library function was called correctly in its most simple form. 25 | There is support for the different ways to call the library function 26 | } 27 | \examples{ 28 | \dontrun{ 29 | # example solution 30 | library(ggvis) 31 | 32 | # sct to test whether ggvis was loaded 33 | ex() \%>\% check_library("ggvis") 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /man/check_logic.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-logic.R 3 | \name{check_logic} 4 | \alias{check_logic} 5 | \alias{check_correct} 6 | \alias{check_or} 7 | \title{Combine tests} 8 | \usage{ 9 | check_correct(state, ...) 10 | 11 | check_or(state, ...) 12 | } 13 | \arguments{ 14 | \item{state}{The state. Typically \code{\link{ex}} but can also be a 15 | lower-level state if you're using nested \code{check_or}s or 16 | \code{check_correct}s} 17 | 18 | \item{...}{sets of tests. In the case of \code{check_correct}, the first set 19 | is the \code{check_code}, the second set is the \code{diagnose_code}. For 20 | \code{check_or}, an unrestricted number of sets of tests: only one of these 21 | tests has to pass for the \code{check_or} to pass.} 22 | } 23 | \description{ 24 | \code{check_correct} checks whether a set of tests passes, and does 25 | additional, more precise tests if these tests fail. In addition to the state, 26 | it takes two code chunks; \itemize{\item{\code{check_code}: specifies the 27 | code that checks on the (typically, final results of the) student's code. 28 | These tests are executed silently, without the reporter generating 29 | information for these.} \item{\code{diagnose_code}: Set of tests that gets 30 | executed if the tests in \code{check_code} fail. These tests contain more 31 | detailed tests, to pinpoint the problem.} } 32 | } 33 | \details{ 34 | \code{check_correct} increases the flexibility for the student: if the tests 35 | in \code{check_code} pass, the results of the tests in \code{diagnose_code} 36 | are not considered. If you test for the end result in \code{check_code}, and 37 | only do more rigorous testing in \code{diagnose_code}, you can allow for 38 | multiple solutions to a challenge. 39 | 40 | Similarly, \code{check_or} checks whether one of many test sets pass. That 41 | way, you can allow for multiple solutions. 42 | 43 | Both \code{check_or} and \code{check_correct} makes the state you feed it to 44 | its subtests available as \code{.} (the dot), similar to how magrittr does 45 | it. 46 | } 47 | \examples{ 48 | \dontrun{ 49 | # Example 1 solution code 50 | x <- mean(1:3) 51 | 52 | # Example SCT 53 | ex() \%>\% check_correct( 54 | check_object(., "x") \%>\% check_equal(), 55 | check_fun(., "mean") \%>\% check_arg("x") \%>\% check_equal() 56 | ) 57 | 58 | # Following submissions will all be accepted: 59 | x <- mean(1:3) 60 | x <- 2 61 | x <- mean(-1:5) 62 | 63 | # Example 2 solution code 64 | # a <- 3; b <- 4 65 | 66 | # Example SCT 67 | ex() \%>\% check_or( 68 | check_object(., 'a') \%>\% check_equal(), 69 | check_object(., 'b') \%>\% check-equal() 70 | ) 71 | 72 | # Following submissions will all be accepted: 73 | a <- 3; b <- 4 74 | a <- 3 75 | b <- 4 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /man/check_mc.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-mc.R 3 | \name{check_mc} 4 | \alias{check_mc} 5 | \title{Test a multiple choice exercise} 6 | \usage{ 7 | check_mc(state, correct, no_selection_msg = NULL, feedback_msgs = NULL) 8 | } 9 | \arguments{ 10 | \item{state}{the state passed to it. Use \code{ex()} at all times.} 11 | 12 | \item{correct}{number of the correct answer (or vector of numbers, if several options are fine)} 13 | 14 | \item{no_selection_msg}{feedback message in case the student did not select an answer.} 15 | 16 | \item{feedback_msgs}{vector of feedback messages for both the incorrect exercises as the correct exercise. 17 | Order the messages according to how they are listed in the instructions. For example, if there are four options, 18 | the second of which is correct, a vector of four feedback messages should be provided. The first message corresponds 19 | to feedback on the incorrect selection of the first option, the second message corresponds to the feedback message for 20 | the correct collection. The third and fourth messages correspond to feedback on the incorrect selection of the third and 21 | fourth option.} 22 | } 23 | \description{ 24 | This code expects the DM.result variable to be defined by the DataCamp frontend. 25 | There is need to define the success_msg seperately, since it is defined inside the function. 26 | } 27 | \examples{ 28 | \dontrun{ 29 | # Example solution: second instruction correct out of three options. 30 | 31 | # Corresponding SCT: 32 | msg1 <- "Not good, try again!" 33 | msg2 <- "Nice one!" 34 | msg3 <- "Not quite, give it another shot." 35 | ex() \%>\% check_mc(2, feedback_msgs = c(msg1, msg2, msg3)) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /man/check_option.MarkdownChunkState.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-rmd.R 3 | \name{check_option.MarkdownChunkState} 4 | \alias{check_option.MarkdownChunkState} 5 | \title{Check markdown code chunk option} 6 | \usage{ 7 | \method{check_option}{MarkdownChunkState}(state, name, 8 | not_found_msg = NULL, append = TRUE, ...) 9 | } 10 | \arguments{ 11 | \item{state}{the state to start from. Should be a state produced by 12 | \code{\link{check_chunk}}.} 13 | 14 | \item{name}{name of the chunk option to zoom in on.} 15 | 16 | \item{not_found_msg}{If specified, this overrides the automatically generated 17 | message in case the option wasn't specified.} 18 | 19 | \item{append}{Whether or not to append the feedback to feedback built in 20 | previous states.} 21 | 22 | \item{...}{S3 stuff} 23 | } 24 | \value{ 25 | A state that zooms in on the code chunk option. 26 | } 27 | \description{ 28 | Checks if a code chunk option was specified. If not, generates a feedback 29 | message. If yes, zooms in on the code chunk option so you can use 30 | \code{\link{check_equal}} to verify equality. 31 | } 32 | -------------------------------------------------------------------------------- /man/check_option.MarkdownYamlState.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-rmd.R 3 | \name{check_option.MarkdownYamlState} 4 | \alias{check_option.MarkdownYamlState} 5 | \title{Check markdown YAML header option} 6 | \usage{ 7 | \method{check_option}{MarkdownYamlState}(state, name, 8 | not_found_msg = NULL, append = TRUE, ...) 9 | } 10 | \arguments{ 11 | \item{state}{the state to start from. Should be a state produced by 12 | \code{\link{check_yaml}}.} 13 | 14 | \item{name}{name of the YAML header option to zoom in on. If you want to 15 | check a nested option, use \code{c()} to chain together the different 16 | names.} 17 | 18 | \item{not_found_msg}{If specified, this overrides the automatically generated 19 | message in case the option wasn't specified.} 20 | 21 | \item{append}{Whether or not to append the feedback to feedback built in 22 | previous states.} 23 | 24 | \item{...}{S3 stuff} 25 | } 26 | \value{ 27 | A state that zooms in on the YAML header option. 28 | } 29 | \description{ 30 | Checks if a yaml header option was specified. If not, generates a feedback 31 | message. If yes, zooms in on the YAML header option so you can use 32 | \code{\link{check_equal}} to verify equality. 33 | } 34 | -------------------------------------------------------------------------------- /man/check_output.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-output.R 3 | \name{check_output} 4 | \alias{check_output} 5 | \alias{check_output.default} 6 | \alias{check_output_expr} 7 | \title{Check whether the student printed something to the console} 8 | \usage{ 9 | check_output(state, ...) 10 | 11 | \method{check_output}{default}(state, regex, fixed = FALSE, 12 | trim = FALSE, times = 1, output_only = FALSE, missing_msg = NULL, 13 | append = TRUE, ...) 14 | 15 | check_output_expr(state, expr, times = 1, missing_msg = NULL, 16 | append = TRUE) 17 | } 18 | \arguments{ 19 | \item{state}{the state to start from} 20 | 21 | \item{...}{S3 stuff} 22 | 23 | \item{regex}{the regular expression or pattern to look for} 24 | 25 | \item{fixed}{if fixed is TRUE, \code{regex} will be sought for 'as is' in the 26 | output, if fixed = FALSE (the default), \code{regex} will be treated as 27 | actual regular expression.} 28 | 29 | \item{trim}{should the student output be trimmed, so that all newlines and 30 | spaces are removed, before checking?} 31 | 32 | \item{times}{how often should the pattern/expression output be found?} 33 | 34 | \item{output_only}{Consider only regular output, or also messages, warnings 35 | and error? \code{FALSE} by default for \code{check_output} (so it considers 36 | all kinds of output). You cannot specify this argument for 37 | \code{check_output_expr} and \code{test_output_contains}.} 38 | 39 | \item{missing_msg}{Custom message in case the pattern or output wasn't found 40 | often enough.} 41 | 42 | \item{append}{Whether or not to append the feedback to feedback built in 43 | previous states} 44 | 45 | \item{expr}{The expression (as string) for which the output should be in the 46 | student's console output.} 47 | } 48 | \description{ 49 | Check the output of the submission to see if it contains certain elements. 50 | } 51 | \details{ 52 | With \code{check_output}, you can simply specify a regular expression or 53 | pattern (depending on the value of \code{fixed}) that is looked for in the 54 | student's output. By default, regular output, messages, warnings and errors 55 | are considered. 56 | 57 | With \code{test_output_contains} and \code{check_output_expr} you can pass an 58 | expression, that is executed in the student environment, and whose output is 59 | compared to the output the student generated. If the generated output is found 60 | in the student's output, the check passes. By default, only regular output is 61 | considered. 62 | } 63 | \examples{ 64 | \dontrun{ 65 | # Example 1 66 | mtcars 67 | 68 | # SCT 69 | ex() \%>\% check_output_expr("mtcars") 70 | 71 | # Example 2 72 | print("hello!") 73 | 74 | # SCT (robust) 75 | ex() \%>\% check_output("[H|h]ello\\\\!*") 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /man/check_predefined_objects.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-predefined-objects.R 3 | \name{check_predefined_objects} 4 | \alias{check_predefined_objects} 5 | \title{Test predefined R objects} 6 | \usage{ 7 | check_predefined_objects(state, name, eq_condition = "equivalent", 8 | eval = TRUE, undefined_msg = NULL, incorrect_msg = NULL) 9 | } 10 | \arguments{ 11 | \item{state}{the state to start from} 12 | 13 | \item{name}{vector of names of the objects to check} 14 | 15 | \item{eq_condition}{character vector indicating how to compare. See 16 | \code{\link{is_equal}}.} 17 | 18 | \item{eval}{logical vector indicating whether or not you want to check only 19 | the objects' existence or also whether their values match the solution.} 20 | 21 | \item{undefined_msg}{vector version of \code{undefined_msg} of 22 | \code{\link{check_object}}} 23 | 24 | \item{incorrect_msg}{vector version of \code{incorrect_msg} of 25 | \code{\link{check_object}}} 26 | } 27 | \description{ 28 | At the start of your SCT, you typically want to check whether some predefined 29 | variables are still correct. \code{test_predefined_object} allows you to 30 | specify a vector of object names, together with a vector of equivalence 31 | conditions, evaluation specifications, undefined an incorrect messages. 32 | } 33 | \examples{ 34 | \dontrun{ 35 | # Suppose the sample code specifies the variables a, b and c, 36 | # and you want to check that a, b and c haven't changed. 37 | ex() \%>\% check_predefined_objects(c("a", "b", "c")) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /man/check_rmd.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-rmd.R 3 | \name{check_rmd} 4 | \alias{check_rmd} 5 | \title{Check markdown file} 6 | \usage{ 7 | check_rmd(state) 8 | } 9 | \arguments{ 10 | \item{state}{the state to start from} 11 | } 12 | \description{ 13 | Zoom in on contents of R Markdown code submission. 14 | } 15 | -------------------------------------------------------------------------------- /man/check_that.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-that.R 3 | \name{check_that} 4 | \alias{check_that} 5 | \alias{test_what} 6 | \title{Expectation wrapper} 7 | \usage{ 8 | check_that(code, feedback, env = parent.frame()) 9 | 10 | test_what(code, feedback) 11 | } 12 | \arguments{ 13 | \item{code}{The expectation that should be wrapped} 14 | 15 | \item{feedback}{A character string with feedback when the expection is not 16 | met OR a list object, containing multiple pieces of information. This list 17 | should at least contain an element named \code{message}} 18 | 19 | \item{env}{environment in which the test should be evaluated; defaults to \code{parent.frame()}} 20 | } 21 | \description{ 22 | This function wraps around an is_... function. When the expectation fails to 23 | be met, the feedback message is sent to the reporter. You can use 24 | \code{\link{is_true}}, \code{\link{is_false}}, \code{\link{is_gte}} or 25 | \code{\link{is_equal}} 26 | } 27 | \examples{ 28 | \dontrun{ 29 | check_that(is_true(3 == 3)) 30 | check_that(is_false(3 == 4)) 31 | check_that(is_gte(4, 3)) 32 | check_that(is_equal(4, 4)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /man/check_title.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-rmd.R 3 | \name{check_title} 4 | \alias{check_title} 5 | \title{Check markdown header title} 6 | \usage{ 7 | check_title(state, not_found_msg = NULL, append = TRUE) 8 | } 9 | \arguments{ 10 | \item{state}{the state to start from. Should be a state produced by 11 | \code{\link{check_header}}.} 12 | 13 | \item{not_found_msg}{If specified, this overrides the automatically generated 14 | message in case no title was specified.} 15 | 16 | \item{append}{Whether or not to append the feedback to feedback built in 17 | previous states.} 18 | } 19 | \value{ 20 | A state that zooms in on the title of the header. 21 | } 22 | \description{ 23 | Checks if a title was specified for a markdown header. If not, generates a 24 | feedback message. If yes, zooms in on the title so you can use 25 | \code{check_equal}. 26 | } 27 | -------------------------------------------------------------------------------- /man/check_wd.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-wd.R 3 | \name{check_wd} 4 | \alias{check_wd} 5 | \title{Check whether a file exists} 6 | \usage{ 7 | check_wd(state, path, missing_msg = NULL) 8 | } 9 | \arguments{ 10 | \item{state}{the state to start from} 11 | 12 | \item{path}{Path to the file you want to check} 13 | 14 | \item{missing_msg}{Custom feedback message in case the file is missing} 15 | } 16 | \description{ 17 | Check whether a file exists 18 | } 19 | \examples{ 20 | \dontrun{ 21 | # Example 1 solution code: 22 | # write("hello", file = "test.txt") 23 | 24 | # SCT 25 | ex() \%>\% check_wd("test.txt") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /man/check_yaml.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-rmd.R 3 | \name{check_yaml} 4 | \alias{check_yaml} 5 | \title{Check markdown YAML header} 6 | \usage{ 7 | check_yaml(state, error_msg = NULL, append = TRUE) 8 | } 9 | \arguments{ 10 | \item{state}{the state to start from. This state should be produced by 11 | \code{\link{check_rmd}}.} 12 | 13 | \item{error_msg}{If specified, this overrides the automatically generated 14 | message in case the YAML header couldn't be parsed.} 15 | 16 | \item{append}{Whether or not to append the feedback to feedback built in 17 | previous states.} 18 | } 19 | \value{ 20 | A state that zooms in on the YAML header options. 21 | } 22 | \description{ 23 | Checks if a YAML header was specified and can be parsed. If not, generates a 24 | feedback message. If yes, parses the YAML header and zooms in on its options 25 | so you can use \code{\link{check_option}} to verify the options. 26 | } 27 | -------------------------------------------------------------------------------- /man/compare.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/comparison.R 3 | \name{compare} 4 | \alias{compare} 5 | \alias{is_true} 6 | \alias{is_false} 7 | \alias{is_gte} 8 | \alias{is_lt} 9 | \title{Check if object is true, false, >= or <.} 10 | \usage{ 11 | is_true(x) 12 | 13 | is_false(x) 14 | 15 | is_gte(x, y) 16 | 17 | is_lt(x, y) 18 | } 19 | \arguments{ 20 | \item{x}{object to test} 21 | 22 | \item{y}{single numeric value to compare} 23 | } 24 | \description{ 25 | Utility functions to use inside \code{\link{check_that}}. 26 | } 27 | -------------------------------------------------------------------------------- /man/disable_highlighting.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/state.R 3 | \name{disable_highlighting} 4 | \alias{disable_highlighting} 5 | \title{Disable highlighting for any future checks in the chain} 6 | \usage{ 7 | disable_highlighting(state) 8 | } 9 | \arguments{ 10 | \item{state}{the state to create a substate from} 11 | } 12 | \description{ 13 | If the function is used right after \code{ex()}, highlighting is disabled for 14 | the rest of the chain 15 | } 16 | \details{ 17 | If the function used 'deep in a chain', the system will fall back on 18 | highlights that were collected earlier in the state. 19 | } 20 | -------------------------------------------------------------------------------- /man/ex.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/state.R 3 | \name{ex} 4 | \alias{ex} 5 | \title{Get the main state} 6 | \usage{ 7 | ex() 8 | } 9 | \description{ 10 | \code{ex()} should be the start of every SCT chain 11 | } 12 | -------------------------------------------------------------------------------- /man/get_num_hits.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{get_num_hits} 4 | \alias{get_num_hits} 5 | \title{Get the number of hits for a series of regexes} 6 | \usage{ 7 | get_num_hits(regex, x, fixed) 8 | } 9 | \arguments{ 10 | \item{regex}{vector of regular expressions against which to match} 11 | 12 | \item{x}{character vector where matches are sought} 13 | 14 | \item{fixed}{logical. If \code{TRUE}, \code{regex} are strings to be matched as is.} 15 | } 16 | \description{ 17 | Get the number of hits for a series of regexes 18 | } 19 | -------------------------------------------------------------------------------- /man/is_equal.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/is-equal.R 3 | \name{is_equal} 4 | \alias{is_equal} 5 | \alias{is_equal.default} 6 | \alias{is_equal.formula} 7 | \title{Check equality of two objects} 8 | \usage{ 9 | is_equal(x, y, eq_condition = "equivalent") 10 | 11 | \method{is_equal}{default}(x, y, eq_condition = "equivalent") 12 | 13 | \method{is_equal}{formula}(x, y, eq_condition = "equivalent") 14 | } 15 | \arguments{ 16 | \item{x}{object to test} 17 | 18 | \item{y}{object to compare} 19 | 20 | \item{eq_condition}{how to compare the objects: \code{"equivalent"} (the default, 21 | does not check attributes), \code{"equal"} (checks attributes, but allows for 22 | errors in machine precision), or \code{"identical"} (exactly identical).} 23 | } 24 | \description{ 25 | Utility function to use inside \code{\link{check_that}}. 26 | } 27 | -------------------------------------------------------------------------------- /man/override.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/state.R 3 | \name{override} 4 | \alias{override} 5 | \alias{override_solution} 6 | \alias{override_solution_code} 7 | \alias{override_solution_env} 8 | \title{Functions to override solution code (and parse data) and variables in the solution environment.} 9 | \usage{ 10 | override_solution(state, code = NULL, ...) 11 | 12 | override_solution_code(state, code) 13 | 14 | override_solution_env(state, ...) 15 | } 16 | \arguments{ 17 | \item{state}{the state to create a substate from} 18 | 19 | \item{code}{the solution code to put into the state} 20 | 21 | \item{...}{named environment variables to add to or override in the solution environment} 22 | } 23 | \description{ 24 | Produces a new state with a custom solution code. Mostly useful inside 25 | \code{check_or}, if you want to test for different cases. 26 | } 27 | -------------------------------------------------------------------------------- /man/reexports.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \docType{import} 4 | \name{reexports} 5 | \alias{reexports} 6 | \alias{\%>\%} 7 | \title{Objects exported from other packages} 8 | \keyword{internal} 9 | \description{ 10 | These objects are imported from other packages. Follow the links 11 | below to see their documentation. 12 | 13 | \describe{ 14 | \item{magrittr}{\code{\link[magrittr]{\%>\%}}} 15 | }} 16 | 17 | -------------------------------------------------------------------------------- /man/run_until_fail.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/test-exercise.R 3 | \name{run_until_fail} 4 | \alias{run_until_fail} 5 | \title{Run SCT until it fails} 6 | \usage{ 7 | run_until_fail(code) 8 | } 9 | \arguments{ 10 | \item{code}{the SCT script to run as an expression} 11 | } 12 | \description{ 13 | Run SCT until it fails 14 | } 15 | -------------------------------------------------------------------------------- /man/s3defs.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/aaa-s3-definitions.R 3 | \name{s3defs} 4 | \alias{s3defs} 5 | \alias{check_equal} 6 | \alias{check_equal.default} 7 | \alias{check_result} 8 | \alias{check_result.default} 9 | \alias{check_body} 10 | \alias{check_body.default} 11 | \alias{check_option} 12 | \alias{check_option.default} 13 | \title{S3 definitions} 14 | \usage{ 15 | check_equal(state, ...) 16 | 17 | \method{check_equal}{default}(state, ...) 18 | 19 | check_result(state, ...) 20 | 21 | \method{check_result}{default}(state, ...) 22 | 23 | check_body(state, ...) 24 | 25 | \method{check_body}{default}(state, ...) 26 | 27 | check_option(state, ...) 28 | 29 | \method{check_option}{default}(state, ...) 30 | } 31 | \arguments{ 32 | \item{state}{State to start from} 33 | 34 | \item{...}{Additional arguments passed to implementations of the S3 method} 35 | } 36 | \description{ 37 | S3 definitions 38 | } 39 | -------------------------------------------------------------------------------- /man/setup_state.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils-state.R 3 | \name{setup_state} 4 | \alias{setup_state} 5 | \title{Set up state for local experimentation.} 6 | \usage{ 7 | setup_state(sol_code = "", stu_code = "", sol_env = NULL, 8 | stu_env = NULL, stu_result = NULL, pec = character(), 9 | ex_type = "NormalExercise", force_diagnose = FALSE) 10 | } 11 | \arguments{ 12 | \item{sol_code}{Solution script as a string. If it is not specified, the 13 | student code will be used.} 14 | 15 | \item{stu_code}{Student submission as a string. If it is not specified, the 16 | solution code will be used.} 17 | 18 | \item{sol_env}{Solution environment. If this is specified, the solution code is not rerun.} 19 | 20 | \item{stu_env}{Student environment. If this is specified, the student code is not rerun.} 21 | 22 | \item{stu_result}{Result of calling \code{\link{evaluate}} on the student code. If this is 23 | is specified, this overrides the output generated by running \code{stu_code}.} 24 | 25 | \item{pec}{Pre-exercise-code as a string} 26 | 27 | \item{ex_type}{Type of exercise as a string. Defaults to NormalExercise.} 28 | 29 | \item{force_diagnose}{whether diagnose tests have to pass even if the checks pass (FALSE by default)} 30 | } 31 | \value{ 32 | The exercise state, from which you can start chaining. 33 | } 34 | \description{ 35 | It runs both the solution and the student submission, and populates the state 36 | with parse data, output, etc. After running this function, the state is 37 | available thorugh \code{\link{ex}}, from which you can start your SCT chains. 38 | In a way, this function is a very light weight version of DataCamp's R Backend. 39 | } 40 | \note{ 41 | This function is only supposed to be used locally when experimenting. 42 | It should never be used in the eventual SCT script of an exercise. 43 | } 44 | \examples{ 45 | \dontrun{ 46 | setup_state( 47 | sol_code = "a <- 1", 48 | stu_code = "a <- 2" 49 | ) 50 | 51 | ex() \%>\% check_object('a') \%>\% check_equal() 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /man/state.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/state.R 3 | \docType{data} 4 | \name{state} 5 | \alias{state} 6 | \alias{State} 7 | \alias{RootState} 8 | \alias{ChildState} 9 | \title{testwhat states.} 10 | \format{An object of class \code{R6ClassGenerator} of length 24.} 11 | \usage{ 12 | State 13 | 14 | RootState 15 | 16 | ChildState 17 | } 18 | \description{ 19 | Root State has no parent state. 20 | ChildState does have state. 21 | Both inherit from the prototypical State class 22 | } 23 | \keyword{datasets} 24 | -------------------------------------------------------------------------------- /man/success_msg.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/success-msg.R 3 | \name{success_msg} 4 | \alias{success_msg} 5 | \title{Define the success message} 6 | \usage{ 7 | success_msg(msg, praise = FALSE) 8 | } 9 | \arguments{ 10 | \item{msg}{The success message as a character string.} 11 | 12 | \item{praise}{Whether or not to prepend a message of praise from the \code{praise} package.} 13 | } 14 | \description{ 15 | If all tests in an SCT pass, the student is presented with a 16 | congratulatory message. You can specify this message with 17 | /code{success_msg()}. It does not matter where in the SCT 18 | you specify this message, but at the end makes most sense. 19 | } 20 | \details{ 21 | For multiple choice exercises, the success message is specified 22 | inside \code{\link{check_mc}}, so an additional call of \code{success_msg} 23 | is not necessary. 24 | } 25 | -------------------------------------------------------------------------------- /man/test_chunk_options.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rstudio-test-chunk-options.R 3 | \name{test_chunk_options} 4 | \alias{test_chunk_options} 5 | \title{Check whether the student defined the correct chunk options (R Markdown 6 | exercises)} 7 | \usage{ 8 | test_chunk_options(options = NULL, not_called_msg = NULL, 9 | incorrect_msg = NULL) 10 | } 11 | \arguments{ 12 | \item{options}{Set of options} 13 | 14 | \item{not_called_msg}{feedback message if option was not specified} 15 | 16 | \item{incorrect_msg}{feedback message if option was incorrectly set} 17 | } 18 | \description{ 19 | Check whether the student defined the correct chunk options in an R Markdown 20 | exercise 21 | } 22 | \details{ 23 | This test can only be called inside a test_rmd_group() call! 24 | } 25 | \keyword{internal} 26 | -------------------------------------------------------------------------------- /man/test_exercise.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/test-exercise.R 3 | \name{test_exercise} 4 | \alias{test_exercise} 5 | \title{Run all tests for an exercise} 6 | \usage{ 7 | test_exercise(sct, ex_type, pec, student_code, solution_code, student_env, 8 | solution_env, output_list, allow_errors = FALSE, 9 | force_diagnose = FALSE, seed = 42) 10 | } 11 | \arguments{ 12 | \item{sct}{Submission correctness tests as a character string.} 13 | 14 | \item{ex_type}{Type of the exercise} 15 | 16 | \item{pec}{pre-exercise-code} 17 | 18 | \item{student_code}{character string representing the student code} 19 | 20 | \item{solution_code}{character string representing the solution code} 21 | 22 | \item{student_env}{environment containing the objects defined by the student.} 23 | 24 | \item{solution_env}{environment containing the objects defined by solution code} 25 | 26 | \item{output_list}{the output structure that is generated by RBackend} 27 | 28 | \item{allow_errors}{whether or not errors are allowed by RBackend (FALSE by default)} 29 | 30 | \item{force_diagnose}{whether diagnose tests have to pass even if the checks pass (FALSE by default)} 31 | 32 | \item{seed}{random seed that is used for SCTs that run expressions (42 by default).} 33 | } 34 | \value{ 35 | A list with components \code{passed} that indicates whether all 36 | tests were sucessful, and \code{feedback} that contains a feedback message. 37 | } 38 | \description{ 39 | Run all tests for an exercise and report the results (including feedback). 40 | This function is run by R Backend and should not be used by course creators. 41 | } 42 | -------------------------------------------------------------------------------- /man/test_expr.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-expr.R 3 | \name{check_expr} 4 | \alias{check_expr} 5 | \alias{check_result.ExprState} 6 | \alias{check_output.ExprState} 7 | \alias{check_error.ExprState} 8 | \alias{check_equal.ExprResultState} 9 | \alias{check_equal.ExprOutputState} 10 | \alias{check_equal.ExprErrorState} 11 | \title{Check the result, output or errors thrown by an expression} 12 | \usage{ 13 | check_expr(state, expr) 14 | 15 | \method{check_result}{ExprState}(state, error_msg = NULL, 16 | append = TRUE, ...) 17 | 18 | \method{check_output}{ExprState}(state, error_msg = NULL, 19 | append = TRUE, ...) 20 | 21 | \method{check_error}{ExprState}(state, no_error_msg = NULL, 22 | append = TRUE, ...) 23 | 24 | \method{check_equal}{ExprResultState}(state, eq_condition = "equivalent", 25 | eq_fun = NULL, incorrect_msg = NULL, append = TRUE, ...) 26 | 27 | \method{check_equal}{ExprOutputState}(state, eq_fun = NULL, 28 | incorrect_msg = NULL, append = TRUE, ...) 29 | 30 | \method{check_equal}{ExprErrorState}(state, eq_fun = NULL, 31 | incorrect_msg = NULL, append = TRUE, ...) 32 | } 33 | \arguments{ 34 | \item{state}{state to start from (only for \code{check_} functions)} 35 | 36 | \item{expr}{the expression to run} 37 | 38 | \item{error_msg}{custom message in case the expression throws an error while 39 | it shouldn't} 40 | 41 | \item{append}{Whether or not to append the feedback to feedback built in 42 | previous states} 43 | 44 | \item{...}{S3 stuff} 45 | 46 | \item{no_error_msg}{custom message in case the expression doesn't throw an 47 | error while it should} 48 | 49 | \item{eq_condition}{character string indicating how to compare. See 50 | \code{\link{is_equal}}.} 51 | 52 | \item{eq_fun}{optional argument to specify a custom equality function. The 53 | function should take two arguments and always return a single boolean 54 | value: \code{TRUE} or \code{FALSE}.} 55 | 56 | \item{incorrect_msg}{custom message in case the result, output or error of 57 | the expression does not correspond with the solution} 58 | } 59 | \description{ 60 | Run an expression in student and solution environment and compare the result, 61 | output or error that is thrown by it. 62 | } 63 | \examples{ 64 | \dontrun{ 65 | # Example 1 66 | a <- c(1, 2, 3, 4, 5, 6) 67 | 68 | # SCT 69 | ex() \%>\% check_expr("a[c(2, 4)]") \%>\% check_result() \%>\% check_equal() 70 | 71 | # Example 2 72 | my_fun <- function() { print('hello') } 73 | 74 | # SCT 75 | ex() \%>\% check_expr("my_fun()") \%>\% check_output() \%>\% check_equal() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /man/test_object.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/check-object.R 3 | \name{test_object} 4 | \alias{test_object} 5 | \alias{check_object} 6 | \alias{check_column} 7 | \alias{check_element} 8 | \alias{check_equal.ObjectState} 9 | \alias{check_equal.ObjectColumnState} 10 | \alias{check_equal.ObjectElementState} 11 | \title{Check R object existence and value} 12 | \usage{ 13 | check_object(state, name, undefined_msg = NULL, append = TRUE) 14 | 15 | check_column(state, col, col_missing_msg = NULL, append = TRUE) 16 | 17 | check_element(state, el, el_missing_msg = NULL, append = TRUE) 18 | 19 | \method{check_equal}{ObjectState}(state, incorrect_msg = NULL, 20 | append = TRUE, eq_condition = "equivalent", eq_fun = NULL, ...) 21 | 22 | \method{check_equal}{ObjectColumnState}(state, incorrect_msg = NULL, 23 | append = TRUE, eq_condition = "equivalent", eq_fun = NULL, ...) 24 | 25 | \method{check_equal}{ObjectElementState}(state, incorrect_msg = NULL, 26 | append = TRUE, eq_condition = "equivalent", eq_fun = NULL, ...) 27 | } 28 | \arguments{ 29 | \item{state}{the state to start from} 30 | 31 | \item{name}{name of the object to test.} 32 | 33 | \item{undefined_msg}{Optional feedback message in case the student did not 34 | define the object. A meaningful message is automatically generated if not 35 | supplied.} 36 | 37 | \item{append}{Whether or not to append the feedback to feedback built in 38 | previous states} 39 | 40 | \item{col}{name of column to check} 41 | 42 | \item{col_missing_msg}{Custom message in case data frame column is missing} 43 | 44 | \item{el}{name of element to check} 45 | 46 | \item{el_missing_msg}{Custom message in case element is messing.} 47 | 48 | \item{incorrect_msg}{Custom feedback message in case the student's object is 49 | not the same as in the sample solution.} 50 | 51 | \item{eq_condition}{character string indicating how to compare. See 52 | \code{\link{is_equal}}.} 53 | 54 | \item{eq_fun}{optional argument to specify a custom equality function. The 55 | function should take two arguments and always return a single boolean 56 | value: \code{TRUE} or \code{FALSE}.} 57 | 58 | \item{...}{S3 stuff} 59 | } 60 | \description{ 61 | Check whether a student defined a certain object (correctly) 62 | } 63 | \examples{ 64 | \dontrun{ 65 | 66 | # Example 1 67 | x <- mean(1:3, na.rm = TRUE) 68 | 69 | # sct to only check existence of x 70 | ex() \%>\% 71 | check_object("x") 72 | 73 | # sct to check existence and equality 74 | ex() \%>\% 75 | check_object("x") \%>\% 76 | check_equal() 77 | 78 | # Example 2 79 | df <- data.frame(a = 1:3, b = LETTERS[1:3]) 80 | 81 | # sct to test column a 82 | ex() \%>\% 83 | check_object("df") \%>\% 84 | check_column("a") \%>\% 85 | check_equal() 86 | 87 | # Example 3 88 | lst <- list(a = 1, b = 2) 89 | 90 | # sct to test only element b 91 | ex() \%>\% 92 | check_object("lst") \%>\% 93 | check_element("b") \%>\% 94 | check_equal() 95 | 96 | # Example 4 97 | today <- Sys.Date() 98 | 99 | # sct to check if classes are equal 100 | ex() \%>\% 101 | check_object("today") \%>\% 102 | check_equal(eq_fun = function(x, y) { all.equal(class(x), class(y)) }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /man/test_pipe.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rstudio-test-pipe.R 3 | \name{test_pipe} 4 | \alias{test_pipe} 5 | \title{Check whether a student used the pipe operator sufficiently (ggvis and dplyr exercises)} 6 | \usage{ 7 | test_pipe(num = 1, absent_msg = NULL, insuf_msg = NULL) 8 | } 9 | \arguments{ 10 | \item{num}{minimal number of times the pipe operator has to appear (default = 1)} 11 | 12 | \item{absent_msg}{feedback message in case the student did not use a single pipe.} 13 | 14 | \item{insuf_msg}{feeback message in case the student did not use the pipe operator sufficiently.} 15 | } 16 | \description{ 17 | Check whether a student used the pipe sufficiently. By default, the function only checks if 18 | the pipe was used at least once. The user can also select the minimal 19 | number of occurrences of the pipe. 20 | } 21 | \keyword{internal} 22 | -------------------------------------------------------------------------------- /man/test_props.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rstudio-test-props.r 3 | \name{test_props} 4 | \alias{test_props} 5 | \title{Check whether the student used the correct properties (ggvis exercises)} 6 | \usage{ 7 | test_props(index = 1, funs = "ggvis", props = NULL, 8 | allow_extra = TRUE, not_called_msg = NULL, incorrect_msg = NULL, 9 | error_msg = NULL) 10 | } 11 | \arguments{ 12 | \item{index}{number of ggvis caommdn to be checked} 13 | 14 | \item{funs}{the function in which to look for the x and y data. If the same info is found in one function, the test passes. 15 | All the functions that the teacher specifies, must be present in the students' solution! The function only looks for 16 | properties inside the first mentioned function by the teacher.} 17 | 18 | \item{props}{set of properties to be checked. If not specified, all properties found in the solution or checked on. If 19 | specified as an empty charactor vector (c()), only the calling of the functions will be checked on.} 20 | 21 | \item{allow_extra}{whether or not the definition of additional properties is accepted (default TRUE)} 22 | 23 | \item{not_called_msg}{feedback message in case the specified function(s) was/were not found.} 24 | 25 | \item{incorrect_msg}{feedback message in case the student specified properties do not correspond with the ones in the solution.} 26 | 27 | \item{error_msg}{feedback maessage in case the student submitted a faulty ggvis call} 28 | } 29 | \description{ 30 | Check whether the student used at least as many and the correct properties as the solution inside a specific 31 | command and inside a specific function. By default, this function will compare the ggvis functions of both 32 | student and solution. However, the teacher can also state that the definition of data can be done in other 33 | functions. 34 | } 35 | \keyword{internal} 36 | -------------------------------------------------------------------------------- /man/test_rmd_group.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rstudio-test-rmd-group.R 3 | \name{test_rmd_group} 4 | \alias{test_rmd_group} 5 | \title{Test a single R Markdown file group (R Markdown exercises)} 6 | \usage{ 7 | test_rmd_group(group_number, code) 8 | } 9 | \arguments{ 10 | \item{group_number}{Number of the group.} 11 | 12 | \item{code}{SCT code to test the group (in curly braces)} 13 | } 14 | \description{ 15 | Test a single R Markdown file group (R Markdown exercises) with arbitrary testwhat functions. 16 | } 17 | \keyword{internal} 18 | -------------------------------------------------------------------------------- /man/test_text.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rstudio-test-text.R 3 | \name{test_text} 4 | \alias{test_text} 5 | \title{Test inline text and formatting (Markdown)} 6 | \usage{ 7 | test_text(text, format = "any", freq = 1, not_called_msg = NULL, 8 | incorrect_msg = NULL) 9 | } 10 | \arguments{ 11 | \item{text}{Text to match (can be a regular expression!)} 12 | 13 | \item{format}{the format of the text that the text should be in ("any", "italics", "bold", "code", "inline_code", "brackets", "parentheses", "list"). 14 | If none of the above, the format string is appended to text in front and in the back and used as a regexp.} 15 | 16 | \item{freq}{How often the text should appear with this formatting} 17 | 18 | \item{not_called_msg}{feedback message if the text was not there} 19 | 20 | \item{incorrect_msg}{feedback message if the text was not properly formatted} 21 | } 22 | \description{ 23 | Test inline text and its formatting for R Markdown exercises. 24 | This test can only be called inside a test_rmd_group() call! 25 | } 26 | \keyword{internal} 27 | -------------------------------------------------------------------------------- /man/test_yaml_header.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/rstudio-test-yaml-header.R 3 | \name{test_yaml_header} 4 | \alias{test_yaml_header} 5 | \title{Check yaml header (Markdown)} 6 | \usage{ 7 | test_yaml_header(options = NULL, check_equality = TRUE, 8 | not_called_msg = NULL, incorrect_msg = NULL) 9 | } 10 | \arguments{ 11 | \item{options}{Set of options. Embedded options have to be specified using the dot notation.} 12 | 13 | \item{check_equality}{whether or not to actually check the value assigned to the option (default TRUE)} 14 | 15 | \item{not_called_msg}{feedback message if option was not specified (optional but recommended)} 16 | 17 | \item{incorrect_msg}{feedback message if option was incorrectly set (optional but recommended)} 18 | } 19 | \description{ 20 | Check whether the student specified the correct options in the yaml header (for 21 | R Markdown exercises). This test should be called outside an test_rmd_group call. 22 | } 23 | \keyword{internal} 24 | -------------------------------------------------------------------------------- /man/testwhat_learnr.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils-learnr.R 3 | \name{testwhat_learnr} 4 | \alias{testwhat_learnr} 5 | \title{A checker function to use with learnr} 6 | \usage{ 7 | testwhat_learnr(label = NULL, solution_code = NULL, user_code = NULL, 8 | check_code = NULL, envir_result = NULL, evaluate_result = NULL, 9 | ...) 10 | } 11 | \arguments{ 12 | \item{label}{Label for exercise chunk} 13 | 14 | \item{solution_code}{R code submitted by the user} 15 | 16 | \item{user_code}{Code provided within the "-solution" chunk for the 17 | exercise.} 18 | 19 | \item{check_code}{Code provided within the "-check" chunk for the exercise.} 20 | 21 | \item{envir_result}{The R environment after the execution of the chunk.} 22 | 23 | \item{evaluate_result}{The return value from the \code{evaluate::evaluate} 24 | function.} 25 | 26 | \item{...}{Unused (include for compatibility with parameters to be added in 27 | the future)} 28 | } 29 | \value{ 30 | An R list which contains several fields indicating the result of the 31 | check. 32 | } 33 | \description{ 34 | For exercise checking, learnr tutorials require a function that learnr can 35 | use in the background to run the code in each "-check" chunk and to format 36 | the results into a format that learnr can display. The function must accept a 37 | specific set of inputs and return a specific type of output. Users are not 38 | intended to use the function themselves, but to pass it to the 39 | \code{exercise.checker} knitr chunk option within the setup chunk of the 40 | tutorial. 41 | } 42 | \details{ 43 | Similar to grader's \code{grade_learnr()}, testwhat provides 44 | \code{testwhat_learnr()} for this purpose. To enable exercise checking in 45 | your learnr tutorial, set \code{tutorial_options(exercise.checker = 46 | testwhat_learnr)} in the setup chunk of your tutorial. 47 | } 48 | -------------------------------------------------------------------------------- /man/tw.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \docType{data} 4 | \name{tw} 5 | \alias{tw} 6 | \title{tw singleton object to access data across SCT chains.} 7 | \format{An object of class \code{list} of length 4.} 8 | \usage{ 9 | tw 10 | } 11 | \description{ 12 | tw singleton object to access data across SCT chains. 13 | } 14 | \keyword{datasets} 15 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library("testthat") 2 | library("testwhat") 3 | 4 | test_check("testwhat") -------------------------------------------------------------------------------- /tests/testthat/.Rapp.history: -------------------------------------------------------------------------------- 1 | load("/Users/filip/workspace/testwhat/tests/testthat/temp.rds") 2 | -------------------------------------------------------------------------------- /tests/testthat/checking-function-calls.R: -------------------------------------------------------------------------------- 1 | ## ----setup, include = FALSE---------------------------------------------- 2 | knitr::opts_chunk$set( 3 | collapse = TRUE, 4 | comment = "#>", 5 | eval = FALSE 6 | ) 7 | 8 | ## ------------------------------------------------------------------------ 9 | # Round pi to three digits 10 | round(pi, 3) 11 | 12 | ## ------------------------------------------------------------------------ 13 | fun <- ex() %>% check_function("round") %>% { 14 | check_arg(., "x") %>% check_equal() 15 | check_arg(., "digits") %>% check_equal() 16 | } 17 | 18 | ## ------------------------------------------------------------------------ 19 | fun <- ex() %>% check_function("round", not_called_msg = "Have you used `round()` to round `pi`?") 20 | fun %>% 21 | check_arg("x", arg_not_specified_msg = "Have you specified the number to round?") %>% 22 | check_equal("Have you correctly specified that `round()` should round `pi`?") 23 | fun %>% 24 | check_arg("digits", arg_not_specfied_msg = "Have you specified to how many digits the number should be rounded?") %>% 25 | check_equal("Have you correctly set the `digits` argument to 3?") 26 | 27 | ## ------------------------------------------------------------------------ 28 | fun <- ex() %>% check_function("round") 29 | fun %>% check_arg("x") %>% check_equal(eval = FALSE) 30 | fun %>% check_arg("digits") %>% check_equal() 31 | 32 | ## ------------------------------------------------------------------------ 33 | # Call round on pi 34 | round(pi, 3) 35 | 36 | # Call round on e 37 | round(exp(1), 3) 38 | 39 | ## ------------------------------------------------------------------------ 40 | ex() %>% check_function("round", index = 1) %>% check_arg("x") %>% check_equal() 41 | ex() %>% check_function("round", index = 2) %>% check_arg("x") %>% check_equal() 42 | 43 | ## ------------------------------------------------------------------------ 44 | # Call round on pi 45 | round(pi, 3) 46 | 47 | # Call round on e 48 | round(exp(1), 3) 49 | 50 | ## ------------------------------------------------------------------------ 51 | ex() %>% check_function("round") %>% check_arg("x") %>% check_equal() 52 | 53 | ## ------------------------------------------------------------------------ 54 | # Call round on pi 55 | round(pi, 3) 56 | 57 | # Call round on e 58 | round(exp(2), 3) 59 | 60 | ## ------------------------------------------------------------------------ 61 | ex() %>% check_function("round", index = 1) %>% check_arg("x") %>% check_equal() 62 | ex() %>% check_function("round", index = 2) %>% check_arg("x") %>% check_equal() 63 | 64 | ## ------------------------------------------------------------------------ 65 | print(1.1234) 66 | 67 | ## ------------------------------------------------------------------------ 68 | print("1.1234") 69 | 70 | ## ------------------------------------------------------------------------ 71 | grepl(pattern = "a{2}", x = "aabb") 72 | grepl(pat = "a{2}", x = "aabb") 73 | grepl("a{2}", x = "aabb") 74 | grepl("a{2}", "aabb") 75 | grepl(x = "aabb", pattern = "a{2}") 76 | grepl(x = "aabb", "a{2}") 77 | 78 | ## ------------------------------------------------------------------------ 79 | grepl(pattern = "a{2}", x = "aabb") 80 | 81 | ## ------------------------------------------------------------------------ 82 | sum(1, 2, 3, 4, NA, na.rm = TRUE) 83 | 84 | ## ------------------------------------------------------------------------ 85 | ex() %>% check_function("sum") %>% { 86 | check_arg(., "...") %>% check_equal() 87 | check_arg(., "na.rm") %>% check_equal() 88 | } 89 | 90 | ## ------------------------------------------------------------------------ 91 | sum(c(1, 2, 3, 4, NA), na.rm = TRUE) 92 | 93 | ## ------------------------------------------------------------------------ 94 | test_output_contains("10", incorrect_msg = "Did you correctly print out the sum?") 95 | 96 | ## ------------------------------------------------------------------------ 97 | df <- data.frame(time = seq(0, 2*pi, 0.01)) 98 | df$res <- sin(df$time) 99 | 100 | # create a plot of res vs time 101 | plot(df$time, df$res) 102 | 103 | ## ------------------------------------------------------------------------ 104 | test_function("plot", args = c("x", "y")) 105 | 106 | ## ------------------------------------------------------------------------ 107 | test_or({ 108 | fun <- ex() %>% check_function('plot') 109 | fun %>% check_arg('x') %>% check_equal() 110 | fun %>% check_arg('y') %>% check_equal() 111 | }, { 112 | fun <- ex() %>% override_solution('plot(res ~ time, data = df)') %>% check_function('plot') 113 | fun %>% check_arg('formula') %>% check_equal() 114 | fun %>% check_arg('data') %>% check_equal() 115 | }, { 116 | ex() %>% override_solution('plot(df$res ~ df$time)') %>% check_function('plot') %>% check_arg('formula') %>% check_equal() 117 | }) 118 | 119 | ## ------------------------------------------------------------------------ 120 | # Calculate sum of vector 1 to 5 121 | sum(1:5) 122 | 123 | ## ------------------------------------------------------------------------ 124 | ex() %>% check_function("sum") %>% check_result() %>% check_equal() 125 | 126 | -------------------------------------------------------------------------------- /tests/testthat/helper.R: -------------------------------------------------------------------------------- 1 | library(testwhat) 2 | 3 | `%||%` <- function(a, b) { 4 | if (!is.null(a)) a else b 5 | } 6 | 7 | test_it <- function(lst) { 8 | ex_type <- lst$DC_TYPE %||% "NormalExercise" 9 | setup_state(sol_code = lst$DC_SOLUTION %||% "", 10 | stu_code = lst$DC_CODE %||% "", 11 | sol_env = NULL, 12 | stu_env = NULL, 13 | stu_result = NULL, 14 | pec = lst$DC_PEC %||% "", 15 | ex_type = ex_type, 16 | force_diagnose = lst$DC_FORCE_DIAGNOSE %||% FALSE) 17 | post_process(run_until_fail(parse(text = lst$DC_SCT %||% "")), 18 | ex_type = ex_type) 19 | } 20 | 21 | passes <- function(res, mess_patt = NULL) { 22 | expect_true(res$correct) 23 | if (!is.null(mess_patt)) { 24 | expect_true(grepl(mess_patt, res$message)) 25 | } 26 | } 27 | 28 | passes2 <- function(res) { 29 | expect_true(inherits(res, "State")) 30 | } 31 | 32 | fails <- function(res, mess_patt = NULL) { 33 | expect_false(res$correct) 34 | if (!is.null(mess_patt)) { 35 | expect_true(grepl(mess_patt, res$message)) 36 | } 37 | } 38 | 39 | fb_contains <- function(res, mess_patt, fixed = TRUE) { 40 | expect_true(grepl(mess_patt, res$message, fixed = fixed)) 41 | } 42 | 43 | fb_excludes <- function(res, mess_patt, fixed = TRUE) { 44 | expect_false(grepl(mess_patt, res$message, fixed = fixed)) 45 | } 46 | 47 | line_info <- function(res, line_start, line_end, column_start, column_end) { 48 | expect_equal(res$line_start, line_start) 49 | expect_equal(res$line_end, line_end) 50 | if(!missing(column_start)) expect_equal(res$column_start, column_start) 51 | if(!missing(column_end)) expect_equal(res$column_end, column_end) 52 | } 53 | 54 | no_line_info <- function(res) { 55 | expect_false(any(c("line_start", "line_end", "column_start", "column_end") %in% names(res))) 56 | } 57 | 58 | print_fb <- function(output) { 59 | cat("\n", "FBM: \"", testwhat:::trim(res$message), "\"\n", sep = "") 60 | } -------------------------------------------------------------------------------- /tests/testthat/test-check-code.R: -------------------------------------------------------------------------------- 1 | context("check_code") 2 | 3 | # Note: the hashtags in the DC_CODE's below are to avoid any runtime errors 4 | # (they aren't a problem actually) 5 | 6 | test_that("check_code - basic", { 7 | lst <- list() 8 | lst$DC_SCT <- "ex() %>% check_code('a{2}')" 9 | 10 | lst$DC_CODE <- "# a" 11 | output <- test_it(lst) 12 | fails(output, mess_patt = "the pattern a\\{2\\}") 13 | 14 | lst$DC_CODE <- "# aa" 15 | output <- test_it(lst) 16 | passes(output) 17 | }) 18 | 19 | test_that("check_code - basic - custom", { 20 | lst <- list() 21 | lst$DC_SCT <- "ex() %>% check_code('a{2}', missing_msg = 'missing')" 22 | 23 | lst$DC_CODE <- "# a" 24 | output <- test_it(lst) 25 | fails(output, mess_patt = "Missing") 26 | 27 | lst$DC_CODE <- "# aa" 28 | output <- test_it(lst) 29 | passes(output) 30 | }) 31 | 32 | test_that("check_code - different patterns", { 33 | lst <- list() 34 | lst$DC_SCT <- "ex() %>% check_code(c('a{2}', 'b{2}'))" 35 | 36 | lst$DC_CODE <- "# a" 37 | output <- test_it(lst) 38 | fails(output) 39 | 40 | lst$DC_CODE <- "# aa" 41 | output <- test_it(lst) 42 | passes(output) 43 | 44 | lst$DC_CODE <- "# bb" 45 | output <- test_it(lst) 46 | passes(output) 47 | }) 48 | 49 | test_that("check_code - fixed", { 50 | lst <- list() 51 | lst$DC_SCT <- "ex() %>% check_code('a{2}', fixed = TRUE)" 52 | 53 | lst$DC_CODE <- "# a" 54 | output <- test_it(lst) 55 | fails(output) 56 | 57 | lst$DC_CODE <- "# aa" 58 | output <- test_it(lst) 59 | fails(output) 60 | 61 | lst$DC_CODE <- "# a{2}" 62 | output <- test_it(lst) 63 | passes(output) 64 | }) 65 | 66 | test_that("check_code - times", { 67 | lst <- list() 68 | lst$DC_SCT <- "ex() %>% check_code('a{2}', times = 2)" 69 | 70 | lst$DC_CODE <- "# a" 71 | output <- test_it(lst) 72 | fails(output) 73 | 74 | lst$DC_CODE <- "# aa" 75 | output <- test_it(lst) 76 | fails(output) 77 | 78 | lst$DC_CODE <- "# aaaa" 79 | output <- test_it(lst) 80 | passes(output) 81 | }) 82 | 83 | test_that("check_code - drop_comments", { 84 | lst <- list() 85 | lst$DC_SCT <- "ex() %>% check_code('a', drop_comments = TRUE)" 86 | 87 | lst$DC_CODE <- "# a" 88 | output <- test_it(lst) 89 | fails(output) 90 | 91 | lst$DC_CODE <- "a <- 1" 92 | output <- test_it(lst) 93 | passes(output) 94 | }) 95 | 96 | 97 | test_that("check_code - different patterns + fixed + times", { 98 | lst <- list() 99 | lst$DC_SCT <- "ex() %>% check_code(c('a{2}', 'b{2}'), times = 3, fixed = TRUE)" 100 | 101 | lst$DC_CODE <- "# aaaabb" 102 | output <- test_it(lst) 103 | fails(output) 104 | 105 | lst$DC_CODE <- "# aabbbb" 106 | output <- test_it(lst) 107 | fails(output) 108 | 109 | lst$DC_CODE <- "# bbbb" 110 | output <- test_it(lst) 111 | fails(output) 112 | 113 | lst$DC_CODE <- "# a{2} b{2}" 114 | output <- test_it(lst) 115 | fails(output) 116 | 117 | lst$DC_CODE <- "# a{2} b{2} b{2}" 118 | output <- test_it(lst) 119 | passes(output) 120 | 121 | lst$DC_CODE <- "# a{2} a{2} b{2}" 122 | output <- test_it(lst) 123 | passes(output) 124 | 125 | lst$DC_CODE <- "# a{2} b{2} b{2}" 126 | output <- test_it(lst) 127 | passes(output) 128 | }) 129 | 130 | test_that("check_code - backwards compatible (test_student_typed)", { 131 | lst <- list() 132 | lst$DC_SCT <- "test_student_typed(c('a{2}', 'b{2}'), times = 3)" # fixed = TRUE by default! 133 | 134 | lst$DC_CODE <- "# aaaabb" 135 | output <- test_it(lst) 136 | fails(output) 137 | 138 | lst$DC_CODE <- "# aabbbb" 139 | output <- test_it(lst) 140 | fails(output) 141 | 142 | lst$DC_CODE <- "# bbbb" 143 | output <- test_it(lst) 144 | fails(output) 145 | 146 | lst$DC_CODE <- "# a{2} b{2}" 147 | output <- test_it(lst) 148 | fails(output) 149 | 150 | lst$DC_CODE <- "# a{2} b{2} b{2}" 151 | output <- test_it(lst) 152 | passes(output) 153 | 154 | lst$DC_CODE <- "# a{2} a{2} b{2}" 155 | output <- test_it(lst) 156 | passes(output) 157 | 158 | lst$DC_CODE <- "# a{2} b{2} b{2}" 159 | output <- test_it(lst) 160 | passes(output) 161 | }) 162 | 163 | test_that("check_code - test_student_typed doesn't append", { 164 | lst <- list() 165 | lst$DC_SOLUTION <- "if (TRUE) { print('hello') }" 166 | lst$DC_CODE <- "if (TRUE) { print('goodnight') }" 167 | 168 | lst$DC_SCT <- "test_if_else(if_expr_test = test_student_typed('hello'))" 169 | output <- test_it(lst) 170 | fails(output) 171 | fb_contains(output, "Check the first if statement.") 172 | fb_contains(output, "Check the if part.") 173 | fb_contains(output, "Have you typed hello?") 174 | 175 | lst$DC_SCT <- "test_if_else(if_expr_test = test_student_typed('hello', not_typed_msg = 'nottyped'))" 176 | output <- test_it(lst) 177 | fails(output) 178 | fb_excludes(output, "Check the first if statement.") 179 | fb_excludes(output, "Check the if part.") 180 | fb_contains(output, "Nottyped") 181 | }) -------------------------------------------------------------------------------- /tests/testthat/test-check-error.R: -------------------------------------------------------------------------------- 1 | context("check_error") 2 | 3 | test_that("check_error", { 4 | lst <- list() 5 | lst$DC_SCT <- "ex () %>% check_error()" 6 | 7 | lst$DC_CODE <- "3 + 3" 8 | output <- test_it(lst) 9 | passes(output) 10 | 11 | lst$DC_CODE <- "\"a\" + 3" 12 | output <- test_it(lst) 13 | fails(output) 14 | }) 15 | 16 | test_that("check_error - backwards compatible", { 17 | lst <- list() 18 | lst$DC_SCT <- "test_error()" 19 | 20 | lst$DC_CODE <- "3 + 3" 21 | output <- test_it(lst) 22 | passes(output) 23 | 24 | lst$DC_CODE <- "\"a\" + 3" 25 | output <- test_it(lst) 26 | fails(output) 27 | }) 28 | 29 | test_that("check_error - line of error", { 30 | lst <- list() 31 | lst$DC_SCT <- "ex() %>% check_error()" 32 | lst$DC_ECHO <- TRUE # This is important here! 33 | 34 | lst$DC_CODE <- "a <- b" 35 | output <- test_it(lst) 36 | fails(output) 37 | line_info(output, 1, 1) 38 | 39 | lst$DC_CODE <- "a <- b\na <- b" 40 | output <- test_it(lst) 41 | fails(output) 42 | line_info(output, 1, 1) 43 | 44 | lst$DC_CODE <- "b <- 4\na <- b\nrm(b)\na <- b" 45 | output <- test_it(lst) 46 | fails(output) 47 | line_info(output, 4, 4) 48 | 49 | lst$DC_CODE <- "sum(\"a\", \n\n \"b\")" 50 | output <- test_it(lst) 51 | fails(output) 52 | line_info(output, 1, 3) 53 | }) 54 | 55 | test_that("check_error - incorrect_msg", { 56 | lst <- list() 57 | lst$DC_SCT <- "test_error(incorrect_msg = 'additionalmessage')" 58 | 59 | lst$DC_CODE <- "3 + 3" 60 | output <- test_it(lst) 61 | passes(output) 62 | 63 | lst$DC_CODE <- "\"a\" + 3" 64 | output <- test_it(lst) 65 | fb_contains(output, "non-numeric argument to binary operator") 66 | fb_contains(output, "additionalmessage") 67 | fails(output) 68 | }) -------------------------------------------------------------------------------- /tests/testthat/test-check-ggplot.R: -------------------------------------------------------------------------------- 1 | context("test_ggplot") 2 | 3 | pec <- "library(ggplot2)" 4 | ex_code <- 'ggplot(mtcars, aes(x = factor(cyl), fill = factor(am))) + 5 | geom_bar(position = "dodge") + 6 | scale_fill_manual("Transmission", values = c("#E41A1C", "#377EB8"), labels = c("Manual", "Automatic")) + 7 | scale_y_continuous("Number") + 8 | scale_x_discrete("Cylinders") + xlab("test")' 9 | 10 | test_that("test_ggplot works 1", { 11 | res <- setup_state(ex_code, ex_code, pec = pec) %>% check_ggplot(1, check = c("geom", "scale"), exact_geom = TRUE, check_extra = "xlab") 12 | passes2(res) 13 | }) 14 | 15 | test_that("backwards comp", { 16 | setup_state(ex_code, ex_code, pec = pec) 17 | res <- test_ggplot(1, check = c("geom", "scale"), exact_geom = TRUE, check_extra = "xlab") 18 | passes2(res) 19 | }) 20 | 21 | test_that("test_ggplot works 2", { 22 | s <- setup_state( 23 | stu_code = 'ggplot(mtcars, aes(x = wt, y = mpg)) + geom_smooth(se = F)', 24 | sol_code = 'ggplot(mtcars, aes(x = wt, y = mpg)) + stat_smooth(method = "auto",se = F)', 25 | pec = pec 26 | ) 27 | 28 | passes2(s %>% check_ggplot(1, check = "geom", check_geom_params = "method")) 29 | passes2(s %>% check_ggplot(1, check = "geom", check_geom_params = "se")) 30 | }) 31 | 32 | test_that("spots wrong facetting (grid)", { 33 | code <- "ggplot(CO2, aes(conc, uptake)) + geom_point() + facet_grid(Treatment ~ Type)" 34 | s <- setup_state( 35 | stu_code = code, 36 | sol_code = "ggplot(CO2, aes(conc, uptake)) + geom_point() + facet_grid(. ~ Plant)", 37 | pec = pec 38 | ) 39 | expect_error(check_ggplot(s), class = "sct_failure") 40 | s <- setup_state( 41 | stu_code = code, 42 | sol_code = code, 43 | pec = pec 44 | ) 45 | passes2(check_ggplot(s)) 46 | }) 47 | 48 | test_that("spots wrong facetting (wrap)", { 49 | code <- "ggplot(CO2, aes(conc, uptake)) + geom_point() + facet_wrap(~ Type)" 50 | s <- setup_state( 51 | stu_code = code, 52 | sol_code = "ggplot(CO2, aes(conc, uptake)) + geom_point() + facet_wrap(~ Plant)", 53 | pec = pec 54 | ) 55 | expect_error(check_ggplot(s), class = "sct_failure") 56 | s <- setup_state( 57 | stu_code = code, 58 | sol_code = code, 59 | pec = pec 60 | ) 61 | passes2(check_ggplot(s)) 62 | }) 63 | 64 | test_that("can handle the pipe operator", { 65 | code <- "mtcars %>% filter(gear == 4) %>% ggplot(aes(x = hp, y = wt)) + geom_point()" 66 | s <- setup_state( 67 | pec = paste0(pec, "\nlibrary(dplyr)"), 68 | stu_code = code, 69 | sol_code = code 70 | ) 71 | passes2(check_ggplot(s)) 72 | }) 73 | 74 | test_that("can handle british students", { 75 | code <- "ggplot(mtcars, aes(x = wt, y = hp)) + geom_point(aes(colour = factor(cyl)))" 76 | scale <- " + scale_colour_manual(values = c('red', 'blue', 'green'))" 77 | s <- setup_state( 78 | pec = pec, 79 | stu_code = gsub("scale_colour", "scale_color", paste0(code, scale)), 80 | sol_code = paste0(code, scale) 81 | ) 82 | passes2(check_ggplot(s)) 83 | s2 <- setup_state( 84 | pec = pec, 85 | stu_code = paste0(code, scale), 86 | sol_code = paste0(code, scale) 87 | ) 88 | passes2(check_ggplot(s2)) 89 | s3 <- setup_state( 90 | pec = pec, 91 | stu_code = code, 92 | sol_code = paste0(code, scale) 93 | ) 94 | expect_error(check_ggplot(s3), class = "sct_failure") 95 | }) 96 | 97 | test_that("can handle exotic geom_labels", { 98 | code <- "ggplot(cars, aes(speed, dist)) + geom_label(label = rownames(cars))" 99 | s <- setup_state( 100 | pec = pec, 101 | stu_code = code, 102 | sol_code = code 103 | ) 104 | passes2(check_ggplot(s)) 105 | }) 106 | 107 | test_that("different ways of specifying ggplots", { 108 | # normal + assignment 109 | plotcalls <- c( 110 | "ggplot(cars, aes(speed, dist)) + geom_point()", 111 | "x <- ggplot(cars, aes(speed, dist)) + geom_point()" 112 | ) 113 | for (p in plotcalls) { 114 | s <- setup_state(pec = pec, stu_code = p, sol_code = plotcalls[1]) 115 | passes2(check_ggplot(s)) 116 | } 117 | 118 | # incremental 119 | code <- "x <- ggplot(cars, aes(speed, dist))\nx + geom_point()" 120 | s <- setup_state(pec = pec, stu_code = code, sol_code = code) 121 | passes2(check_ggplot(s, 1)) 122 | passes2(check_ggplot(s, 2)) 123 | 124 | # in pec 125 | local_pec <- "library(ggplot2)\nx <- ggplot(cars, aes(speed, dist))" 126 | code <- "x + geom_point()" 127 | s <- setup_state(pec = local_pec, stu_code = code, sol_code = code) 128 | passes2(check_ggplot(s, 1)) 129 | }) 130 | 131 | -------------------------------------------------------------------------------- /tests/testthat/test-check-library-function.R: -------------------------------------------------------------------------------- 1 | context("test_library_function") 2 | 3 | test_that("test/check_library(_function) works", { 4 | for (sct in c("test_library_function('yaml')", 5 | "ex() %>% check_library('yaml')")) { 6 | lst <- list(DC_SCT = sct) 7 | for (s in c("library(yaml)", 8 | "library('yaml')", 9 | "library(\"yaml\")", 10 | "require(yaml)", 11 | "require('yaml')", 12 | "require(\"yaml\")")) { 13 | lst$DC_CODE <- s 14 | output <- test_it(lst) 15 | passes(output) 16 | } 17 | 18 | for (s in c("", "library(ggvis)", "require('ggvis')")) { 19 | lst$DC_CODE <- s 20 | output <- test_it(lst) 21 | fails(output) 22 | } 23 | } 24 | }) 25 | 26 | test_that("test/check_library(_function) messaging", { 27 | for (sct in c("test_library_function('yaml', not_called_msg = 'NCM', incorrect_msg = 'ICM')", 28 | "ex() %>% check_library('yaml', not_called_msg = 'NCM', incorrect_msg = 'ICM')")) { 29 | lst <- list(DC_SCT = sct) 30 | lst$DC_CODE <- "" 31 | output <- test_it(lst) 32 | fails(output, "NCM") 33 | 34 | lst$DC_CODE <- "#library(" 35 | output <- test_it(lst) 36 | fails(output, "NCM") 37 | 38 | lst$DC_CODE <- "library('ggvis')" 39 | output <- test_it(lst) 40 | fails(output, "ICM") 41 | } 42 | }) 43 | 44 | -------------------------------------------------------------------------------- /tests/testthat/test-check-mc.R: -------------------------------------------------------------------------------- 1 | context("test_mc") 2 | 3 | test_that("check_mc works", { 4 | lst <- list() 5 | lst$DC_TYPE <- "MultipleChoiceExercise" 6 | lst$DC_CODE <- "DM.result <- 2" 7 | 8 | lst$DC_SCT <- "ex() %>% check_mc(2, feedback_msgs = c('this is the WRONG answer', 'this is the CORRECT answer'))" 9 | output <- test_it(lst) 10 | passes(output, mess_patt = "This is the CORRECT answer") 11 | 12 | lst$DC_SCT <- "ex() %>% check_mc(2)" 13 | output <- test_it(lst) 14 | passes(output) 15 | 16 | lst$DC_SCT <- "ex() %>% check_mc(1, feedback_msgs = c('this is the CORRECT answer', 'this is the WRONG answer'))" 17 | output <- test_it(lst) 18 | fails(output, mess_patt = "This is the WRONG answer") 19 | 20 | lst$DC_SCT <- "ex() %>% check_mc(1)" 21 | output <- test_it(lst) 22 | fails(output) 23 | 24 | lst$DC_SCT <- "ex() %>% check_mc(1, feedback_msgs = c('not enugh messages'))" 25 | expect_error(test_it(lst)) 26 | 27 | lst <- list() 28 | lst$DC_TYPE <- "MultipleChoiceExercise" 29 | lst$DC_CODE <- "not.DM.result <- 12" 30 | lst$DC_SCT <- "ex() %>% check_mc(1)" 31 | output <- test_it(lst) 32 | fails(output, mess_patt = "Please select one of the options!") 33 | 34 | lst <- list() 35 | lst$DC_TYPE <- "MultipleChoiceExercise" 36 | lst$DC_CODE <- "DM.result <- 2" 37 | lst$DC_SCT <- "ex() %>% check_mc(c(1, 2))" 38 | output <- test_it(lst) 39 | passes(output) 40 | }) 41 | 42 | test_that("test_mc works", { 43 | lst <- list() 44 | lst$DC_TYPE <- "MultipleChoiceExercise" 45 | lst$DC_CODE <- "DM.result <- 2" 46 | 47 | lst$DC_SCT <- "test_mc(2, feedback_msgs = c('this is the WRONG answer', 'this is the CORRECT answer'))" 48 | output <- test_it(lst) 49 | passes(output, mess_patt = "This is the CORRECT answer") 50 | 51 | lst$DC_SCT <- "test_mc(2)" 52 | output <- test_it(lst) 53 | passes(output) 54 | 55 | lst$DC_SCT <- "test_mc(1, feedback_msgs = c('this is the CORRECT answer', 'this is the WRONG answer'))" 56 | output <- test_it(lst) 57 | fails(output, mess_patt = "This is the WRONG answer") 58 | 59 | lst$DC_SCT <- "test_mc(1)" 60 | output <- test_it(lst) 61 | fails(output) 62 | 63 | lst$DC_SCT <- "test_mc(1, feedback_msgs = c('not enugh messages'))" 64 | expect_error(test_it(lst)) 65 | 66 | lst <- list() 67 | lst$DC_TYPE <- "MultipleChoiceExercise" 68 | lst$DC_CODE <- "not.DM.result <- 12" 69 | lst$DC_SCT <- "test_mc(1)" 70 | output <- test_it(lst) 71 | fails(output, mess_patt = "Please select one of the options!") 72 | 73 | lst <- list() 74 | lst$DC_TYPE <- "MultipleChoiceExercise" 75 | lst$DC_CODE <- "DM.result <- 2" 76 | lst$DC_SCT <- "test_mc(c(1, 2))" 77 | output <- test_it(lst) 78 | passes(output) 79 | }) -------------------------------------------------------------------------------- /tests/testthat/test-check-that.R: -------------------------------------------------------------------------------- 1 | context("check_that") 2 | 3 | test_that("check_that works as it should", { 4 | expect_error(run_until_fail(check_that(is_true(TRUE)))) 5 | expect_error(run_until_fail(check_that(is_true(TRUE), feedback = NULL))) 6 | expect_error(run_until_fail(check_that(is_true(TRUE), feedback = list(not_message = "test")))) 7 | expect_error(run_until_fail(check_that(is_true(TRUE), feedback = list(message = NULL)))) 8 | }) 9 | 10 | test_that("backwards compatibility", { 11 | tw$set(state = RootState$new(test_env = environment())) 12 | expect_true(run_until_fail(test_what(expect_true(TRUE), feedback = 'testtest'))$correct) 13 | expect_false(run_until_fail(test_what(expect_true(FALSE), feedback = 'testtest'))$correct) 14 | }) 15 | -------------------------------------------------------------------------------- /tests/testthat/test-check-wd.R: -------------------------------------------------------------------------------- 1 | context("check_wd") 2 | 3 | test_that("check_wd - 1", { 4 | withr::with_file("testing.txt", { 5 | writeLines("test", con = "testing.txt") 6 | s <- setup_state(stu_code = "") 7 | passes2(ex() %>% check_wd(path = 'testing.txt')) 8 | passes2(test_file_exists('testing.txt')) 9 | }) 10 | }) 11 | 12 | test_that("check_wd - 2", { 13 | expect_error(ex() %>% check_wd('non_existing.txt'), 14 | regexp = "The file `non_existing.txt` does not appear to be", 15 | class = "sct_failure") 16 | expect_error(test_file_exists('non_existing.txt'), 17 | regexp = "The file `non_existing.txt` does not appear to be", 18 | class = "sct_failure") 19 | 20 | expect_error(ex() %>% check_wd('non_existing.txt', missing_msg = 'incorrect'), 21 | regexp = "Incorrect", 22 | class = "sct_failure") 23 | expect_error(test_file_exists('non_existing.txt', incorrect_msg = 'incorrect'), 24 | regexp = "Incorrect", 25 | class = "sct_failure") 26 | }) 27 | 28 | test_that("check_wd - 3", { 29 | expect_error(ex() %>% check_wd('test/non_existing.txt'), 30 | regexp = "The file `non_existing.txt` does not appear to be inside the folder `test` in your working directory", 31 | class = "sct_failure") 32 | expect_error(test_file_exists('test/non_existing.txt'), 33 | regexp = "The file `non_existing.txt` does not appear to be inside the folder `test` in your working directory", 34 | class = "sct_failure") 35 | 36 | expect_error(ex() %>% check_wd('test/non_existing.txt', missing_msg = "incorrect"), 37 | regexp = "Incorrect", 38 | class = "sct_failure") 39 | expect_error(test_file_exists('test/non_existing.txt', incorrect_msg = "incorrect"), 40 | regexp = "Incorrect", 41 | class = "sct_failure") 42 | 43 | }) -------------------------------------------------------------------------------- /tests/testthat/test-content-examples.R: -------------------------------------------------------------------------------- 1 | context("content_examples") 2 | 3 | test_that("exercise intermediate r (1)", { 4 | lst <- list() 5 | lst$DC_CODE <- "today <- Sys.Date()\nday1 <- today - 11" 6 | lst$DC_SOLUTION <- "today <- Sys.Date()\nday1 <- today - 11" 7 | lst$DC_SCT <- 'test_object("day1")' 8 | output <- test_it(lst) 9 | passes(output) 10 | }) 11 | 12 | test_that("exercise intermediate r (2)", { 13 | lst <- list() 14 | lst$DC_PEC <- "linkedin <- c(16, 9, 13, 5, 2, 17, 14)" 15 | lst$DC_SOLUTION <- "for (li in linkedin) { print(li) }" 16 | lst$DC_CODE <- "for (li in linkedin) { print(li + 1) }" 17 | lst$DC_SCT <- "test_output_contains('invisible(lapply(linkedin,print))')" 18 | output <- test_it(lst) 19 | fails(output) 20 | 21 | lst$DC_CODE <- lst$DC_SOLUTION 22 | output <- test_it(lst) 23 | passes(output) 24 | }) 25 | 26 | test_that("exercise ggplot2 - v1", { 27 | lst <- list() 28 | lst$DC_PEC <- "library(ggplot2)" 29 | lst$DC_SCT <- " 30 | test_function_v2('qplot', 'data', index = 1) 31 | test_function_v2('qplot', 'x', eval = FALSE, index = 1) 32 | test_function_v2('qplot', 'data', index = 2) 33 | test_function_v2('qplot', 'x', eval = FALSE, index = 2) 34 | test_function_v2('qplot', 'y', eval = FALSE, index = 2) 35 | test_function_v2('qplot', 'data', index = 3) 36 | test_function_v2('qplot', 'x', eval = FALSE, index = 3) 37 | test_function_v2('qplot', 'y', eval = FALSE, index = 3) 38 | test_function_v2('qplot', 'geom', eval = FALSE, index = 3) 39 | test_error() 40 | success_msg('Good job!') 41 | " 42 | lst$DC_SOLUTION <- " 43 | qplot(factor(cyl), data = mtcars) 44 | qplot(factor(cyl), factor(vs), data = mtcars) 45 | qplot(factor(cyl), factor(vs), data = mtcars, geom = 'jitter') 46 | " 47 | lst$DC_CODE <- lst$DC_SOLUTION 48 | output <- test_it(lst) 49 | passes(output) 50 | }) 51 | 52 | test_that("exercise ggplot2 - v2", { 53 | lst <- list() 54 | lst$DC_PEC <- "library(ggplot2)" 55 | lst$DC_SCT <- " 56 | test_function_v2('qplot', args = c('data', 'x'), eval = c(T, F), index = 1) 57 | test_function_v2('qplot', c('data', 'x', 'y'), eval = c(T, F, F), index = 2) 58 | test_function_v2('qplot', c('data', 'x', 'y','geom'), eval = c(T, F, F, F), index = 3) 59 | test_error() 60 | " 61 | lst$DC_SOLUTION <- " 62 | qplot(factor(cyl), data = mtcars) 63 | qplot(factor(cyl), factor(vs), data = mtcars) 64 | qplot(factor(cyl), factor(vs), data = mtcars, geom = 'jitter') 65 | " 66 | lst$DC_CODE <- lst$DC_SOLUTION 67 | output <- test_it(lst) 68 | passes(output) 69 | }) 70 | 71 | # # NOT FIXED! 72 | # test_that("exercise cleaning data", { 73 | # lst <- list() 74 | # lst$DC_PEC <- "states <- c('a', 'b', 'c', 'd')" 75 | # lst$DC_CODE <- "states\nstates_upper <- toupper(states)\ntolower <- tolower(states_upper)" 76 | # lst$DC_SOLUTION <- "states\nstates_upper <- toupper(states)\ntolower(states_upper)" 77 | # lst$DC_SCT <- "test_function('tolower', 'x')" 78 | # output <- test_it(lst) 79 | # passes(output) 80 | # }) -------------------------------------------------------------------------------- /tests/testthat/test-highlighting.R: -------------------------------------------------------------------------------- 1 | context("highlighting") 2 | 3 | test_that("disable_highlighting() works", { 4 | 5 | lst <- list(DC_CODE = "if (3 > 2) round(1)", 6 | DC_SOLUTION = "if (3 > 2) round(2)") 7 | 8 | lst$DC_SCT <- "ex() %>% check_if_else() %>% check_if() %>% check_function('round') %>% check_arg('x') %>% check_equal()" 9 | output <- test_it(lst) 10 | fails(output) 11 | line_info(output, 1, 1, 18, 18) 12 | 13 | # fall back on body highlighting 14 | lst$DC_SCT <- "ex() %>% check_if_else() %>% check_if() %>% check_function('round') %>% disable_highlighting() %>% check_arg('x') %>% check_equal()" 15 | output <- test_it(lst) 16 | fails(output) 17 | line_info(output, 1, 1, 12, 19) 18 | 19 | # disableing is preserved throughout chain 20 | lst$DC_SCT <- "ex() %>% disable_highlighting() %>% check_if_else() %>% check_if() %>% check_function('round') %>% check_arg('x') %>% check_equal()" 21 | output <- test_it(lst) 22 | fails(output) 23 | no_line_info(output) 24 | }) 25 | -------------------------------------------------------------------------------- /tests/testthat/test-message-utils.R: -------------------------------------------------------------------------------- 1 | context("message-utils") 2 | 3 | test_that("yaml_option_desc", { 4 | expect_equal(yaml_option_desc("a"), "`a`") 5 | expect_equal(yaml_option_desc(c("a", "b")), "`a:b`") 6 | expect_equal(yaml_option_desc(c("a", "b", "c")), "`a:b:c`") 7 | }) -------------------------------------------------------------------------------- /tests/testthat/test-messages.R: -------------------------------------------------------------------------------- 1 | context("messages") 2 | 3 | test_that("trim works as expected", { 4 | x <- "hello. this? is! a. test?! ok? fine." 5 | res <- capitalize(x) 6 | expected <- "Hello. This? Is! A. Test?! Ok? Fine." 7 | expect_equal(res, expected) 8 | }) 9 | 10 | test_that("language is deprecated", { 11 | expect_message(set_language("en"), "Different languages are no longer supported in testwhat") 12 | }) -------------------------------------------------------------------------------- /tests/testthat/test-rstudio-test-pipe.R: -------------------------------------------------------------------------------- 1 | context("test_pipe") 2 | 3 | test_that("test_pipe works", { 4 | lst <- list() 5 | lst$DC_CODE <- "mean(abs(-5:6))" 6 | lst$DC_SCT <- "test_pipe(1)" 7 | output <- test_it(lst) 8 | fails(output) 9 | lst$DC_SCT <- "test_pipe(1, insuf_msg = 'missing')" 10 | output <- test_it(lst) 11 | fails(output, mess_patt = "Missing") 12 | 13 | lst$DC_CODE <- "-5:6 %>% abs() %>% mean()" 14 | lst$DC_SCT <- "test_pipe(1)" 15 | output <- test_it(lst) 16 | passes(output) 17 | lst$DC_SCT <- "test_pipe(2)" 18 | output <- test_it(lst) 19 | passes(output) 20 | lst$DC_SCT <- "test_pipe(3)" 21 | output <- test_it(lst) 22 | fails(output) 23 | lst$DC_SCT <- "test_pipe(3, insuf_msg = 'not enough man!')" 24 | output <- test_it(lst) 25 | fails(output, mess_patt = "Not enough man!") 26 | }) -------------------------------------------------------------------------------- /tests/testthat/test-rstudio-test-rmd-group.R: -------------------------------------------------------------------------------- 1 | context("test_rmd_group") 2 | 3 | test_that("test_rmd_group works", { 4 | lst <- list() 5 | lst$DC_TYPE <- "MarkdownExercise" 6 | 7 | lst$DC_CODE <- c(my_doc.Rmd = "\n ---\n title: \"Testing\"\n author: \"Tester\"\n date: \"January 1, 2015\"\n output: html_document\n ---\n \n This\n is\n a\n test\n \n ```{r}\n dim(cars)\n ```\n \n You can also embed plots, for example:\n \n ```{r, echo=FALSE}\n plot(cars)\n ```\n \n Wowww.") 8 | lst$DC_SOLUTION <- c(my_sol.Rmd = "\n ---\n title: \"Testing\"\n author: \"Tester\"\n date: \"January 1, 2015\"\n output: html_document\n ---\n \n This\n is\n a\n test\n but\n doesn't \n matter\n \n ```{r}\n dim(cars)\n ```\n \n You can also embed plots, for falafel:\n \n ```{r, echo=FALSE}\n str(cars)\n ```\n \n Wowww.") 9 | lst$DC_SCT <- "test_rmd_group(1, NULL)" 10 | output <- test_it(lst) 11 | passes(output) 12 | 13 | lst$DC_CODE <- c(my_doc.Rmd = "\n ---\n title: \"Testing\"\n author: \"Tester\"\n date: \"January 1, 2015\"\n output: html_document\n ---\n \n This\n is\n a\n test\n \n ```{r, echo = TRUE}\n dim(cars)\n ```\n \n You can also embed plots, for example:\n \n ```{r, message = FALSE}\n plot(cars)\n ```\n \n Wowww.") 14 | lst$DC_SOLUTION <- c(my_sol.Rmd = "\n ---\n title: \"Testing\"\n author: \"Tester\"\n date: \"January 1, 2015\"\n output: html_document\n ---\n \n This\n is\n a\n test\n but\n doesn't \n matter\n \n ```{r, echo = TRUE}\n dim(cars)\n ```\n \n You can also embed plots, for falafel:\n \n ```{r, message = FALSE}\n str(cars)\n ```") 15 | lst$DC_SCT <- "test_rmd_group(1, NULL)" 16 | output <- test_it(lst) 17 | fails(output, mess_patt = "Make sure the structure of your document is OK.") 18 | 19 | lst$DC_CODE <- c(my_doc.Rmd = "\n ---\n title: \"Testing\"\n author: \"Tester\"\n date: \"January 1, 2015\"\n output: html_document\n ---\n \n This\n is\n a\n test\n \n ```{r, echo = TRUE}\n dim(cars)\n ```\n \n You can also embed plots, for example:\n \n ```{r, message = FALSE}\n plot(cars)\n ```\n \n Wowww.") 20 | lst$DC_SOLUTION <- c(my_sol.Rmd = "\n ---\n title: \"Testing\"\n author: \"Tester\"\n date: \"January 1, 2015\"\n output: html_document\n ---\n \n This\n is\n a\n test\n but\n doesn't \n matter\n \n ```{r, echo = TRUE}\n dim(cars)\n ```\n \n You can also embed plots, for falafel:\n \n ```{r, message = FALSE}\n str(cars)\n ```\n \n ```{r}\n mean(cars)\n ```") 21 | lst$DC_SCT <- "test_rmd_group(1, NULL)" 22 | output <- test_it(lst) 23 | fails(output, mess_patt = "Make sure you have the correct amount of inline") 24 | 25 | lst$DC_CODE <- c(my_doc.Rmd = "\n ---\n title: \"Testing\"\n author: \"Tester\"\n date: \"January 1, 2015\"\n output: html_document\n ---\n \n This\n is\n a\n test\n \n ```{r, echo = TRUE}\n dim(cars)\n ```\n \n You can also embed plots, for example:\n \n ```{r, message = FALSE}\n plot(cars)\n ```\n \n Wowww.\n\n ```{r}\n mean(cars)\n ```") 26 | lst$DC_SOLUTION <- c(my_sol.Rmd = "\n ---\n title: \"Testing\"\n author: \"Tester\"\n date: \"January 1, 2015\"\n output: html_document\n ---\n \n This\n is\n a\n test\n but\n doesn't \n matter\n \n ```{r, echo = TRUE}\n dim(cars)\n ```\n \n You can also embed plots, for falafel:\n \n ```{r, message = FALSE}\n str(cars)\n ```\n \n ```{r}\n mean(cars)\n ```\n \n Woowwww.") 27 | lst$DC_SCT <- "test_rmd_group(1, NULL)" 28 | output <- test_it(lst) 29 | fails(output, mess_patt = "Make sure the overall code structure of your document is OK.") 30 | }) -------------------------------------------------------------------------------- /tests/testthat/test-rstudio-test-text.R: -------------------------------------------------------------------------------- 1 | context("test_text") 2 | 3 | test_that("test_text works", { 4 | lst <- list() 5 | lst$DC_TYPE <- "MarkdownExercise" 6 | lst$DC_CODE <- c(my_doc.Rmd = " 7 | --- 8 | title: \"Testing\" 9 | author: \"Tester\" 10 | date: \"January 1, 2015\" 11 | output: html_document 12 | --- 13 | 14 | This 15 | is 16 | a 17 | test 18 | 19 | testing **good** 20 | 21 | ```{r} 22 | dim(cars) 23 | ``` 24 | 25 | You can also embed plots, for example: 26 | 27 | ```{r, echo=FALSE} 28 | plot(cars) 29 | ``` 30 | 31 | Wowww.") 32 | 33 | lst$DC_SOLUTION <- c(my_sol.Rmd = " 34 | --- 35 | title: \"Testing\" 36 | author: \"Tester\" 37 | date: \"January 1, 2015\" 38 | output: html_document 39 | --- 40 | 41 | This 42 | is 43 | a 44 | test 45 | 46 | testing **good** 47 | 48 | ```{r} 49 | dim(cars) 50 | ``` 51 | 52 | You can also embed plots, for example: 53 | 54 | ```{r, echo=FALSE} 55 | plot(cars) 56 | ``` 57 | 58 | Wowww.") 59 | 60 | lst$DC_SCT <- "test_rmd_group(1, test_text('test'))" 61 | output <- test_it(lst) 62 | passes(output) 63 | 64 | lst$DC_SCT <- "test_rmd_group(1, test_text('test', freq = 2))" 65 | output <- test_it(lst) 66 | passes(output) 67 | 68 | lst$DC_SCT <- "test_rmd_group(1, test_text('good', format = 'bold'))" 69 | output <- test_it(lst) 70 | passes(output) 71 | 72 | lst$DC_SCT <- "test_rmd_group(1, test_text('rest', not_called_msg = 'not calllled'))" 73 | output <- test_it(lst) 74 | fails(output, mess_patt = "Not calllled") 75 | 76 | lst$DC_SCT <- "test_rmd_group(1, test_text('good', freq = 2, not_called_msg = 'not called twice'))" 77 | output <- test_it(lst) 78 | fails(output, mess_patt = "Not called twice") 79 | 80 | lst$DC_SCT <- "test_rmd_group(1, test_text('good', format = 'parentheses', incorrect_msg = 'that is not correct'))" 81 | output <- test_it(lst) 82 | fails(output, mess_patt = "That is not correct") 83 | }) 84 | 85 | -------------------------------------------------------------------------------- /tests/testthat/test-state.R: -------------------------------------------------------------------------------- 1 | context("override_solution") 2 | 3 | test_that("override_solution", { 4 | s <- setup_state('x <- 2', 'x <- 2') 5 | expect_equal(get("x", envir = s$get("student_env")), 2) 6 | expect_equal(get("x", envir = s$get("solution_env")), 2) 7 | s2 <- override_solution(s, x = 3) 8 | expect_equal(get("x", envir = s2$get("student_env")), 2) 9 | expect_equal(get("x", envir = s2$get("solution_env")), 3) 10 | s3 <- override_solution_env(s, x = 3) 11 | expect_equal(get("x", envir = s3$get("student_env")), 2) 12 | expect_equal(get("x", envir = s3$get("solution_env")), 3) 13 | 14 | code <- 'mean(1:3)' 15 | s <- setup_state(code, code) 16 | expect_equal(s$get("student_code"), code) 17 | expect_equal(s$get("solution_code"), code) 18 | s2 <- override_solution(s, code = 'sum(1:3)') 19 | expect_equal(s2$get("student_code"), code) 20 | expect_equal(s2$get("solution_code"), 'sum(1:3)') 21 | s3 <- override_solution_code(s, code = 'sum(1:3)') 22 | expect_equal(s3$get("student_code"), code) 23 | expect_equal(s3$get("solution_code"), 'sum(1:3)') 24 | 25 | expect_error(override_solution(s, code = 'sum(1:3')) 26 | }) 27 | 28 | -------------------------------------------------------------------------------- /tests/testthat/test-success-msg.R: -------------------------------------------------------------------------------- 1 | context("success_msg") 2 | 3 | test_that("success_msg", { 4 | lst <- list(DC_SCT = "success_msg('this is correct')") 5 | output <- test_it(lst) 6 | passes(output, mess_patt = "This is correct") 7 | lst <- list(DC_SCT = "success_msg('this is correct', praise = TRUE)") 8 | output <- test_it(lst) 9 | passes(output, mess_patt = "You are") # verify random praise 10 | passes(output, mess_patt = "This is correct") 11 | }) 12 | 13 | -------------------------------------------------------------------------------- /tests/testthat/test-test-an-object.R: -------------------------------------------------------------------------------- 1 | context("test_an_object") 2 | 3 | test_that("test_an_object", { 4 | lst <- list() 5 | lst$DC_SOLUTION <- "a <- 2" 6 | lst$DC_SCT <- "test_an_object('a')" 7 | 8 | lst$DC_CODE <- "a <- 2" 9 | output <- test_it(lst) 10 | passes(output) 11 | 12 | lst$DC_CODE <- "b <- 2" 13 | output <- test_it(lst) 14 | passes(output) 15 | 16 | lst$DC_CODE <- "a <- 3" 17 | output <- test_it(lst) 18 | fails(output) 19 | 20 | lst$DC_CODE <- "b <- 3" 21 | output <- test_it(lst) 22 | fails(output) 23 | }) 24 | -------------------------------------------------------------------------------- /tests/testthat/test-test-exercise.R: -------------------------------------------------------------------------------- 1 | context("test_exercise") 2 | 3 | test_that("test_exercise works properly", { 4 | 5 | run_test_exercise <- function(stu_code, stu_env, sct = '', output_list = list(), allow_errors=FALSE) { 6 | test_exercise( 7 | sct = sct, 8 | ex_type = "NormalExercise", 9 | pec = "", 10 | student_code = stu_code, 11 | solution_code = "x <- 4", 12 | student_env = list2env(stu_env, parent = globalenv()), 13 | solution_env = list2env(list(x = 4), parent = globalenv()), 14 | output_list = output_list, 15 | allow_errors = allow_errors, 16 | seed = 42 17 | ) 18 | } 19 | 20 | # sct fails 21 | res <- run_test_exercise(stu_code = "x <- 5", 22 | stu_env = list(x = 5), 23 | sct = "ex() %>% check_object('x') %>% check_equal()") 24 | expect_false(res$correct) 25 | 26 | # sct fails with custom message 27 | res <- run_test_exercise(stu_code = "x <- 5", 28 | stu_env = list(x = 5), 29 | sct = "ex() %>% check_object('x') %>% check_equal(incorrect_msg='abc')") 30 | expect_false(res$correct) 31 | expect_true(grepl('Abc', res$message)) 32 | 33 | # sct fails with custom message outside of function 34 | res <- run_test_exercise(stu_code = "x <- 5", 35 | stu_env = list(x = 5), 36 | sct = "msg <- 'abc'\nex() %>% check_object('x') %>% check_equal(incorrect_msg = msg)") 37 | expect_false(res$correct) 38 | expect_true(grepl('Abc', res$message)) 39 | 40 | # sct passes, but contains an error 41 | res <- run_test_exercise(stu_code = "x <- y", 42 | stu_env = list(), 43 | sct = '', 44 | output_list = list(list(type = 'r-error', payload = 'undefined variable'))) 45 | expect_false(res$correct) 46 | expect_true(grepl('Your code contains an error that you should fix', res$message)) 47 | 48 | # sct passes, contains an error, but errors are allowed: should be fine 49 | res <- run_test_exercise(stu_code = "x <- y", 50 | stu_env = list(), 51 | sct = '', 52 | output_list = list(list(type = 'r-error', payload = 'undefined variable')), 53 | allow_errors = TRUE) 54 | expect_true(res$correct) 55 | 56 | # sct passes 57 | res <- run_test_exercise(stu_code = "x <- 4", 58 | stu_env = list(x = 4), 59 | sct = "ex() %>% check_object('x') %>% check_equal()") 60 | expect_true(res$correct) 61 | }) 62 | 63 | -------------------------------------------------------------------------------- /tests/testthat/test-test-predefined-objects.R: -------------------------------------------------------------------------------- 1 | context("test-predefined-objects") 2 | 3 | test_that("test_predefined_objects works", { 4 | lst <- list() 5 | lst$DC_SOLUTION <- "a <- 2\nb <- 3\nc <- c(x = 2)\nd <- 4" 6 | lst$DC_CODE <- "a <- 2\nb <- 3\nc <- 2" 7 | 8 | lst$DC_SCT <- "test_predefined_objects()" 9 | expect_error(test_it(lst)) 10 | 11 | lst$DC_SCT <- "test_predefined_objects(c('a', 'b', 'c'))" 12 | output <- test_it(lst) 13 | passes(output) 14 | 15 | lst$DC_SCT <- "test_predefined_objects(c('a', 'b', 'c'), eq_condition = 'equivalent')" 16 | output <- test_it(lst) 17 | passes(output) 18 | 19 | lst$DC_SCT <- "test_predefined_objects(c('a', 'b', 'c'), eq_condition = c('equivalent', 'equivalent'))" 20 | output <- test_it(lst) 21 | passes(output) 22 | 23 | lst$DC_SCT <- "test_predefined_objects(c('a', 'b', 'c'), eq_condition = c('equivalent', 'equivalent', 'equal'))" 24 | output <- test_it(lst) 25 | fails(output, "change the contents") 26 | 27 | lst$DC_SCT <- "test_predefined_objects(c('a', 'b', 'c', 'd'))" 28 | output <- test_it(lst) 29 | fails(output, "remove the predefined") 30 | 31 | lst$DC_SCT <- "test_predefined_objects(c('a', 'b', 'c'), eq_condition = c('equivalent', 'equivalent', 'equal'), eval = c(T, T, F))" 32 | output <- test_it(lst) 33 | passes(output) 34 | 35 | lst$DC_SCT <- "test_predefined_objects(c('a', 'b', 'c'), eq_condition = 'equal', incorrect_msg = 'notok')" 36 | output <- test_it(lst) 37 | fails(output, 'Notok') 38 | 39 | lst$DC_SCT <- "test_predefined_objects(c('a', 'b', 'c'), eq_condition = 'equal', incorrect_msg = c('anotok', 'bnotok', 'cnotok'))" 40 | output <- test_it(lst) 41 | fails(output, 'Cnotok') 42 | 43 | lst$DC_SCT <- "test_predefined_objects(c('a', 'b', 'c', 'd'), undefined_msg = 'notok')" 44 | output <- test_it(lst) 45 | fails(output, 'Notok') 46 | 47 | lst$DC_SCT <- "test_predefined_objects(c('a', 'b', 'c', 'd'), undefined_msg = c('anotok', 'bnotok', 'cnotok', 'dnotok'))" 48 | output <- test_it(lst) 49 | fails(output, 'Dnotok') 50 | 51 | }) -------------------------------------------------------------------------------- /tests/testthat/test-utils-state.R: -------------------------------------------------------------------------------- 1 | context("utils-state") 2 | 3 | test_that("setup_state works", { 4 | code <- "x <- 5\nx" 5 | s <- setup_state(stu_code = code, sol_code = code, pec = "y <- 6") 6 | expect_equal(get("x", s$get("student_env")), 5) 7 | expect_equal(get("x", s$get("solution_env")), 5) 8 | expect_equal(get("y", s$get("student_env")), 6) 9 | expect_equal(get("y", s$get("solution_env")), 6) 10 | expect_equal("[1] 5", s$get("output_list")[[length(s$get("output_list"))]]$payload) 11 | 12 | s <- setup_state("", "", stu_result = evaluate::evaluate("7")) 13 | expect_equal("[1] 7", s$get("output_list")[[length(s$get("output_list"))]]$payload) 14 | 15 | s <- setup_state(stu_code = code) 16 | expect_equal(get("x", s$get("student_env")), 5) 17 | expect_false("y" %in% ls(s$get("student_env"))) 18 | expect_false("x" %in% ls(s$get("solution_env"))) 19 | expect_false("y" %in% ls(s$get("solution_env"))) 20 | expect_equal("[1] 5", s$get("output_list")[[length(s$get("output_list"))]]$payload) 21 | 22 | s <- setup_state(sol_code = code) 23 | expect_equal(get("x", s$get("solution_env")), 5) 24 | expect_false("y" %in% ls(s$get("solution_env"))) 25 | expect_false("x" %in% ls(s$get("student_env"))) 26 | expect_false("y" %in% ls(s$get("student_env"))) 27 | 28 | s <- setup_state() 29 | expect_equal(length(s$get("solution_env")), 0) 30 | expect_equal(length(s$get("student_env")), 0) 31 | 32 | erroneous_code <- "non_existing_var" 33 | expect_error(setup_state(sol_code = erroneous_code)) 34 | s <- setup_state(stu_code = erroneous_code, sol_code = code) 35 | expect_true("r-error" %in% sapply(s$get("output_list"), `[[`, "type")) 36 | expect_equal("Error: object 'non_existing_var' not found", s$get("output_list")[[length(s$get("output_list"))]]$payload) 37 | }) 38 | -------------------------------------------------------------------------------- /tests/testthat/test-utils.R: -------------------------------------------------------------------------------- 1 | context("utils") 2 | 3 | test_that("accessor works", { 4 | tw <- tw_accessors() 5 | tw$initialize(data = list(a = 2, b = 3, c = 4)) 6 | expect_equal(tw$get("a"), 2) 7 | expect_equal(tw$get("b"), 3) 8 | expect_equal(tw$get("c"), 4) 9 | expect_equal(tw$get("d"), NULL) 10 | tw$set(d = 5) 11 | expect_equal(tw$get("a"), 2) 12 | expect_equal(tw$get("b"), 3) 13 | expect_equal(tw$get("c"), 4) 14 | expect_equal(tw$get("d"), 5) 15 | tw$clear() 16 | expect_equal(tw$get(), list()) 17 | expect_equal(tw$get("a"), NULL) 18 | }) 19 | 20 | test_that("get_num_hits works", { 21 | expect_equal(get_num_hits('a', 'abc', TRUE), 1) 22 | expect_equal(get_num_hits('a', 'aba', TRUE), 2) 23 | }) 24 | 25 | test_that("get_num_hits uses Perl regex", { 26 | expect_equal(get_num_hits('^[^b]+(?!b)', 'abc', FALSE), 0) 27 | expect_equal(get_num_hits('^[^b]+(?!b)', 'ac', FALSE), 1) 28 | multiline_string <- 'a 29 | a' 30 | expect_equal(get_num_hits('a.*', multiline_string, FALSE), 1) 31 | expect_equal(get_num_hits('(?-s)a.*', multiline_string, FALSE), 2) 32 | }) 33 | 34 | test_that("check_defined works", { 35 | testenv <- new.env() 36 | assign("x", 5, envir = testenv) 37 | expect_silent(check_defined("x", testenv)) 38 | expect_error(check_defined("y", testenv)) 39 | }) 40 | 41 | test_that("remove_comments works", { 42 | cases <- c("# c\n4", "#c\n4", "4\n#c", "4\n\n#c", "#c\n4\n# c\n") 43 | for (case in cases) { 44 | no_comments_stripped = stringr::str_trim(remove_comments(case)) 45 | expect_equal(no_comments_stripped, "4") 46 | } 47 | cases2 <- c("#", "#\n") 48 | for (case in cases2) { 49 | expect_equal(remove_comments("#"), "") 50 | } 51 | }) 52 | 53 | test_that("fail_if_v2_only", { 54 | withr::with_envvar(c(TESTWHAT_V2_ONLY = ''), { 55 | expect_equal(fail_if_v2_only(), NULL) 56 | }) 57 | withr::with_envvar(c(TESTWHAT_V2_ONLY = '0'), { 58 | expect_equal(fail_if_v2_only(), NULL) 59 | }) 60 | withr::with_envvar(c(TESTWHAT_V2_ONLY = '1'), { 61 | expect_error(fail_if_v2_only()) 62 | }) 63 | withr::with_envvar(c(TESTWHAT_V2_ONLY = '1'), { 64 | expect_error(fail_if_v2_only('test'), regexp = 'test') 65 | }) 66 | }) 67 | 68 | test_that("unpipe works with a single pipe", { 69 | expected <- quote(select(cars, speed)) 70 | actual <- unpipe(quote(cars %>% select(speed))) 71 | expect_identical(actual, expected) 72 | }) 73 | 74 | test_that("unpipe works with a single pipe and dot", { 75 | expected <- quote(select(cars, speed)) 76 | actual <- unpipe(quote(cars %>% select(., speed))) 77 | expect_identical(actual, expected) 78 | }) 79 | 80 | test_that("unpipe works with two pipes", { 81 | expected <- quote(filter(select(cars, speed), speed > 20)) 82 | actual <- unpipe(quote(cars %>% select(speed) %>% filter(speed > 20))) 83 | expect_identical(actual, expected) 84 | }) 85 | 86 | test_that("unpipe works with two pipes and dot", { 87 | expected <- quote(filter(select(cars, speed), speed > 20)) 88 | actual <- unpipe(quote(cars %>% select(., speed) %>% filter(., speed > 20))) 89 | expect_identical(actual, expected) 90 | }) 91 | 92 | test_that("unpipe works with qualifed function names", { 93 | expected <- quote(dplyr::filter(dplyr::select(cars, speed), speed > 20)) 94 | actual <- unpipe(quote(cars %>% dplyr::select(speed) %>% dplyr::filter(speed > 20))) 95 | expect_identical(actual, expected) 96 | }) 97 | 98 | test_that("unpipe works with multiple dots", { 99 | expected <- quote(setNames(letters, letters)) 100 | actual <- unpipe(quote(letters %>% setNames(., .))) 101 | expect_identical(actual, expected) 102 | }) 103 | -------------------------------------------------------------------------------- /testwhat.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: No 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | BuildType: Package 16 | PackageUseDevtools: Yes 17 | PackageInstallArgs: --no-multiarch --with-keep.source 18 | PackageRoxygenize: rd,collate,namespace,vignette 19 | -------------------------------------------------------------------------------- /vignettes/checking-control-flow.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Checking control flow" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Checking control flow} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r setup, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | eval = FALSE 15 | ) 16 | ``` 17 | 18 | You can use `check_for()`, `check_while()` and `check_if_else()` chains to check whether a student has properly coded control constructs. 19 | 20 | ## Example 1: For loop 21 | 22 | Consider the following solution: 23 | 24 | ```{r} 25 | # Print out the integers 1 to 10 26 | for (i in 1:10) { 27 | print(i) 28 | } 29 | ``` 30 | 31 | A suitable SCT for this exercise can be the following: 32 | 33 | ```{r} 34 | ex() %>% check_for() %>% { 35 | check_cond(.) %>% check_code("in\\s*1:10", missing_msg = "You can use `i in 1:10` to define your for loop.") 36 | check_body(.) %>% check_function("print") %>% check_arg("x") %>% check_equal(eval = FALSE) 37 | } 38 | ``` 39 | 40 | - `check_for()` checks whether students have specified a `for` loop in their code. If this is the case, it produces a substate, referring to the condition of the for loop and its body. 41 | - Next, you can 'zoom in' on the condition and body of the `for` loop, using `check_cond()` and `check_body()` respectively. After this zooming in, you can continue the chain of SCT functions to check these 'parts' of the code. The `check_code()` call, for example, only looks to match the regular expression in the iteration part of the `for` loop. 42 | 43 | ## Example 2: Check if else 44 | 45 | Consider the following solution: 46 | 47 | ```{r} 48 | # Predefined value of x 49 | x <- TRUE 50 | 51 | # Code the if else construct 52 | if (x) { 53 | print("x is TRUE!") 54 | } else { 55 | print("x is not TRUE!") 56 | } 57 | ``` 58 | 59 | The following SCT checks its correctness: 60 | 61 | ```{r} 62 | ex() %>% check_object("x") 63 | ex() %>% check_if_else() %>% { 64 | check_cond(.) %>% check_code("x") 65 | check_if(.) %>% check_function("print") %>% check_arg("x") %>% check_equal() 66 | check_else(.) %>% check_function("print") %>% check_arg("x") %>% check_equal() 67 | } 68 | ``` 69 | 70 | - `check_if_else()` parses the student code and checks if there is an if-else statement in there. 71 | - `check_cond`, `check_if`, and `check_else()` all 'zoom in' on a particular part of the if-else statement: the condition, the body of the `if` statement and the body of the `else` statement. After this zooming in, you can continue the chain of SCT functions to check these 'parts' of the code. The first `check_function("print")` chain for example, only looks for the function call inside the `if` body. 72 | 73 | ### Example 3: Check if, else if, else 74 | 75 | It is also possible to use `else if` in R. How to test this? Well, behind the scenes, R parses the following structure: 76 | 77 | ```{r} 78 | if (condition1) { 79 | expression 80 | } else if (condition2) { 81 | expression 82 | } else { 83 | expression 84 | } 85 | ``` 86 | 87 | as if it follows this structure: 88 | 89 | ```{r} 90 | if (condition1) { 91 | expression 92 | } else { 93 | if (condition2) { 94 | 95 | } else { 96 | 97 | } 98 | } 99 | ``` 100 | 101 | If you want to test such a piece of code, you therefore need the following construct: 102 | 103 | ```{r} 104 | ex() %>% check_if_else() %>% { 105 | check_cond(.) %>% ... 106 | check_if(.) %>% ... 107 | check_else(.) %>% check_if_else() %>% { 108 | check_cond(.) %>% ... 109 | check_if(.) %>% ... 110 | check_else(.) %>% ... 111 | } 112 | } 113 | ``` 114 | 115 | ## Caution 116 | 117 | Not all of the SCT chains that you specify after a 'zooming in' step might work as you'd expect. Remember that functions like `check_object()` and `check_function()` often depend on both the student and solution environment. It is possible that during execution of control flow these values change. The `testwhat` functions will always compare the 'end environment' of student and solution, it is not possible to do matching on intermediate values of objects that are changed further in the script. 118 | 119 | -------------------------------------------------------------------------------- /vignettes/checking-function-definitions.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Checking function definitions" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Checking function definitions} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r setup, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | eval = FALSE 15 | ) 16 | ``` 17 | 18 | Some exercises require a student to hand-craft a function to make their code DRY. With `check_fun_def()` and a range of related helper `check_` functions you can check the correctness of these user-defined functions. 19 | 20 | As an example, have a look at the following solution: 21 | 22 | ```{r} 23 | # Define my_fun 24 | my_fun <- function(a, b) { 25 | abs(a) + abs(b) 26 | } 27 | ``` 28 | 29 | The following SCT checks the function definition and whether the function was properly called: 30 | 31 | ```{r} 32 | ex() %>% check_fun_def("my_fun") %>% { 33 | check_arguments(.) 34 | check_call(., 1, 2) %>% check_result() %>% check_equal() 35 | check_call(., -1, 2) %>% check_result() %>% check_equal() 36 | check_call(., 1, -2) %>% check_result() %>% check_equal() 37 | check_call(., -1, -2) %>% check_result() %>% check_equal() 38 | check_body(.) %>% { 39 | check_function(., "abs", index = 1) 40 | check_function(., "abs", index = 2) 41 | } 42 | } 43 | ``` 44 | 45 | - `check_fun_def` checks whether `my_fun` was defined by the student. If this is the case, it will produce a substate that contains a reference to the function arguments and its body. 46 | - `check_arguments()` checks whether the student specified the same number of arguments as the function definition in the solution did. 47 | - `check_call()` will create an R expression that calls the function `my_fun()` with the arguments specified in it. As an example, `check_call(., 1, 2)` will generate the expression `my_fun(1, 2)`. `check_result()` will then execute this expression (in both student and solution workspace), and focus on the result of the expression. Finally, `check_equal()` verifies whether these call results correspond. 48 | - `check_body()` zooms in on the body of the function definition (as if the body of the function was the only code in the student submission). If, for example, the student used `abs()` outside the function definition, this would be ignored. Notice here that no arguments are specified. If we did this, `check_function()` would compare the actual values of the arguments. This is information that is not available, as `a` and `b` are not variables that are available in the workspace, only in the scope of the function. 49 | 50 | **Note:** in addition to `check_result()`, you can also pipe the result of `check_call()` into `check_output()` and `check_error()`, and follow up with `check_equal()`. These functions will respectively check the output and error the function call generated when executed in the student/solution environment. 51 | -------------------------------------------------------------------------------- /vignettes/checking-markdown-documents.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Checking markdown documents" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Checking markdown documents} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r setup, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | eval = FALSE 15 | ) 16 | ``` 17 | 18 | DataCamp's campus application also features exercises of the type `MarkdownExercise`, where students are asked to make changes to a markdown document. When they submit their document, DataCamp's RBackend will render the markdown doc and verify its correctness with testwhat. Because a markdown document is pretty different from an R script, testwhat features a set of dedicated functions to test elements of a markdown document. This article will give one extended example, and walk through what the different functions do. For more details about each of the functions, check out the function documentation (see Reference section). 19 | 20 | Until the extended example is added and elaborated on, have a look at [the tests in `test-check-rmd.R` of the testwhat repository on GitHub](https://github.com/datacamp/testwhat/blob/master/tests/testthat/test-check-rmd.R). At the end, you will find a full example of an SCT chain that tests an entire Markdown document. The tests should be largely self-explanatory. 21 | -------------------------------------------------------------------------------- /vignettes/checking-output.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Checking output" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Checking output} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r setup, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | eval = FALSE 15 | ) 16 | ``` 17 | 18 | ## Search for pattern in output 19 | 20 | `check_output()` enables you to check for patterns or literal matches in the output the student generated with his or her submission. Where `check_output_expr()` (see next section) _executes_ the `expr` argument you pass it and then tries to match it to the student's output, the `check_output()` function doesn't perform any execution: it simply looks for matches for the `pattern` you specify. 21 | 22 | ### Example 23 | 24 | Suppose you want to check whether the student printed out `My name is `. A solution could look like this: 25 | 26 | ```{r} 27 | # Print out your name 28 | "My name is DataCamp" 29 | ``` 30 | 31 | To allow for different names here, you can use the following SCT: 32 | 33 | ```{r} 34 | ex() %>% check_output("[M|m]y name is [a-zA-Z]+?", missing_msg = "Have you printed out your name?") 35 | ``` 36 | 37 | The regex allows you to allow both `my` and `My`, and to allow any name, as long as its first character is a letter and there's at least one such character. 38 | 39 | [Click here to learn more about regular expressions in R](https://stat.ethz.ch/R-manual/R-devel/library/base/html/regex.html). 40 | 41 | ## Search if student output contains output of expression 42 | 43 | `check_output_expr()` executes the `expr` - an R expression as a string that you specify - and records the output it generates. Next, it tries to match this output to the output that the student generated with his or her submission. With `times` you can specify how often this match should occur, and with `incorrect_msg` you can override the automatically generated message in case the test failed. 44 | 45 | **`check_output_expr()` is used very often, typically to check if the student correctly printed out a variable, whatever its class**. It is also pretty robust to different solutions: how people end up printing the variable doesn't matter, as long as the output contains the correct info it's all good. 46 | 47 | **WATCH OUT:** `check_output_expr()` is only appropriate to test _textual output_, so actual printouts to the console. It will not work to test function calls that do not generate any output, or that generate plot output. Using, for example, `test_output_contains("hist(mtcars$mpg)")` to test whether a student correctly created a histogram of the `mpg` column of `mtcars` makes no sense: the `hist()` function does not generate textual output that can be captured with `capture.output()`. 48 | 49 | ### Example 50 | 51 | Suppose you want the student to print out the fourth row of the `mtcars` data frame. The solution would look as follows: 52 | 53 | ```{r} 54 | # Print out the fourth row of mtcars 55 | mtcars[4, ] 56 | ``` 57 | 58 | The following SCT would check this: 59 | 60 | ```{r} 61 | ex() %>% check_output_expr("mtcars[4, ]", missing_msg = "Have you used `[4, ]` to print out the fourth row of `mtcars`?") 62 | ``` 63 | 64 | You simply pass the expression that would generate the correct printout as an expression, and `check_output_expr()` takes care of the rest. You can also use a custom `missing_msg` to give meaningful feedback to the student. 65 | -------------------------------------------------------------------------------- /vignettes/checking-through-string-matching.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Checking through string matching" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Checking through string matching} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r setup, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | eval = FALSE 15 | ) 16 | ``` 17 | 18 | With `check_code()`, you can look through the student's submission to find a match with the string or strings specified in `strings`. With `fixed` you can declare whether or not to use regular expressions. If `fixed = FALSE`, the default, the strings are used as regular expressions for which a match is sought. when `fixed = TRUE`, `check_code()` will consider the strings you pass as actual strings that have to be found exactly in the student's submission. `times` tells how often (one of) the string(s) should be matched. 19 | 20 | Before a match is sought, the student submission as well as the `strings` argument is 'cleaned up': spaces are removed, all `<-` are replaced by `=`, double strings are changed to single strings, etc. That way, you don't have to code all equivalent options. 21 | 22 | **CAUTION**: It is often tempting to use `check_code()` as it's straightforward to use, but **you should avoid using this function**, as it imposes severe restrictions on how a student can solve an exercise. Often, there are many different ways to solve an exercise. Unless you have a very advanced regular expression or list a bunch of options, `check_code()` will not be able to accept all these different approaches. **Always think about better ways to test a student submission before you resort to `check_code()`**. 23 | 24 | ## Example 1: regular expression 25 | 26 | Suppose you want the student to type the sentence "R is Rsome!", but you want to allow for some small mistakes regarding capitalization and the exclamation mark. The solution looks like this: 27 | 28 | ```{r} 29 | # Write the sentence in quotes 30 | "R is Rsome!" 31 | ``` 32 | 33 | The SCT could look as follows: 34 | 35 | ```{r} 36 | ex() %>% check_code("^\"[r|R] is [r|R]some!?\"$", 37 | not_typed_msg = "Have you correctly written the sentence `R is Rsome!`?") 38 | ``` 39 | 40 | All of the following student submission would be accepted by this `testwhat` function call: 41 | 42 | - `"R is Rsome!"` 43 | - `"R is Rsome"` 44 | - `"r is Rsome!"` 45 | - `"r is Rsome"` 46 | - `"R is rsome!"` 47 | - `"R is rsome"` 48 | - `"r is rsome!"` 49 | - `"r is rsome"` 50 | 51 | [Click here to learn more about regular expressions in R](https://stat.ethz.ch/R-manual/R-devel/library/base/html/regex.html). 52 | 53 | ## Example 2: Exact matching 54 | 55 | Suppose you want to check whether a student coded a SQL expression correctly: 56 | 57 | ```{r} 58 | # Create the correct SQL expression 59 | x <- "SELECT posts FROM tweets WHERE n_char > 10" 60 | ``` 61 | 62 | The student can solve this in many ways, all of which should be accepted: 63 | 64 | - `x <- "select posts from tweets where n_char > 10"` 65 | - `x <- "SELECT posts FROM tweets WHERE n_char > 10"` 66 | - `x <- "SELECT posts from tweets where n_char > 10"` 67 | - ... 68 | 69 | You can thus write the following SCT. In `strings` you specify a vector with all options. This time, you have to set `fixed = TRUE` so you can do exact string matching. Finally, you can chose to override the `not_typed_msg` to give a custom message if the student didn't type what you expected. 70 | 71 | ```{r} 72 | ex() %>% check_code(c("select posts from tweets where n_char > 10", 73 | "SELECT posts FROM tweets WHERE n_char > 10", 74 | "SELECT posts from tweets where n_char > 10"), 75 | fixed = TRUE, 76 | not_typed_msg = "Have you correctly coded the SQL query?") 77 | ``` 78 | 79 | However, the above SCT doesn't really cut it. To be really robust to all different ways of coding this query, you'll either want to use regular expressions inside `test_student_typed()` (see example 2) or do a check on the resulting data frame from executing the SQL query (preferable, since than you automatically cover any correct way of doing things). 80 | -------------------------------------------------------------------------------- /vignettes/extensions.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Extensions" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Extensions} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r setup, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | eval = FALSE 15 | ) 16 | ``` 17 | 18 | There can be cases where the functions `testwhat` features are not sufficient to robustly check a student submission while providing good feedback at the same time. In addition, there might be courses in which you find yourself using the same SCT pattern over and over. 19 | 20 | `teswhat.ext` is a public R package [on GitHub](https://www.github.com/datacamp/testwhat.ext) that can feature both custom functionality as well 'meta SCTs' to employ in multiple exercises. 21 | 22 | To get started with using `teswhat.ext`: 23 | 24 | - Clone the [GitHub repo](https://www.github.com/datacamp/testwhat.ext), create a new branch and add your custom SCT functionality. 25 | - Make a PR and assign somebody from Content Engineering as a reviewer. 26 | - When approved, the PR will be merged in, and a new release will be created for you. 27 | - In the `requirements.r` file of the course in which you want to use your custom functionality, add the following line: 28 | 29 | ```{r} 30 | remotes::install_github("datacamp/testwhat.ext", ref = "v1.2.3") 31 | ``` 32 | 33 | where `"v1.2.3"` points to the new release of `testwhat.ext`. 34 | - In the `sct` block of your exercise, use `library(testwhat.ext)`. You can now use your custom built SCT function as if it was a function exported by `testwhat`: 35 | 36 | ```{r} 37 | library(testwhat.ext) 38 | ex() %>% check_function_my_version("mean") %>% ... 39 | ``` 40 | 41 | A word of warning. Before you embark onto the wild adventure of writing your own SCT functions, be aware that there is **a lot** that students can do wrong with their submission, and it is your job to catch every edge case that can potentially break the code. Therefore, it is advised to combine already existing `testwhat` functions in your extension function, rather than building `check_` functions from scratch. 42 | -------------------------------------------------------------------------------- /vignettes/glossary.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Glossary" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Glossary} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r setup, include = FALSE} 11 | library(knitr) 12 | library(jsonlite) 13 | 14 | knitr::opts_chunk$set( 15 | collapse = TRUE, 16 | comment = "#>", 17 | eval = FALSE 18 | ) 19 | 20 | clean <- function(x) { 21 | lines <- strsplit(x, "\\n")[[1]] 22 | clean1 <- lines[!grepl("^#", lines)] 23 | clean2 <- lines[!grepl("^success_msg", lines)] 24 | return(paste0(clean2, collapse = "\n")) 25 | } 26 | 27 | knit_hooks$set(autogen = function(before, options, envir) { 28 | if (before) { 29 | url <- sprintf("https://www.datacamp.com/api/courses/%i/chapters/%i/exercises.json", options$crs_id, options$chap_id) 30 | ex <- fromJSON(url, simplifyDataFrame = FALSE)[[options$ex_num]] 31 | content <- sprintf("# solution\n%s\n\n# sct\n%s\n", clean(ex$solution), clean(ex$sct)) 32 | knitr::knit_hooks$get("source")(content, options) 33 | } 34 | }) 35 | ``` 36 | 37 | For more context on why this particular SCT is a good idea, you can consult the Guides. None of the examples below specify any custom messages; you can consult the function documentation to figure out how you can override the automatically generated messages. 38 | 39 | ### Check object 40 | 41 | ```{r, eval = TRUE, autogen=TRUE, crs_id=10945, chap_id=34104, ex_num=1} 42 | ``` 43 | 44 | ```{r, eval = TRUE, autogen=TRUE, crs_id=10945, chap_id=34104, ex_num=2} 45 | ``` 46 | 47 | ### Check function call 48 | 49 | ```{r, eval = TRUE, autogen=TRUE, crs_id=10945, chap_id=34104, ex_num=3} 50 | ``` 51 | 52 | ### Check function result 53 | 54 | ```{r, eval = TRUE, autogen=TRUE, crs_id=10945, chap_id=34104, ex_num=4} 55 | ``` 56 | 57 | ### Check object created through function call 58 | 59 | ```{r, eval = TRUE, autogen=TRUE, crs_id=10945, chap_id=34104, ex_num=5} 60 | ``` 61 | 62 | ### Check output 63 | 64 | ```{r, eval = TRUE, autogen=TRUE, crs_id=10945, chap_id=34104, ex_num=6} 65 | ``` 66 | 67 | ```{r, eval = TRUE, autogen=TRUE, crs_id=10945, chap_id=34104, ex_num=7} 68 | ``` 69 | 70 | 71 | ### Check if else 72 | 73 | ```{r, eval = TRUE, autogen=TRUE, crs_id=10945, chap_id=34104, ex_num=8} 74 | ``` 75 | 76 | ### Check for loop 77 | 78 | ```{r, eval = TRUE, autogen=TRUE, crs_id=10945, chap_id=34104, ex_num=9} 79 | ``` 80 | 81 | ### Check function definition 82 | 83 | ```{r, eval = TRUE, autogen=TRUE, crs_id=10945, chap_id=34104, ex_num=10} 84 | ``` 85 | 86 | ### Check `library` or `require` call 87 | 88 | ```{r, eval = TRUE, autogen=TRUE, crs_id=10945, chap_id=34104, ex_num=11} 89 | ``` 90 | 91 | ### Check through string matching 92 | 93 | ```{r, eval = TRUE, autogen=TRUE, crs_id=10945, chap_id=34104, ex_num=12} 94 | ``` 95 | 96 | 97 | ### Check ggplot 98 | 99 | ```{r, eval = TRUE, autogen=TRUE, crs_id=10945, chap_id=34104, ex_num=13} 100 | ``` 101 | 102 | 103 | NOTE: These exercises are pulled from a [DataCamp course](https://campus-no-cache.datacamp.com/courses/10945/34104?ex=1). You can find the source [here](https://github.com/datacamp/courses-xwhat-examples/tree/testwhat). 104 | -------------------------------------------------------------------------------- /vignettes/syntax.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Syntax" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Syntax} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r setup, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | eval = FALSE 15 | ) 16 | ``` 17 | 18 | `testwhat` uses the pipe operator (`%>%`) from the [`magrittr`](https://cran.r-project.org/web/packages/magrittr/vignettes/magrittr.html) package to 'chain together' SCT functions. Every chain starts with the `ex()` function call, which holds the exercise state. This exercise state contains all the information that is required to check if an exercise is correct, which are: 19 | 20 | + the student submission and the solution as text, and their corresponding parse trees. 21 | + a reference to the student environment and the solution environment. 22 | + the output and errors that were generated when executing the student code. 23 | 24 | As SCT functions are chained together with `%>%`, the exercise state is copied and adapted into so-called child states to zoom in on particular parts of the code. 25 | 26 | ## Example 27 | 28 | Consider the following snippet of markdown that represents part of an exercise: 29 | 30 | `@solution` 31 | ```{r} 32 | x <- 4 33 | if (x > 0) { 34 | print("x is positive") 35 | } 36 | ``` 37 | 38 | `@sct` 39 | ```{r} 40 | ex() %>% check_if_else() %>% { 41 | check_cond(.) %>% check_code(c("x\\s+>\\s+0", "0\\s+<\\s+x")) # chain A 42 | check_if(.) %>% check_function("print") %>% check_arg("x") %>% check_equal() # chain B 43 | } 44 | ``` 45 | 46 | - `check_if_else()` will check whether an `if` statement was coded, and will afterwards 'zoom in' on the if statement only. 47 | - Chain A: `check_cond()` will consequently zoom in on the condition part of the `if` statement, so `check_code()` will only look inside this fragment of the student submission. 48 | - Chain B: Similarly, `check_if()` starts from the `if` statement, and zooms in on the body of the `if` statement, after which `check_function()` will only look for the `print` call inside this fragment of the student submission. 49 | 50 | To further explain this example, assume the following student submission: 51 | 52 | ```{r} 53 | x <- 4 54 | if (x < 0) { 55 | print("x is negative") 56 | } 57 | ``` 58 | 59 | In chain A, this is what happens: 60 | 61 | - `check_if_else()` considers the entire submission (as contained in `ex()`), and produces a child state that contains the `if` statements in student and solution: 62 | 63 | ```{r} 64 | # solution 65 | if (x > 0) { 66 | print("x is positive") 67 | } 68 | 69 | # student 70 | if (x < 0) { 71 | print("x is negative") 72 | } 73 | ``` 74 | 75 | - `check_cond()` considers the state above produced by `check_if_else()`, and produces a child state with only the condition parts of the `if` statements: 76 | 77 | ```{r} 78 | # solution 79 | x > 0 80 | 81 | # student 82 | x < 0 83 | ``` 84 | 85 | - `check_code()` considers the state above produced by `check_cond()`, and tries to match the regexes to `x < 0` student snippet. None of the regexes match, so the test fails. 86 | 87 | Assume now that the student corrects the mistake, and submits the following (which is still not correct): 88 | 89 | ```{r} 90 | x <- 4 91 | if (x > 0) { 92 | print("x is negative") 93 | } 94 | ``` 95 | 96 | Chain A will go through the same steps and will pass this time as `x > 0` in the student submission now matches one of the regexes. In Chain B, this is what happens: 97 | 98 | - `check_if()` considers the state produced by `check_if_else()`, and produces a child state with only the body parts of the `if` statements: 99 | 100 | ```{r} 101 | # solution 102 | print("x is positive") 103 | 104 | # student 105 | print("x is negative") 106 | ``` 107 | 108 | - `check_function()` considers the state above produced by `check_if()`, and tries to find the function `print()`. Next, it produces a state with references to the different arguments that were specified and their values: 109 | 110 | ``` 111 | # solution 112 | { "x": "x is positive" } 113 | 114 | # student 115 | { "x": "x is negative" } 116 | ``` 117 | 118 | - `check_arg()` checks if the argument `x` is specified, and produces a child state that zooms in on the actual value of `x`: 119 | 120 | ```r 121 | # solution 122 | "x is positive" 123 | 124 | # student 125 | "x is negative" 126 | ``` 127 | 128 | - Finally, `check_equal()` compares the equality of the two 'focused' arguments. They are not equal, so the check fails. 129 | 130 | --------------------------------------------------------------------------------