Skip to content

Commit

Permalink
Merge pull request #130 from selkamand/26-add-explanation-of-low-code…
Browse files Browse the repository at this point in the history
…-cov

26 add explanation of low code cov
  • Loading branch information
selkamand authored Nov 13, 2024
2 parents e935f9d + 31ed56b commit a47b9c2
Show file tree
Hide file tree
Showing 10 changed files with 593 additions and 12 deletions.
6 changes: 3 additions & 3 deletions R/assert_create.R
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ assert_create <- function(func, default_error_msg = NULL){
}

# Ensure func has at least 1 argument
if(func_arg_count(func) == 0){
if(func_arg_count(func, dots = "count_as_0") == 0){
if (func_supports_variable_arguments(func))
additional_note = " (Note '...' does NOT count as an argument)"
else additional_note = ""
Expand All @@ -72,7 +72,7 @@ assert_create <- function(func, default_error_msg = NULL){

# Assert that function has no arguments named 'msg' or 'call', 'arg_name', since we need to add our own
if(any(c('msg', 'call', 'arg_name') %in% names(args))){
cli::cli_abort("Function supplied to `func` argument of `create_dataframe` cannot include paramaters namex 'msg' or 'call', 'arg_name', since we add our own arguments with these names")
cli::cli_abort("Function supplied to `func` argument of `assert_create` cannot include paramaters named 'msg', 'call', or 'arg_name', since assert_create adds these arguments to every assertion")
}

# Change add 'msg', 'call' and 'arg_name' arguments at the end
Expand Down Expand Up @@ -204,7 +204,7 @@ assert_create_chain <- function(...){
))
}

# Check functions all have the required arguments (x, msg & call)
# Check functions all have the required arguments (msg, call and arg_name)
if(!all(vapply(dot_args, function(f){ all(c('msg', 'call', 'arg_name') %in% func_arg_names(f)) }, FUN.VALUE = logical(1)))){
cli::cli_abort(
c("Input to {.strong assert_create_chain} must must be {.strong functions} created by {.strong `assert_create()`}",
Expand Down
10 changes: 6 additions & 4 deletions R/assert_files.R
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ get_file_extensions <- function(filenames) {
}, character(1))
}

#' Title
#' Has Extension
#'
#' @param x object to test
#' @param extensions valid extensions (character vector). Do not include the '.', e.g. supply `extensions = 'txt'` not `extensions = '.txt'`
Expand All @@ -46,16 +46,18 @@ has_extension <- function(x, extensions, compression = FALSE){
all(observed_ext %in% extensions)
}

# Which of the filenames are missing the required extension?
files_missing_extension <- function(x, extensions, compression = FALSE){
original = x
if(compression){
x = sub(x = x,"\\.(gz|bz2|xz)$","")
x = sub(x = x,"\\.(gz|bz2|xz)$","")
}

observed_ext <- get_file_extensions(x)
x[!observed_ext %in% extensions]
original[!observed_ext %in% extensions]
}

# Files ---------------------------------------------------------------
# File Assertions ---------------------------------------------------------------

#' Assert that all files exist
#'
Expand Down
6 changes: 3 additions & 3 deletions R/set_operations.R
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,9 @@ sets_are_equivalent <- function(x, y){
else if(any_missing & !any_extra) "missing"

missing_plural = if(length(missing_values) > 1) "s" else ""
missing_plural_the = if(length(missing_values) < 2) " a " else ""
missing_plural_the = if(length(missing_values) < 2) "a " else ""
extra_plural = if(length(extra_values) > 1) "s" else ""
extra_plural_the = if(length(extra_values) < 2) " an " else ""
extra_plural_the = if(length(extra_values) < 2) "an " else ""


if(failure_mode == "both"){
Expand All @@ -188,6 +188,6 @@ sets_are_equivalent <- function(x, y){
return(paste0("'{arg_name}' contains ", extra_plural_the, "unexpected value",extra_plural,": {setopts_exlusive_to_first(x, y)}."))
}
else if(failure_mode == "missing"){
return(paste0("'{arg_name}' is missing" ,missing_plural_the, " required value",missing_plural,": {setopts_exlusive_to_first(y, x)}."))
return(paste0("'{arg_name}' is missing " ,missing_plural_the, "required value",missing_plural,": {setopts_exlusive_to_first(y, x)}."))
}
}
4 changes: 2 additions & 2 deletions man/has_extension.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions tests/testthat/test-assert_create.R
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,24 @@ cli::test_that_cli(configs = "plain", "created assertion() functions throw infor
expect_true(assert_f1('a', 'b'))
})


cli::test_that_cli(configs = "plain", "assert_create edge case errors", {

# Function has dots
expect_no_error(assert_create(func = function(a, b, ...){ FALSE }))

# Function has dots but no other arguments
expect_error(assert_create(func = function( ...){ FALSE }), regexp = "must have at least 1 paramater.*Note '\\.\\.\\.' does NOT count as an argument")

# Function has names that clash with those assert_create adds to all assertions
expect_error(assert_create(func = function(msg){ FALSE }), regexp = "cannot include paramaters named 'msg', 'call', or 'arg_name", fixed=TRUE)

# arg_name is not a string
assertion <- assert_create(func = function(a){ FALSE }, default_error_msg = "{arg_name} is ignored - this function always throws an error")
expect_error(assertion(a, arg_name = 2), regexp = "arg_name must be a string, not a numeric")
})


# Test Creation of Assertion Chains -----------------------------------------------------
cli::test_that_cli(configs = "plain", "assertion chains can evaluate expressions part and not get confused if they contain variable names", {
#assert_is_character <- assert_create(is.character, "Error: {arg_name} must be a character")
Expand All @@ -130,6 +148,26 @@ cli::test_that_cli(configs = "plain", "assertion chains can evaluate expressions
expect_error(assert_chain(length(y)), regexp = "length(y) must be a character", fixed = TRUE)
})

cli::test_that_cli(configs = "plain", "Common assert_create_chain errors", {

# Throws error if argument given to assert_create_chain is not a function
expect_error(assert_create_chain(
2,
assert_create(is.numeric, "{arg_name} must be numeric")
), regexp = "Input to assert_create_chain must must be functions created by `assert_create()`", fixed=TRUE)

# Throws error a function doesn't have the required arguments (msg, call and arg_name)
expect_error(assert_create_chain(
function(x, msg, arg_name, notcall){},
assert_create(is.numeric, "{arg_name} must be numeric")
), regexp = "Input to assert_create_chain must must be functions created by `assert_create()`", fixed=TRUE)

# Throws error if functions have less than 4 args (some_obj_to_test and officially required functions: msg, call, arg_name)
expect_error(assert_create_chain(
function(msg, call, arg_name){}, # 3 args only
assert_create(is.numeric, "{arg_name} must be numeric")
), regexp = "Input to assert_create_chain must must be functions created by `assert_create()`", fixed=TRUE)
})

cli::test_that_cli(configs = "plain", "assert_create_chain: user supplied custom error message has access to the environment in which it was called", {
assert_chain<- assert_create_chain(
Expand All @@ -152,3 +190,11 @@ cli::test_that_cli(configs = "plain", "assert_create_chain: user supplied custo
age = "26"
expect_error(assert_chain(age, "{arg_name} must be a number, not a {class(arg_value)}"), "age must be a number, not a character", fixed=TRUE)
})


cli::test_that_cli(configs = "plain", "assert_create_chain_example", {
expect_no_error(assert_create_chain_example())
expect_true(is.character(assert_create_chain_example()))
})


130 changes: 130 additions & 0 deletions tests/testthat/test-assert_files.R
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,133 @@ test_that("assert_directory_does_not_exist() works", {
expect_error(assert_file_does_not_exist(dirpath), regexp = "Directory .* already exists")
})


# Test Underlying Functions -----------------------------------------------

## All files exist ------------------------------------------------
test_that("all_files_exist() works", {
# False when files don't exist
expect_false(all_files_exist(c("ALKAWDWLKDLWADJWLD", "ADASKJDLAJWLDJKDKLWDJLAKDJWLKLDAW")))

# Create two temporary files
f1 <- withr::local_tempfile()
f2 <- withr::local_tempfile()
f3 <- "Does Not exist"

# Write some content to the files to ensure they exist
writeLines("Test content 1", f1)
writeLines("Test content 2", f2)

# Test that both files exist
expect_true(file.exists(f1))
expect_true(file.exists(f2))

# Test all_files_exist() function
expect_true(all_files_exist(c(f1, f2)))
expect_false(all_files_exist(c(f1, f2, f3)))
})

## Has Extension ------------------------------------------------
test_that("has_extension works() works", {
# False when files don't exist
expect_false(all_files_exist(c("ALKAWDWLKDLWADJWLD", "ADASKJDLAJWLDJKDKLWDJLAKDJWLKLDAW")))

# Create two temporary files
f1 <- withr::local_tempfile()
f2 <- withr::local_tempfile()
f3 <- "Does Not exist"

# Write some content to the files to ensure they exist
writeLines("Test content 1", f1)
writeLines("Test content 2", f2)

# Test that both files exist
expect_true(file.exists(f1))
expect_true(file.exists(f2))

# Test all_files_exist() function
expect_true(all_files_exist(c(f1, f2)))
expect_false(all_files_exist(c(f1, f2, f3)))
})


test_that("has_extension works with single valid extensions", {
expect_true(has_extension("file.txt", extensions = "txt"))
expect_true(has_extension("file.csv", extensions = "csv"))
expect_false(has_extension("file.doc", extensions = "txt"))
})

test_that("has_extension works with multiple valid extensions", {
expect_true(has_extension("file.txt", extensions = c("txt", "csv")))
expect_true(has_extension("file.csv", extensions = c("txt", "csv")))
expect_false(has_extension("file.doc", extensions = c("txt", "csv")))
})

test_that("has_extension handles multiple files", {
expect_true(has_extension(c("file1.txt", "file2.txt"), extensions = "txt"))
expect_false(has_extension(c("file1.txt", "file2.csv"), extensions = "txt"))
expect_true(has_extension(c("file1.txt", "file2.csv"), extensions = c("txt", "csv")))
})

test_that("has_extension handles compression correctly", {
expect_true(has_extension("file.txt.gz", extensions = "txt", compression = TRUE))
expect_false(has_extension("file.doc.gz", extensions = "txt", compression = TRUE))
expect_true(has_extension("file.txt.bz2", extensions = "txt", compression = TRUE))
expect_true(has_extension("file.csv.xz", extensions = "csv", compression = TRUE))
expect_false(has_extension("file.csv.zip", extensions = "csv", compression = TRUE)) # zip is not supported as compression
})

test_that("has_extension handles files without extensions", {
expect_false(has_extension("file", extensions = "txt"))
expect_false(has_extension("file", extensions = c("txt", "csv")))
expect_true(has_extension("file", extensions = "")) # No extension, treated as valid if `extensions` includes an empty string
})

test_that("has_extension handles mixed cases and unexpected inputs", {
expect_false(has_extension(c("file.TXT", "file.csv"), extensions = "txt")) # Case sensitivity
expect_true(has_extension("file.txt.gz", extensions = "txt", compression = TRUE))
expect_false(has_extension("file.txt.gz", extensions = "gz", compression = TRUE)) # Compression stripped
expect_true(has_extension(c("file.txt", "file.TXT"), extensions = c("txt", "TXT"))) # Mixed-case extensions
})

## Files Missing Extension ------------------------------------------------

test_that("files_missing_extension identifies files without specified extensions", {
# Only "file.doc" does not match the specified extensions
expect_equal(files_missing_extension(c("file.txt", "file.csv", "file.doc"), extensions = c("txt", "csv")),
c("file.doc"))

# Both "file.doc" and "file.pdf" do not match "txt"
expect_equal(files_missing_extension(c("file.txt", "file.doc", "file.pdf"), extensions = "txt"),
c("file.doc", "file.pdf"))

# No files are missing the specified extension
expect_equal(files_missing_extension(c("file.txt", "file.csv"), extensions = c("txt", "csv")),
character(0))
})


test_that("files_missing_extension handles compression correctly", {
# Only "file.doc.gz" does not match "txt" when compression is enabled
expect_equal(files_missing_extension(c("file.txt.gz", "file.csv.bz2", "file.doc.gz"), extensions = "txt", compression = TRUE),
c("file.csv.bz2", "file.doc.gz"))

# When compression is disabled, all files with compression extensions are treated as missing
expect_equal(files_missing_extension(c("file.txt.gz", "file.csv.bz2", "file.doc.gz"), extensions = c("txt", "csv"), compression = FALSE),
c("file.txt.gz", "file.csv.bz2", "file.doc.gz"))

# Mixed case: Only "file.doc.xz" is missing the specified extensions
expect_equal(files_missing_extension(c("file.txt.gz", "file.csv.bz2", "file.doc.xz"), extensions = c("txt", "csv"), compression = TRUE),
c("file.doc.xz"))
})

test_that("files_missing_extension handles files without any extensions", {
# Files without extensions should be identified as missing
expect_equal(files_missing_extension(c("file1", "file2.txt", "file3"), extensions = "txt"),
c("file1", "file3"))

# If extensions includes an empty string, files without extensions should be considered valid
expect_equal(files_missing_extension(c("file1", "file2.txt", "file3"), extensions = c("txt", "")),
character(0))
})

76 changes: 76 additions & 0 deletions tests/testthat/test-assert_functions.R
Original file line number Diff line number Diff line change
@@ -1,3 +1,79 @@

# Test Underlying Functions -----------------------------------------------
# Define some test functions for validation
fn_0_args <- function() {}
fn_1_arg <- function(a) {}
fn_2_args <- function(a, b) {}
fn_1_arg_with_dots <- function(a, ...) {}
fn_2_args_with_dots <- function(a, b, ...) {}

# Unit tests for `function_expects_n_arguments_advanced`
test_that("function_expects_n_arguments_advanced behaves correctly for exact argument count", {
# Exact argument match
expect_true(function_expects_n_arguments_advanced(fn_0_args, 0))
expect_true(function_expects_n_arguments_advanced(fn_1_arg, 1))
expect_true(function_expects_n_arguments_advanced(fn_2_args, 2))

# Mismatched argument counts
expect_match(function_expects_n_arguments_advanced(fn_1_arg, 2), "must expect exactly {.strong {n}} argument", fixed = TRUE)
expect_match(function_expects_n_arguments_advanced(fn_2_args, 1), "must expect exactly {.strong {n}} argument", fixed = TRUE)
})

test_that("function_expects_n_arguments_advanced handles dots behavior correctly", {
# Test with `...` and dots="throw_error"
expect_match(function_expects_n_arguments_advanced(fn_1_arg_with_dots, 1, dots = "throw_error"),
"must not contain ... arguments", fixed = TRUE)

# Test with `...` and dots="count_as_0"
expect_true(function_expects_n_arguments_advanced(fn_1_arg_with_dots, 1, dots = "count_as_0"))
expect_true(function_expects_n_arguments_advanced(fn_2_args_with_dots, 2, dots = "count_as_0"))

# Test with `...` and dots="count_as_1"
expect_true(function_expects_n_arguments_advanced(fn_1_arg_with_dots, 2, dots = "count_as_1"))
expect_true(function_expects_n_arguments_advanced(fn_2_args_with_dots, 3, dots = "count_as_1"))

# Test with `...` and dots="count_as_inf"
expect_true(function_expects_n_arguments_advanced(fn_1_arg_with_dots, Inf, dots = "count_as_inf"))
expect_true(function_expects_n_arguments_advanced(fn_2_args_with_dots, Inf, dots = "count_as_inf"))
})

test_that("function_expects_n_arguments_advanced returns correct error for non-function inputs", {
# Input is not a function
expect_match(function_expects_n_arguments_advanced(42, 1), "must be a function, not a", fixed = TRUE)
expect_match(function_expects_n_arguments_advanced("not_a_function", 1), "must be a function, not a", fixed = TRUE)
})

test_that("function_expects_n_arguments_advanced correctly counts arguments for functions with no arguments", {
# Functions with no arguments should pass with 0 expected arguments
expect_true(function_expects_n_arguments_advanced(fn_0_args, 0))

# Mismatch for functions with no arguments
expect_match(function_expects_n_arguments_advanced(fn_0_args, 1), "must expect exactly {.strong {n}} argument", fixed = TRUE)
})

test_that("function_expects_n_arguments_advanced correctly counts arguments for functions with multiple arguments", {
# Functions with multiple arguments should match exactly
expect_true(function_expects_n_arguments_advanced(fn_2_args, 2))

# Mismatch for functions with multiple arguments
expect_match(function_expects_n_arguments_advanced(fn_2_args, 3), "must expect exactly {.strong {n}} argument", fixed = TRUE)
})

test_that("function_expects_n_arguments_advanced handles `dots` parameter correctly with complex cases", {
# Functions with variadic args (dots) should adapt based on `dots` parameter
expect_true(function_expects_n_arguments_advanced(fn_1_arg_with_dots, 1, dots = "count_as_0"))
expect_true(function_expects_n_arguments_advanced(fn_2_args_with_dots, 2, dots = "count_as_0"))
expect_true(function_expects_n_arguments_advanced(fn_1_arg_with_dots, 2, dots = "count_as_1"))
expect_true(function_expects_n_arguments_advanced(fn_2_args_with_dots, 3, dots = "count_as_1"))
expect_true(function_expects_n_arguments_advanced(fn_1_arg_with_dots, Inf, dots = "count_as_inf"))
expect_true(function_expects_n_arguments_advanced(fn_2_args_with_dots, Inf, dots = "count_as_inf"))
})




# Test Assertions ---------------------------------------------------------

cli::test_that_cli("assert_function_expects_n_arguments() works", config = "plain", {
# Works for functions with the correct number of arguments
my_func <- function(x, y) { x + y }
Expand Down
Loading

0 comments on commit a47b9c2

Please sign in to comment.