forked from elinw/swirl
-
Notifications
You must be signed in to change notification settings - Fork 0
/
answerTests2.R
507 lines (496 loc) · 19.8 KB
/
answerTests2.R
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
#' Answer Tests
#'
#' Answer tests are how swirl determines whether a user has answered
#' a question correctly or not. Each question has one or more answer
#' tests associated with it, all of which must be satisfied in order for
#' a user's response to be considered correct. As the instructor, you
#' can specify any combination of our predefined answer tests or create your
#' own custom answer tests to suit your specific needs. This document will
#' explain your options.
#'
#' @name AnswerTests
#'
#' @details
#' For each question that you author as part of a swirl lesson, you
#' must specify exactly one \emph{correct answer}. This is separate and
#' distinct from the answer tests. This does not have to be
#' the only correct answer, but it must answer the question correctly.
#' If a user \code{\link{skip}}s your question, this is the answer that will be
#' entered on his or her behalf.
#'
#' If you're using the \href{https://github.com/swirldev/swirlify}{swirlify}
#' authoring tool, the correct answer will
#' be automatically translated into the appropriate answer test for most
#' question types. Questions that require the user to enter a valid
#' command at the R prompt (which we'll refer to as \emph{command questions})
#' are the only exception. Since there are often many possible ways to
#' answer a command question, you must determine how you'd like swirl to
#' assess the correctness of a user's response. This is where answer
#' tests come in.
#'
#' You can specify any number of answer tests. If you use more than one, you
#' must separate them with semicolons. If you do not specify any answer tests
#' for a command question, then the default test will be used. The default
#' test is \code{omnitest(correctExpr='<correct_answer_here>')}, which will
#' simply check that the user's expression matches the expression that you
#' provided as a correct answer.
#'
#' In many cases, the default answer test will provide sufficient vetting of
#' a user's response to a command question. While it is somewhat restrictive
#' in the sense that it requires an exact match of expressions (ignoring
#' whitespace), it is liberating to the course author for two reasons.
#' \enumerate{
#' \item It allows for fast prototyping of content. As you're developing
#' content, you may find that determining how to test for correctness
#' distracts you from the message you're trying to communicate.
#' \item You don't have to worry about what happens if the user enters
#' an incorrect response, but is allowed to proceed because of an oversight
#' in the answer tests. Since swirl sessions are continuous, accepting
#' an incorrect answer early in a lesson can cause problems later on. By
#' using the default answer test, you eliminate this burden by requiring an
#' exact match of expressions and hence not allowing the user to advance
#' until you are certain they've entered the correct response.
#' }
#'
#' It's important to keep in mind that as your content matures, you can always
#' go back and make your answer testing strategy more elaborate. The main
#' benefit of using tests other than the default is that the user will not be
#' required to enter an expression exactly the way you've specified it. He or
#' she will have more freedom in terms of how they respond to a question, as
#' long as they satisfy the conditions that you see as being most important.
#'
#' @section Predefined Answer Tests:
#' Each of the predefined answer tests listed below has
#' its own help file, where you'll find more detailed explanations and
#' examples.
#'
#' \code{\link{any_of_exprs}}: Test that the user's expression matches any of several possible expressions.
#'
#' \code{\link{calculates_same_value}}: Test that the user's expression evaluates to a certain value.
#'
#' \code{\link{expr_creates_var}}: Test that a new variable has been created.
#'
#' \code{\link{expr_identical_to}}: Test that the user has entered a particular expression.
#'
#' \code{\link{expr_is_a}}: Test that the expression itself is of a specific \code{\link{class}}.
#'
#' \code{\link{expr_uses_func}}: Test that a particular function has been used.
#'
#' \code{\link{func_of_newvar_equals}}: Test the result of a computation such as \code{mean(newVar)} applied to a specific (user-named) variable created in a previous question.
#'
#' \code{\link{omnitest}}: Test for a correct expression, a correct value, or both.
#'
#' \code{\link{val_has_length}}: Test that the value of the expression has a particular \code{\link{length}}.
#'
#' \code{\link{val_matches}}: Test that the user's expression matches a regular expression (\code{\link{regex}}).
#'
#' \code{\link{var_is_a}}: Test that the \emph{value} of the expression is of a specific \code{\link{class}}.
#'
#' @section Custom Answer Tests:
#' Occasionally, you may want to test something that is outside the scope of
#' our predefined answer tests. Fortunately, this is very easy to do. If you
#' are using the swirlify authoring tool, then a file called
#' \code{customTests.R} (case-sensitive) is automatically created in the lesson
#' directory. If it's not there already, you can create the file manually.
#'
#' In this file, you can write your own answer tests. These answer tests are
#' then available to you just the same as any of the standard tests. However,
#' the scope of a custom answer test is limited to the lesson within which
#' you've defined it.
#'
#' Each custom answer test is simply an R function that follows a few
#' basic rules:
#' \enumerate{
#' \item Give the function a distinct name that will help you remember what
#' is does (e.g. \code{creates_matrix_with_n_rows}).
#' \item The first line of the function body is
#' \code{e <- get("e", parent.frame())}, which gives you access to the
#' environment \code{e}. Any important information, such as the expression
#' typed by the user, will be available to you through \code{e}.
#' \item Access the expression entered by the user with \code{e$expr} and
#' the value of the expression with \code{e$val}.
#' Note that \code{e$expr} comes in
#' the form of an unevaluated R \code{\link{expression}}.
#' \item The function returns \code{TRUE} if the test is passed and
#' \code{FALSE} otherwise. You should be careful that no other
#' value could be returned (e.g. \code{NA}, \code{NULL}, etc.)
#' }
#'
#' @family AnswerTests
NULL
#' Test for a correct expression, a correct value, or both.
#'
#' Omnitest can test for a correct expression, a correct value,
#' or both. In the case of values it is limited to testing for
#' character or numeric vectors of length 1.
#' @param correctExpr the correct or expected expression as a string
#' @param correctVal the correct value (numeric or character)
#' @param strict a logical value indicating that the expression should be as expected even if the value is correct. If \code{FALSE} (the default) a correct value will pass the test even if the expression is not as expected, but a notification will be issued.
#' @param eval_for_class a logical value. If TRUE, evaluate the first argument of an S3 method to determine its class. Default=TRUE. Global value may also be set as customTests$EVAL_FOR_CLASS.
#' @examples
#' \dontrun{
#'
#' # Test that a user has chosen a correct menu item
#' #
#' omnitest(correctVal='Men in a college dorm.')
#'
#' # Test that a user has entered a correct number at the
#' # command line
#' #
#' omnitest(correctVal=19)
#'
#' # Test that a user has entered a particular command
#' #
#' omnitest('myVar <- c(3, 5, 7)')
#'
#' # Test that a user has entered a command which computes
#' # a specific value but perhaps in a different manner
#' # than anticipated
#' #
#' omnitest('sd(x)^2', 5.95)
#' #
#' # If the user enters sd(x)*sd(x), rather than sd(x)^2, a notification
#' # will be issued, but the test will not fail.
#'
#' # Test that a user has entered a command which computes
#' # a specific value in a particular way
#' #
#' omnitest('sd(x)^2', 5.95, strict=TRUE)
#' #
#' # In this case, if the user enters sd(x)*sd(x) the test will fail.
#'
#' }
#' @family AnswerTests
omnitest <- function(correctExpr=NULL, correctVal=NULL, strict=FALSE, eval_for_class=as.logical(NA)){
e <- get("e", parent.frame())
# Trivial case
if(is.null(correctExpr) && is.null(correctVal))return(TRUE)
# If eval_for_class is not specified, default to customTests$EVAL_FOR_CLASS.
# If the latter is not set, default to TRUE.
if(is.na(eval_for_class)){
if(exists("EVAL_FOR_CLASS", customTests)){
eval_for_class <- isTRUE(customTests$EVAL_FOR_CLASS)
} else {
eval_for_class <- TRUE
}
}
# If eval_for_class is TRUE, create a parent environment for that in
# in which evaluations for class are to be made.
eval_env <- if(eval_for_class){
cleanEnv(e$snapshot)
} else {
NULL
}
# Testing for correct expression only
if(!is.null(correctExpr) && is.null(correctVal)){
err <- try({
good_expr <- parse(text=correctExpr)[[1]]
ans <- is_robust_match(good_expr, e$expr, eval_for_class, eval_env)
}, silent=TRUE)
if (is(err, "try-error")) {
return(expr_identical_to(correctExpr))
} else {
return(ans)
}
}
# Testing for both correct expression and correct value
# Value must be character or single number
valGood <- as.logical(NA)
if(!is.null(correctVal)){
if(is.character(e$val)){
valResults <- expectThat(e$val,
is_equivalent_to_legacy(correctVal, label=correctVal),
label=(e$val))
if(is(e, "dev") && !valResults$passed)swirl_out(valResults$message)
valGood <- valResults$passed
# valGood <- val_matches(correctVal)
} else if(!is.na(e$val) && is.numeric(e$val) && length(e$val) == 1){
cval <- try(as.numeric(correctVal), silent=TRUE)
valResults <- expectThat(e$val,
equals_legacy(cval, label=correctVal),
label=toString(e$val))
if(is(e, "dev") && !valResults$passed)swirl_out(valResults$message)
valGood <- valResults$passed
}
}
# If a correct expression is given attempt a robust match with user's expression.
exprGood <- TRUE
if(!is.null(correctExpr)){
err <- try({
good_expr <- parse(text=correctExpr)[[1]]
ans <- is_robust_match(good_expr, e$expr, eval_for_class, eval_env)
}, silent=TRUE)
exprGood <- ifelse(is(err, "try-error"), expr_identical_to(correctExpr), ans)
}
if((isTRUE(valGood) || is.na(valGood)) && exprGood){
return(TRUE)
} else if (isTRUE(valGood) && !exprGood && !strict){
swirl_out(s()%N%"That's not the expression I expected but it works.")
swirl_out(s()%N%"I've executed the correct expression in case the result is needed in an upcoming question.")
eval(parse(text=correctExpr),globalenv())
return(TRUE)
} else {
return(FALSE)
}
}
#' Test that the user has entered a particular expression.
#'
#' Test that the user has entered an expression identical to that
#' given as the first argument.
#' @param correct_expression the correct or expected expression as a string
#' @return \code{TRUE} or \code{FALSE}
#' @examples
#' \dontrun{
#' # Test that a user has entered a particular command
#' #
#' expr_identical_to('myVar <- c(3, 5, 7)')
#' }
#' @family AnswerTests
expr_identical_to <- function(correct_expression){
e <- get("e", parent.frame())
expr <- e$expr
if(is.expression(expr))expr <- expr[[1]]
correct <- parse(text=correct_expression)[[1]]
results <- expectThat(expr,
is_identical_to_legacy(correct, label=correct_expression),
label=deparse(expr))
if( is(e, "dev") && !results$passed)swirl_out(results$message)
return(results$passed)
}
#' Test that the user's expression evaluates to a certain value.
#'
#' Test that the value calculated by the user's expression is the same as the
#' value calculated by the given expression.
#' @param expression An expression whose value will be compared to the value
#' of the user's expression.
#' @return \code{TRUE} or \code{FALSE}
#' @examples
#' \dontrun{
#' # Test that a user's expression evaluates to a certain value
#' #
#' calculates_same_value('matrix(1:20, nrow=4, ncol=5)')
#' }
#' @family AnswerTests
calculates_same_value <- function(expression){
e <- get("e", parent.frame())
# Calculate what the user should have done.
eSnap <- cleanEnv(e$snapshot)
val <- eval(parse(text=expression), eSnap)
passed <- isTRUE(all.equal(val, e$val))
if(!passed)e$delta <- list()
return(passed)
}
#' Test that the user's expression matches a regular expression.
#'
#' Returns \code{TRUE} if \code{as.character(e$val)} matches the regular
#' expression given as the first argument.
#' @param regular_expression a regular expression which user value should match
#' @return \code{TRUE} or \code{FALSE}
#' @examples
#' \dontrun{
#' # Test that a user has entered a value matching
#' # '[Cc]ollege [Ss]tudents' or has selected it
#' # in a multiple choice question.
#' #
#' val_matches('[Cc]ollege [Ss]tudents')
#' }
#' @family AnswerTests
val_matches <- function(regular_expression) {
e <- get("e", parent.frame())
userVal <- str_trim(as.character(e$val))
results <- expectThat(userVal,
matches_legacy(regular_expression),
label=userVal)
if(is(e,"dev") && !results$passed)swirl_out(results$message)
return(results$passed)
}
#' Test that the user has entered one of several possible expressions.
#'
#' Returns \code{TRUE} if the expression the user has entered
#' matches any of the expressions given (as character strings) in
#' the argument.
#' @param ... any number of expressions, as character strings
#' @return \code{TRUE} or \code{FALSE}
#' @examples
#' \dontrun{
#'
#' # Test that a user has entered either cor(x, y) or cor(y, x)
#' any_of_exprs('cor(x, y)', 'cor(y, x)')
#' }
#' @family AnswerTests
any_of_exprs <- function(...){
e <- get("e", parent.frame())
any(sapply(c(...), function(expr) omnitest(expr)))
}
#' Test that the value of the expression is of a specific class.
#'
#' Returns \code{TRUE} if a variable of the given name exists
#' in the global environment and is of the given class.
#' @param class expected class which the given variable
#' @param var_name name of the variable
#' @return \code{TRUE} or \code{FALSE}
#' @examples
#' \dontrun{
#' # Test that a variable named "x" in the global environmentis numeric.
#' var_is_a('numeric', 'x')
#' }
#' @family AnswerTests
var_is_a <- function(class, var_name) {
e <- get("e", parent.frame())
class <- str_trim(class)
var_name <- str_trim(var_name)
if(exists(var_name, globalenv())){
val <- get(var_name, globalenv())
label <- val
results <- expectThat(val, is_a_legacy(class), label=label)
if(is(e,"dev") && !results$passed)swirl_out(results$message)
return(results$passed)
} else {
if(is(e,"dev"))swirl_out(paste(var_name, "does not exist."))
return(FALSE)
}
}
#' Test that the expression itself is of a specific \code{class}.
#'
#' Returns \code{TRUE} if \code{e$expr} is of the given \code{\link{class}}.
#' @param class expected \code{class} of the given expression
#' @return \code{TRUE} or \code{FALSE}
#' @examples
#' \dontrun{
#' # Test if the expression entered by a user is an assignment
#' #
#' expr_is_a('<-')
#' }
#' @family AnswerTests
expr_is_a <- function(class) {
e <- get("e", parent.frame())
class <- str_trim(class)
expr <- e$expr
label <- deparse(e$expr)
results <- expectThat(expr, is_a_legacy(class), label=label)
if(is(e,"dev") && !results$passed)swirl_out(results$message)
return(results$passed)
}
#' Test that a particular function has been used.
#'
#' Returns \code{TRUE} if the \code{e$expr} uses the function whose
#' name is given as the first argument.
#' @param func name of the function expected to be used
#' @return \code{TRUE} or \code{FALSE}
#' @examples
#' \dontrun{
#' # Test that the user has entered an expression using sd()
#' #
#' expr_uses_func('sd')
#' }
#' @family AnswerTests
expr_uses_func <- function(func) {
e <- get("e", parent.frame())
func <- str_trim(func)
results <- expectThat(e$expr,
uses_func(func, label=func),
label=deparse(e$expr))
if(is(e,"dev") && !results$passed)swirl_out(results$message)
return(results$passed)
}
#' Test that a new variable has been created.
#'
#' Tests if the \code{e$expr} creates one new variable (of correct name
#' if given.) If so, returns \code{TRUE}.
#' @param correctName expected name of the new variable or \code{NULL}
#' @return \code{TRUE} or \code{FALSE}
#' @examples
#' \dontrun{
#' # Test if the user has entered an expression which creates
#' # a new variable of any name.
#' expr_creates_var()
#' #
#' # Test if the user has entered an expression which creates
#' # a variable named 'myNum'
#' #
#' expr_creates_var('myNum')
#' }
#' @family AnswerTests
expr_creates_var <- function(correctName=NULL){
e <- get("e", parent.frame())
# TODO: Eventually make auto-detection of new variables an option.
# Currently it can be set in customTests.R
delta <- if(!customTests$AUTO_DETECT_NEWVAR){
safeEval(e$expr, e)
} else {
e$delta
}
if(is.null(correctName)){
results <- expectThat(length(delta), equals_legacy(1),
label=paste(deparse(e$expr),
"does not create a variable."))
} else {
results <- expectThat(names(delta),
is_equivalent_to_legacy(correctName, label=correctName),
label=paste(deparse(e$expr),
"does not create a variable named",
correctName))
}
if(results$passed){
e$newVar <- e$val
e$newVarName <- names(delta)[1]
e$delta <- mergeLists(delta, e$delta)
} else if(is(e,"dev")){
swirl_out(results$message)
}
return(results$passed)
}
#' Test that the value of the expression has a particular \code{length}.
#'
#' Test the the \code{\link{length}} of \code{e$val} is that given by the
#' first argument.
#' @param len expected length of the variable created by a user
#' @return \code{TRUE} or \code{FALSE}
#' @examples
#' \dontrun{
#' # Test that the user has created a varible of length 10
#' #
#' val_has_length(10)
#' }
#' @family AnswerTests
val_has_length <- function(len){
e <- get("e", parent.frame())
try(n <- as.integer(len), silent=TRUE)
if(is.na(n)){
stop(message=paste("BUG: specified length", len,
"is not an integer."))
}
results <- expectThat(length(e$val), equals_legacy(n, label=n),
label=paste0("length(c(", toString(e$val), "))"))
if( is(e, "dev") && !results$passed)swirl_out(results$message)
return(results$passed)
}
#' Test the result of a computation applied to a specific (user-named)
#' variable created in a previous question.
#'
#' Tests the result of a computation such as \code{mean(newVar)} applied
#' to a specific variable created in a previous question and
#' saved behind the scenes as \code{e$newVar}.
#' @param correct_expression expression expected to be applied
#' @return \code{TRUE} or \code{FALSE}
#' @examples
#' \dontrun{
#' # Test if user has taken the mean of a variable created
#' # in an earlier question.
#' #
#' func_of_newvar_equals('mean(newVar)')
#' }
#' @family AnswerTests
func_of_newvar_equals <- function(correct_expression){
e <- get("e", parent.frame())
e1 <- cleanEnv(e$snapshot)
assign(e$newVarName, e$newVar, e1)
correctExpr <- gsub("newVar", e$newVarName, correct_expression)
ans <- eval(parse(text=correctExpr), e1)
results <- expectThat(e$val,
equals_legacy(ans,
label=correctExpr),
label=deparse(e$expr))
if(is(e, "dev") && !results$passed)swirl_out(results$message)
return(results$passed)
}