diff --git a/NEWS.md b/NEWS.md index a48d396..bd95835 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,7 @@ # hubAdmin (development version) * Use options to set `schema_version` and `branch` arguments in `download_tasks_schema()` and the `create_*()` family of functions for creating config files programmatically. This allows for setting the schema version and branch globally for the session (#85). +* Make validation of `round_id` patterns explicit. This ties in with schema version v4.0.1 where the pattern the `round_id` property must match if `round_id_from_variable` is `false` is now specified as a regular expression in the schema. This check is also now implemented dynamically on values of the `round_id` variable if `round_id_from_variable` is `true` when validating tasks.json config files. Checks on `round_id` patterns are also now implemented in `create_round()` when creating rounds programmatically. # hubAdmin 1.4.0 diff --git a/R/create_round.R b/R/create_round.R index fdada5d..d74b825 100644 --- a/R/create_round.R +++ b/R/create_round.R @@ -136,6 +136,9 @@ create_round <- function(round_id_from_variable, } if (round_id_from_variable) { check_round_id_variable(model_tasks, round_id) + check_round_id_pattern_vals(model_tasks, round_id) + } else { + check_round_id_pattern(round_id) } structure( @@ -276,3 +279,64 @@ get_schema_round <- function(schema) { "items", "properties" ) } +# Check round_id pattern in `round_id` var values when +# round_id_from_variable = TRUE +check_round_id_pattern_vals <- function(model_tasks, round_id, + call = rlang::caller_env()) { + invalid_round_id_vals <- purrr::map( + model_tasks$model_tasks, + ~ { + round_id_var_vals <- purrr::pluck( + .x, "task_ids", round_id + ) + invalid_round_id_var_patterns(round_id_var_vals) |> + purrr::compact() + } + ) + invalid_round_id_vals <- purrr::set_names( + invalid_round_id_vals, + seq_along(invalid_round_id_vals) + ) + + if (length(unlist(invalid_round_id_vals)) > 0L) { + invalid_vals_bullets <- + purrr::compact(invalid_round_id_vals) |> + # iterate over any model tasks containing invalid values + purrr::imap(~ { + mt_idx <- .y + # iterate over invalid values in "required" and "optional" properties if present + purrr::imap_chr( + .x, + ~ { + # Create a separate message for invalid values in each model task and property + cli::format_inline("In {.arg model_tasks[[{mt_idx}]]${round_id}${.y}}: {.val {.x}}") + } + ) + }) |> + unlist() |> + purrr::set_names("x") + + cli::cli_abort( + c( + "!" = "Values in {.var round_id} var {.val {round_id}} must contain either + ISO formatted dates or alphanumeric characters separated by underscores ('_').", + invalid_vals_bullets + ), + call = call + ) + } +} +# Check round_id pattern when round_id_from_variable = FALSE +check_round_id_pattern <- function(round_id, + call = rlang::caller_env()) { + if (!validate_round_id_pattern(round_id)) { + cli::cli_abort( + c( + "!" = "{.var round_id} must contain either ISO formatted date or + alphanumeric characters separated by underscores ('_').", + "x" = "{.val {round_id}} does not match expected pattern" + ), + call = call + ) + } +} diff --git a/R/get_error_path.R b/R/get_error_path.R index 8a28b4a..3173850 100644 --- a/R/get_error_path.R +++ b/R/get_error_path.R @@ -17,9 +17,15 @@ #' unevaluated in the output and are instead encoded a glue interpolation strings #' (i.e wrapped in `{}`). #' They are defined by variables `round_i`, `model_task_i` or `target_key_i` depending -#' on the depth of the property being validated. Values for these variables need to be passed -#' using `glue::glue_data()` and explicitly passing an index variable -#' and it's value as a named list. +#' on the depth of the property being validated. To interpolate these values into +#' valid instance path, you can either: +#' - wrap `get_error_path()` in `glue::glue()` and let the function interpolate +#' the values using objects available in the caller environment. +#' - wrap `get_error_path()` in `glue::glue_data()` and pass the values explicitly +#' as a named list. +#' +#' Note as well that instance paths are converted to zero indexed format to align +#' with the output of basic JSON schema validation (i.e. performed by `jsonvalidate`) #' @noRd #' @examples #' # Return the instance path to the origin date task ID in the second modeling task diff --git a/R/utils-round_ids.R b/R/utils-round_ids.R new file mode 100644 index 0000000..3e3555b --- /dev/null +++ b/R/utils-round_ids.R @@ -0,0 +1,27 @@ +validate_round_id_pattern <- function(x) { + stringr::str_detect( + x, + "^(\\d{4}-\\d{2}-\\d{2})$|^[A-Za-z0-9_]+$" + ) +} + +# This function expects a list representation of the task ID variable values +# specified in `round_id` when `round_id_from_variable` is `true.` +# Returns a list with `required` and `optional` elements containg either the +# invalid values identified by the `validate_round_id_pattern` function or NULL. +invalid_round_id_var_patterns <- function(round_id_var_vals) { + purrr::map( + round_id_var_vals, + \(.x) { + if (is.null(.x)) { + return(NULL) + } + valid <- validate_round_id_pattern(.x) + invalid <- .x[!valid] + if (length(invalid) == 0L) { + return(NULL) + } + invalid + } + ) +} diff --git a/R/validate-config-utils.R b/R/validate-config-utils.R index 5309abe..99f0787 100644 --- a/R/validate-config-utils.R +++ b/R/validate-config-utils.R @@ -478,6 +478,58 @@ validate_mt_property_unique_vals <- function(model_task_grp, } } +# Check that modeling task round ids match the expected round ID patterns when +# round_id_from_variable = TRUE +validate_mt_round_id_pattern <- function(model_task_grp, + model_task_i, + round_i, + schema, + round_id_from_variable, + round_id_var) { + if (!round_id_from_variable) { + return(NULL) + } + round_id_var_vals <- purrr::pluck( + model_task_grp, "task_ids", round_id_var + ) + invalid_vals <- invalid_round_id_var_patterns(round_id_var_vals) + + if (any(lengths(invalid_vals) > 0L)) { + # Collapse invalid values into a single string + invalid_vals_msg <- purrr::compact(invalid_vals) |> + purrr::map_chr( + ~ glue::glue_collapse(glue::glue("'{.x}'"), ", ", last = " and ") + ) + + error_row <- tibble::tibble( + instancePath = paste( + glue::glue( + get_error_path(schema, "/task_ids", "instance") + ), + # using names(invalid_vals_msg) creates a row for each property + # ("required"/"optional") containing invalid round_id values + round_id_var, names(invalid_vals_msg), + sep = "/" + ), + schemaPath = paste( + get_error_path( + schema, + glue::glue("task_ids/{round_id_var}"), + "schema" + ), names(invalid_vals_msg), + sep = "/" + ), + keyword = "round_id variable pattern", + message = glue::glue( + "round_id variable '{round_id_var}' values must be either ISO formatted + dates or alphanumeric characters separated by '_'." + ), + schema = "^([0-9]{4}-[0-9]{2}-[0-9]{2})$|^[A-Za-z0-9_]+$", + data = glue::glue("invalid values: {invalid_vals_msg}") + ) + return(error_row) + } +} ## ROUND LEVEL VALIDATIONS ---- # Check that round id variables are consistent across modeling tasks validate_round_ids_consistent <- function(round, round_i, @@ -596,7 +648,6 @@ validate_round_derived_task_ids <- function(round, round_i, schema) { out } - ## CONFIG LEVEL VALIDATIONS ---- # Validate that round IDs are unique across all rounds in config file validate_round_ids_unique <- function(config_tasks, schema) { diff --git a/R/validate_config.R b/R/validate_config.R index 9216c38..4057454 100644 --- a/R/validate_config.R +++ b/R/validate_config.R @@ -185,6 +185,8 @@ perform_dynamic_config_validations <- function(validation) { ## Dynamic schema validation utilities ---- val_round <- function(round, round_i, schema) { model_task_grps <- round[["model_tasks"]] + round_id_from_variable <- round[["round_id_from_variable"]] + round_id_var <- round[["round_id"]] c( purrr::imap( @@ -233,6 +235,18 @@ val_round <- function(round, round_i, schema) { schema = schema ) ), + purrr::imap( + model_task_grps, + \(.x, .y) { + validate_mt_round_id_pattern( + model_task_grp = .x, model_task_i = .y, + round_i = round_i, + schema = schema, + round_id_from_variable = round_id_from_variable, + round_id_var = round_id_var + ) + } + ), list( validate_round_ids_consistent( round = round, diff --git a/R/view_config_val_errors.R b/R/view_config_val_errors.R index e5a6582..23d8b76 100644 --- a/R/view_config_val_errors.R +++ b/R/view_config_val_errors.R @@ -80,10 +80,20 @@ clean_error_df <- function(errors_tbl) { if (is.null(errors_tbl)) { return(NULL) } + + # Move any custom error messages to the message column + if (!is.null(purrr::pluck(errors_tbl, "parentSchema", "errorMessage"))) { + error_msg <- !is.na(errors_tbl$parentSchema$errorMessage) + errors_tbl$message[error_msg] <- errors_tbl$parentSchema$errorMessage[error_msg] + } + errors_tbl[c("dataPath", "parentSchema")] <- NULL errors_tbl <- errors_tbl[!grepl("oneOf.+", errors_tbl$schemaPath), ] + # remove superfluous if error. The "then" error is what we are interested in + errors_tbl <- errors_tbl[!errors_tbl$keyword == "if", ] errors_tbl <- remove_superfluous_enum_rows(errors_tbl) + # Get rid of unnecessarily verbose data entry when a data column is a data.frame if (inherits(errors_tbl$data, "data.frame")) { errors_tbl$data <- "" @@ -269,6 +279,16 @@ extract_params_to_data <- function(errors_tbl, errors_tbl } +escape_pattern_dollar <- function(error_df) { + is_pattern <- grepl("pattern", error_df[["keyword"]]) + error_df[["schema"]][is_pattern] <- gsub( + "$", "$", + error_df[["schema"]][is_pattern], + fixed = TRUE + ) + error_df +} + render_errors_df <- function(error_df) { schema_version <- attr(error_df, "schema_version") schema_url <- attr(error_df, "schema_url") @@ -286,6 +306,9 @@ render_errors_df <- function(error_df) { error_df[["schemaPath"]] <- purrr::map_chr(error_df[["schemaPath"]], path_to_tree) error_df[["instancePath"]] <- purrr::map_chr(error_df[["instancePath"]], path_to_tree) error_df[["message"]] <- paste("\u274c", error_df[["message"]]) + # Escape `$` characters to ensure regex pattern does not trigger equation + # formatting in markdown + error_df <- escape_pattern_dollar(error_df) # Create table ---- diff --git a/tests/testthat/_snaps/create_round.md b/tests/testthat/_snaps/create_round.md index a2a174d..d7a1efe 100644 --- a/tests/testthat/_snaps/create_round.md +++ b/tests/testthat/_snaps/create_round.md @@ -400,8 +400,7 @@ --- Code - create_derived_task_ids_round(version = "v4.0.0", branch = "br-v4.0.0", - derived_task_ids = 1L) + create_derived_task_ids_round(version = "v4.0.0", derived_task_ids = 1L) Condition Error in `map()`: i In index: 1. @@ -419,3 +418,38 @@ x `derived_task_ids` value "random_task_id" is not valid `task_id` variable in the provided `model_tasks` object. i Valid `task_id` variables are: "origin_date", "location", and "horizon" +# validating round_id patterns when round_id_from_var = TRUE works + + Code + create_round(round_id_from_variable = TRUE, round_id = "round_id_var", + model_tasks = model_tasks, submissions_due = list(start = "2023-01-12", end = "2023-01-18"), + last_data_date = "2023-01-02") + Condition + Error in `create_round()`: + ! Values in `round_id` var "round_id_var" must contain either ISO formatted dates or alphanumeric characters separated by underscores ('_'). + x In `model_tasks[[1]]$round_id_var$required`: "invalid-round-id-req" + x In `model_tasks[[1]]$round_id_var$optional`: "invalid-round-id-opt1" and "invalid-round-id-opt2" + +--- + + Code + create_round(round_id_from_variable = TRUE, round_id = "round_id_var", + model_tasks = model_tasks, submissions_due = list(start = "2023-01-12", end = "2023-01-18"), + last_data_date = "2023-01-02") + Condition + Error in `create_round()`: + ! Values in `round_id` var "round_id_var" must contain either ISO formatted dates or alphanumeric characters separated by underscores ('_'). + x In `model_tasks[[1]]$round_id_var$optional`: "invalid-round-id-opt1" and "invalid-round-id-opt2" + x In `model_tasks[[2]]$round_id_var$required`: "invalid-round-id-req1" and "invalid-round-id-req2" + +# validating round_id pattern when round_id_from_var = FALSE works + + Code + create_round(round_id_from_variable = FALSE, round_id = "round-id-var", + model_tasks = model_tasks, submissions_due = list(start = "2023-01-12", end = "2023-01-18"), + last_data_date = "2023-01-02") + Condition + Error in `create_round()`: + ! `round_id` must contain either ISO formatted date or alphanumeric characters separated by underscores ('_'). + x "round-id-var" does not match expected pattern + diff --git a/tests/testthat/test-create_round.R b/tests/testthat/test-create_round.R index 6b789e9..30ce7e1 100644 --- a/tests/testthat/test-create_round.R +++ b/tests/testthat/test-create_round.R @@ -182,7 +182,6 @@ test_that("create_round name matching works correctly", { test_that("create_round derived_task_ids argument", { skip_if_offline() - # TODO: Remove branch specification when v4.0.0 released expect_snapshot( create_derived_task_ids_round( version = "v4.0.0", @@ -212,7 +211,7 @@ test_that("create_round derived_task_ids argument", { expect_snapshot( create_derived_task_ids_round( - version = "v4.0.0", branch = "br-v4.0.0", + version = "v4.0.0", derived_task_ids = 1L ), error = TRUE @@ -225,3 +224,251 @@ test_that("create_round derived_task_ids argument", { error = TRUE ) }) + +test_that("validating round_id patterns when round_id_from_var = TRUE works", { + skip_if_offline() + withr::with_options( + list( + hubAdmin.schema_version = "v4.0.1", + hubAdmin.branch = "br-v4.0.1" + ), + { + output_types <- create_output_type( + create_output_type_mean( + is_required = TRUE, + value_type = "double", + value_minimum = 0L + ) + ) + target_metadata <- create_target_metadata( + create_target_metadata_item( + target_id = "inc hosp", + target_name = "Weekly incident influenza hospitalizations", + target_units = "rate per 100,000 population", + target_keys = NULL, + target_type = "discrete", + is_step_ahead = TRUE, + time_unit = "week" + ) + ) + # Check that valid round_id values are accepted + task_ids <- create_task_ids( + create_task_id("round_id_var", + required = "2023-01-09", + optional = c( + "2023-01-02", + "24_25_covid" + ) + ) + ) + model_tasks <- create_model_tasks( + create_model_task( + task_ids = task_ids, + output_type = output_types, + target_metadata = target_metadata + ) + ) + + round <- create_round( + round_id_from_variable = TRUE, + round_id = "round_id_var", + model_tasks = model_tasks, + submissions_due = list( + start = "2023-01-12", + end = "2023-01-18" + ), + last_data_date = "2023-01-02" + ) + expect_s3_class(round, "round") + + # Check that multiple invalid values in both required and optional values + # reported correctly + task_ids <- create_task_ids( + create_task_id("round_id_var", + required = "invalid-round-id-req", + optional = c( + "2023-01-02", + "24_25_covid", + "invalid-round-id-opt1", + "invalid-round-id-opt2" + ) + ) + ) + model_tasks <- create_model_tasks( + create_model_task( + task_ids = task_ids, + output_type = output_types, + target_metadata = target_metadata + ) + ) + expect_snapshot( + create_round( + round_id_from_variable = TRUE, + round_id = "round_id_var", + model_tasks = model_tasks, + submissions_due = list( + start = "2023-01-12", + end = "2023-01-18" + ), + last_data_date = "2023-01-02" + ), + error = TRUE + ) + + # Check that multiple invalid values in required or optional values + # across multiple modeling tasks reported correctly + task_ids_1 <- create_task_ids( + create_task_id("round_id_var", + required = NULL, + optional = c( + "2023-01-02", + "24_25_covid", + "invalid-round-id-opt1", + "invalid-round-id-opt2" + ) + ) + ) + task_ids_2 <- create_task_ids( + create_task_id("round_id_var", + required = c( + "2023-01-02", + "24_25_covid", + "invalid-round-id-req1", + "invalid-round-id-req2" + ), + optional = NULL, + ) + ) + model_tasks <- create_model_tasks( + create_model_task( + task_ids = task_ids_1, + output_type = output_types, + target_metadata = target_metadata + ), + create_model_task( + task_ids = task_ids_2, + output_type = output_types, + target_metadata = target_metadata + ) + ) + expect_snapshot( + create_round( + round_id_from_variable = TRUE, + round_id = "round_id_var", + model_tasks = model_tasks, + submissions_due = list( + start = "2023-01-12", + end = "2023-01-18" + ), + last_data_date = "2023-01-02" + ), + error = TRUE + ) + } + ) +}) + +test_that("validating round_id pattern when round_id_from_var = FALSE works", { + skip_if_offline() + withr::with_options( + list( + hubAdmin.schema_version = "v4.0.1", + hubAdmin.branch = "br-v4.0.1" + ), + { + output_types <- create_output_type( + create_output_type_mean( + is_required = TRUE, + value_type = "double", + value_minimum = 0L + ) + ) + target_metadata <- create_target_metadata( + create_target_metadata_item( + target_id = "inc hosp", + target_name = "Weekly incident influenza hospitalizations", + target_units = "rate per 100,000 population", + target_keys = NULL, + target_type = "discrete", + is_step_ahead = TRUE, + time_unit = "week" + ) + ) + # Check that valid round_id value is accepted while round_id_var invalid + # values are ignored + task_ids <- create_task_ids( + create_task_id("round_id_var", + required = "2023-01-09", + optional = c( + "2023-01-02", + "24_25_covid" + ) + ) + ) + model_tasks <- create_model_tasks( + create_model_task( + task_ids = task_ids, + output_type = output_types, + target_metadata = target_metadata + ) + ) + + round_alpha_num <- create_round( + round_id_from_variable = FALSE, + round_id = "round_id_var", + model_tasks = model_tasks, + submissions_due = list( + start = "2023-01-12", + end = "2023-01-18" + ), + last_data_date = "2023-01-02" + ) + expect_s3_class(round_alpha_num, "round") + + round_iso_date <- create_round( + round_id_from_variable = FALSE, + round_id = "2023-01-15", + model_tasks = model_tasks, + submissions_due = list( + start = "2023-01-12", + end = "2023-01-18" + ), + last_data_date = "2023-01-02" + ) + expect_s3_class(round_iso_date, "round") + + # Check that invalid `round_id` reported correctly + task_ids <- create_task_ids( + create_task_id("round_id_var", + required = "invalid-round-id-req", + optional = c( + "2023-01-02", + "24_25_covid", + "invalid-round-id-opt1", + "invalid-round-id-opt2" + ) + ) + ) + model_tasks <- create_model_tasks( + create_model_task( + task_ids = task_ids, + output_type = output_types, + target_metadata = target_metadata + ) + ) + expect_snapshot( + create_round( + round_id_from_variable = FALSE, + round_id = "round-id-var", + model_tasks = model_tasks, + submissions_due = list( + start = "2023-01-12", + end = "2023-01-18" + ), + last_data_date = "2023-01-02" + ), + error = TRUE + ) + } + ) +}) diff --git a/tests/testthat/test-get_error_path.R b/tests/testthat/test-get_error_path.R index 12e8472..ed3ed3f 100644 --- a/tests/testthat/test-get_error_path.R +++ b/tests/testthat/test-get_error_path.R @@ -155,3 +155,32 @@ test_that("Paths with miltiple potential matches at different depths created cor "/rounds/0/derived_task_ids" ) }) + + +test_that("Instance path index interpolation overriding works", { + skip_if_offline() + schema <- hubUtils::get_schema( + "https://raw.githubusercontent.com/hubverse-org/schemas/main/v4.0.0/tasks-schema.json" + ) + model_task_i <- 1L + round_i <- 2L + # Return the instance path to the origin date task ID in the second modeling task + # Create a non-interpolated path + expect_equal( + get_error_path(schema, "origin_date", "instance"), + "/rounds/{round_i - 1}/model_tasks/{model_task_i - 1}/task_ids/origin_date" + ) + + # Create interpolated instance path using caller environment variables + expect_equal( + glue::glue(get_error_path(schema, "origin_date", "instance")), + "/rounds/1/model_tasks/0/task_ids/origin_date" + ) + + # Create interoplated instance path to the second modeling task, + # overriding the `model_task_i` value in the caller environment + glue::glue_data( + list(model_task_i = 2L), + get_error_path(schema, "origin_date", "instance") + ) +}) diff --git a/tests/testthat/test-validate_config.R b/tests/testthat/test-validate_config.R index e1d8420..760beec 100644 --- a/tests/testthat/test-validate_config.R +++ b/tests/testthat/test-validate_config.R @@ -41,7 +41,7 @@ test_that("Missing files returns an invalid config with an immediate message", { suppressMessages({ expect_message(out <- validate_config(hub_path = tmp), "File does not exist") }) - expect_false(out) + expect_false(unclass(out)) }) test_that("Config for samples fail correctly", { skip_if_offline() @@ -251,3 +251,75 @@ test_that("v4 validation works", { ) ) }) + +test_that("v4.0.1 round_id pattern validation works", { + skip_if_offline() + # TODO: remove branch argument when v4.0.1 is released. + schema <- download_tasks_schema("v4.0.1", branch = "br-v4.0.1") + + # Test that regex pattern matching for round_id properties in jsonvalidate + # identifies expected errors (when round_id_from_variable: false). + expect_false( + res_round_id <- suppressMessages( + validate_config( + config_path = testthat::test_path( + "testdata", + "v4.0.1-tasks-fail-round-id-pattern.json" + ), + branch = "br-v4.0.1" + ) + ) + ) + errors_id <- attr(res_round_id, "errors") + expect_equal(nrow(errors_id), 2L) + expect_equal( + errors_id$message, + c( + "must match pattern \"^([0-9]{4}-[0-9]{2}-[0-9]{2})$|^[A-Za-z0-9_]+$\"", + "must match \"then\" schema" + ) + ) + expect_equal( + errors_id$schema[[1]], + "^([0-9]{4}-[0-9]{2}-[0-9]{2})$|^[A-Za-z0-9_]+$" + ) + expect_equal(errors_id$data[[1]], "invalid-round-id") + + # Test that dynamic regex pattern matching for round_id variable values + # identifies expected errors (when round_id_from_variable: true). + expect_false( + res_round_id_val <- suppressMessages( + validate_config( + config_path = testthat::test_path( + "testdata", + "v4.0.1-tasks-fail-round-id-val-pattern.json" + ), + branch = "br-v4.0.1" + ) + ) + ) + + errors_vals <- attr(res_round_id_val, "errors") + expect_equal(nrow(errors_vals), 2L) + expect_equal( + unique(errors_vals$message), + structure( + "round_id variable 'round_id_var' values must be either ISO formatted\ndates or alphanumeric characters separated by '_'.", # nolint: line_length_linter + class = c( + "glue", + "character" + ) + ) + ) + expect_equal( + unique(errors_vals$schema), + "^([0-9]{4}-[0-9]{2}-[0-9]{2})$|^[A-Za-z0-9_]+$" + ) + expect_equal( + errors_vals$data, + structure(c( + "invalid values: 'invalid-round-id-in-var-req'", + "invalid values: 'invalid-round-id-in-var-opt1' and 'invalid-round-id-in-var-opt2'" + ), class = c("glue", "character")) + ) +}) diff --git a/tests/testthat/test-validate_model_metadata_schema.R b/tests/testthat/test-validate_model_metadata_schema.R index 0176ba3..aa4b6d3 100644 --- a/tests/testthat/test-validate_model_metadata_schema.R +++ b/tests/testthat/test-validate_model_metadata_schema.R @@ -6,7 +6,7 @@ test_that("Missing files returns an invalid config with an immediate message", { "File does not exist" ) }) - expect_false(out) + expect_false(unclass(out)) }) test_that("validate_model_metadata_schema works", { @@ -28,7 +28,7 @@ test_that("validate_model_metadata_schema works", { ) ) ) - expect_false(out_error) + expect_false(unclass(out_error)) expect_snapshot(out_error) # prints .Last.value expect_snapshot(print(out_error)) # prints out_error expect_snapshot(str(attr(out_error, "errors"))) @@ -51,6 +51,6 @@ test_that("validate_model_metadata_schema errors for imparsable json", { "SyntaxError" ) }) - expect_false(out) + expect_false(unclass(out)) }) diff --git a/tests/testthat/testdata/v4.0.1-tasks-fail-round-id-pattern.json b/tests/testthat/testdata/v4.0.1-tasks-fail-round-id-pattern.json new file mode 100644 index 0000000..0813782 --- /dev/null +++ b/tests/testthat/testdata/v4.0.1-tasks-fail-round-id-pattern.json @@ -0,0 +1,277 @@ +{ + "schema_version": "https://raw.githubusercontent.com/hubverse-org/schemas/main/v4.0.1/tasks-schema.json", + "rounds": [{ + "round_id_from_variable": true, + "round_id": "round_id_var", + "model_tasks": [{ + "task_ids": { + "round_id_var": { + "required": null, + "optional": ["24_25_covid", "invalid-round-id-in-var"] + }, + "forecast_date": { + "required": null, + "optional": [ + "2022-12-19", "2022-12-26", + "2023-01-02", "2023-01-09", + "2023-01-16", "2023-01-23", "2023-01-30", "2023-02-06", "2023-02-13", + "2023-02-20", "2023-02-27", "2023-03-06", "2023-03-13", "2023-03-20", + "2023-03-27", "2023-04-03", "2023-04-10", "2023-04-17", "2023-04-24", + "2023-05-01" + ] + }, + "target": { + "required": null, + "optional": ["wk ahead inc flu hosp"] + }, + "horizon": { + "required": [2], + "optional": [1] + }, + "location": { + "required": ["US"], + "optional": [ + "01", + "02" + ] + }, + "target_date": { + "required": null, + "optional": [ + "2022-12-19", "2022-12-26", "2023-01-02", "2023-01-09", + "2023-01-16", "2023-01-23", "2023-01-30", "2023-02-06", "2023-02-13", + "2023-02-20", "2023-02-27", "2023-03-06", "2023-03-13", "2023-03-20", + "2023-03-27", "2023-04-03", "2023-04-10", "2023-04-17", "2023-04-24", + "2023-05-01", "2023-05-08", "2023-05-15" + ] + } + }, + "output_type": { + "sample": { + "output_type_id_params": { + "type": "character", + "min_samples_per_task": 50, + "max_samples_per_task": 100, + "max_length": 10 + }, + "is_required": true, + "value": { + "type": "integer", + "minimum": 0 + } + }, + "mean": { + "output_type_id": { + "required": null + }, + "is_required": true, + "value": { + "type": "double", + "minimum": 0 + } + } + }, + "target_metadata": [{ + "target_id": "wk ahead inc flu hosp", + "target_name": "weekly influenza hospitalization incidence", + "target_units": "rate per 100,000 population", + "target_keys": { + "target": "wk ahead inc flu hosp" + }, + "target_type": "discrete", + "description": "This target represents the counts of new hospitalizations per horizon week.", + "is_step_ahead": true, + "time_unit": "week" + }] + }], + "submissions_due": { + "relative_to": "forecast_date", + "start": -6, + "end": 2 + }, + "derived_task_ids": ["target_date"] + }, + { + "round_id_from_variable": false, + "round_id": "invalid-round-id", + "model_tasks": [{ + "task_ids": { + "round_id_var": { + "required": null, + "optional": null + }, + "forecast_date": { + "required": null, + "optional": [ + "2022-12-19", "2022-12-26", + "2023-01-02", "2023-01-09", + "2023-01-16", "2023-01-23", "2023-01-30", "2023-02-06", "2023-02-13", + "2023-02-20", "2023-02-27", "2023-03-06", "2023-03-13", "2023-03-20", + "2023-03-27", "2023-04-03", "2023-04-10", "2023-04-17", "2023-04-24", + "2023-05-01" + ] + }, + "target": { + "required": null, + "optional": ["wk ahead inc flu hosp"] + }, + "horizon": { + "required": [2], + "optional": [1] + }, + "location": { + "required": ["US"], + "optional": [ + "01", + "02" + ] + }, + "target_date": { + "required": null, + "optional": [ + "2022-12-19", "2022-12-26", "2023-01-02", "2023-01-09", + "2023-01-16", "2023-01-23", "2023-01-30", "2023-02-06", "2023-02-13", + "2023-02-20", "2023-02-27", "2023-03-06", "2023-03-13", "2023-03-20", + "2023-03-27", "2023-04-03", "2023-04-10", "2023-04-17", "2023-04-24", + "2023-05-01", "2023-05-08", "2023-05-15" + ] + } + }, + "output_type": { + "sample": { + "output_type_id_params": { + "type": "character", + "min_samples_per_task": 50, + "max_samples_per_task": 100, + "max_length": 10 + }, + "is_required": true, + "value": { + "type": "integer", + "minimum": 0 + } + }, + "mean": { + "output_type_id": { + "required": null + }, + "is_required": true, + "value": { + "type": "double", + "minimum": 0 + } + } + }, + "target_metadata": [{ + "target_id": "wk ahead inc flu hosp", + "target_name": "weekly influenza hospitalization incidence", + "target_units": "rate per 100,000 population", + "target_keys": { + "target": "wk ahead inc flu hosp" + }, + "target_type": "discrete", + "description": "This target represents the counts of new hospitalizations per horizon week.", + "is_step_ahead": true, + "time_unit": "week" + }] + }], + "submissions_due": { + "relative_to": "forecast_date", + "start": -6, + "end": 2 + }, + "derived_task_ids": ["target_date"] + }, + { + "round_id_from_variable": false, + "round_id": "24_25_flu", + "model_tasks": [{ + "task_ids": { + "round_id_var": { + "required": null, + "optional": null + }, + "forecast_date": { + "required": null, + "optional": [ + "2022-12-19", "2022-12-26", + "2023-01-02", "2023-01-09", + "2023-01-16", "2023-01-23", "2023-01-30", "2023-02-06", "2023-02-13", + "2023-02-20", "2023-02-27", "2023-03-06", "2023-03-13", "2023-03-20", + "2023-03-27", "2023-04-03", "2023-04-10", "2023-04-17", "2023-04-24", + "2023-05-01" + ] + }, + "target": { + "required": null, + "optional": ["wk ahead inc flu hosp"] + }, + "horizon": { + "required": [2], + "optional": [1] + }, + "location": { + "required": ["US"], + "optional": [ + "01", + "02" + ] + }, + "target_date": { + "required": null, + "optional": [ + "2022-12-19", "2022-12-26", "2023-01-02", "2023-01-09", + "2023-01-16", "2023-01-23", "2023-01-30", "2023-02-06", "2023-02-13", + "2023-02-20", "2023-02-27", "2023-03-06", "2023-03-13", "2023-03-20", + "2023-03-27", "2023-04-03", "2023-04-10", "2023-04-17", "2023-04-24", + "2023-05-01", "2023-05-08", "2023-05-15" + ] + } + }, + "output_type": { + "sample": { + "output_type_id_params": { + "type": "character", + "min_samples_per_task": 50, + "max_samples_per_task": 100, + "max_length": 10 + }, + "is_required": true, + "value": { + "type": "integer", + "minimum": 0 + } + }, + "mean": { + "output_type_id": { + "required": null + }, + "is_required": true, + "value": { + "type": "double", + "minimum": 0 + } + } + }, + "target_metadata": [{ + "target_id": "wk ahead inc flu hosp", + "target_name": "weekly influenza hospitalization incidence", + "target_units": "rate per 100,000 population", + "target_keys": { + "target": "wk ahead inc flu hosp" + }, + "target_type": "discrete", + "description": "This target represents the counts of new hospitalizations per horizon week.", + "is_step_ahead": true, + "time_unit": "week" + }] + }], + "submissions_due": { + "relative_to": "forecast_date", + "start": -6, + "end": 2 + }, + "derived_task_ids": ["target_date"] + } + ] +} diff --git a/tests/testthat/testdata/v4.0.1-tasks-fail-round-id-val-pattern.json b/tests/testthat/testdata/v4.0.1-tasks-fail-round-id-val-pattern.json new file mode 100644 index 0000000..ebcfd66 --- /dev/null +++ b/tests/testthat/testdata/v4.0.1-tasks-fail-round-id-val-pattern.json @@ -0,0 +1,190 @@ +{ + "schema_version": "https://raw.githubusercontent.com/hubverse-org/schemas/main/v4.0.1/tasks-schema.json", + "rounds": [{ + "round_id_from_variable": true, + "round_id": "round_id_var", + "model_tasks": [{ + "task_ids": { + "round_id_var": { + "required": ["invalid-round-id-in-var-req"], + "optional": [ + "24_25_covid", + "invalid-round-id-in-var-opt1", + "invalid-round-id-in-var-opt2" + ] + }, + "forecast_date": { + "required": null, + "optional": [ + "2022-12-19", "2022-12-26", + "2023-01-02", "2023-01-09", + "2023-01-16", "2023-01-23", "2023-01-30", "2023-02-06", "2023-02-13", + "2023-02-20", "2023-02-27", "2023-03-06", "2023-03-13", "2023-03-20", + "2023-03-27", "2023-04-03", "2023-04-10", "2023-04-17", "2023-04-24", + "2023-05-01" + ] + }, + "target": { + "required": null, + "optional": ["wk ahead inc flu hosp"] + }, + "horizon": { + "required": [2], + "optional": [1] + }, + "location": { + "required": ["US"], + "optional": [ + "01", + "02" + ] + }, + "target_date": { + "required": null, + "optional": [ + "2022-12-19", "2022-12-26", "2023-01-02", "2023-01-09", + "2023-01-16", "2023-01-23", "2023-01-30", "2023-02-06", "2023-02-13", + "2023-02-20", "2023-02-27", "2023-03-06", "2023-03-13", "2023-03-20", + "2023-03-27", "2023-04-03", "2023-04-10", "2023-04-17", "2023-04-24", + "2023-05-01", "2023-05-08", "2023-05-15" + ] + } + }, + "output_type": { + "sample": { + "output_type_id_params": { + "type": "character", + "min_samples_per_task": 50, + "max_samples_per_task": 100, + "max_length": 10 + }, + "is_required": true, + "value": { + "type": "integer", + "minimum": 0 + } + }, + "mean": { + "output_type_id": { + "required": null + }, + "is_required": true, + "value": { + "type": "double", + "minimum": 0 + } + } + }, + "target_metadata": [{ + "target_id": "wk ahead inc flu hosp", + "target_name": "weekly influenza hospitalization incidence", + "target_units": "rate per 100,000 population", + "target_keys": { + "target": "wk ahead inc flu hosp" + }, + "target_type": "discrete", + "description": "This target represents the counts of new hospitalizations per horizon week.", + "is_step_ahead": true, + "time_unit": "week" + }] + }], + "submissions_due": { + "relative_to": "forecast_date", + "start": -6, + "end": 2 + }, + "derived_task_ids": ["target_date"] + }, + { + "round_id_from_variable": false, + "round_id": "24_25_flu", + "model_tasks": [{ + "task_ids": { + "round_id_var": { + "required": null, + "optional": null + }, + "forecast_date": { + "required": null, + "optional": [ + "2022-12-19", "2022-12-26", + "2023-01-02", "2023-01-09", + "2023-01-16", "2023-01-23", "2023-01-30", "2023-02-06", "2023-02-13", + "2023-02-20", "2023-02-27", "2023-03-06", "2023-03-13", "2023-03-20", + "2023-03-27", "2023-04-03", "2023-04-10", "2023-04-17", "2023-04-24", + "2023-05-01" + ] + }, + "target": { + "required": null, + "optional": ["wk ahead inc flu hosp"] + }, + "horizon": { + "required": [2], + "optional": [1] + }, + "location": { + "required": ["US"], + "optional": [ + "01", + "02" + ] + }, + "target_date": { + "required": null, + "optional": [ + "2022-12-19", "2022-12-26", "2023-01-02", "2023-01-09", + "2023-01-16", "2023-01-23", "2023-01-30", "2023-02-06", "2023-02-13", + "2023-02-20", "2023-02-27", "2023-03-06", "2023-03-13", "2023-03-20", + "2023-03-27", "2023-04-03", "2023-04-10", "2023-04-17", "2023-04-24", + "2023-05-01", "2023-05-08", "2023-05-15" + ] + } + }, + "output_type": { + "sample": { + "output_type_id_params": { + "type": "character", + "min_samples_per_task": 50, + "max_samples_per_task": 100, + "max_length": 10 + }, + "is_required": true, + "value": { + "type": "integer", + "minimum": 0 + } + }, + "mean": { + "output_type_id": { + "required": null + }, + "is_required": true, + "value": { + "type": "double", + "minimum": 0 + } + } + }, + "target_metadata": [{ + "target_id": "wk ahead inc flu hosp", + "target_name": "weekly influenza hospitalization incidence", + "target_units": "rate per 100,000 population", + "target_keys": { + "target": "wk ahead inc flu hosp" + }, + "target_type": "discrete", + "description": "This target represents the counts of new hospitalizations per horizon week.", + "is_step_ahead": true, + "time_unit": "week" + }] + }], + "submissions_due": { + "relative_to": "forecast_date", + "start": -6, + "end": 2 + }, + "derived_task_ids": ["target_date"] + } + ] +}