Skip to content

Commit b208bea

Browse files
committed
Merge branch 'test' into feat/annotations_do_not_remove
2 parents cbd6364 + a36a0f0 commit b208bea

5 files changed

Lines changed: 131 additions & 43 deletions

File tree

R/CM.R

Lines changed: 106 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
# YT#VHf6c40bb7738a4549da708e6cffa92411#VHa84423515cdb57d0fffac288f003e279#
1+
# YT#VHb3a67af97b323ee47762788154a489fd#VHa53bd254d6ac8e6a19dfa057febb06b5#
22
CM <- local({ # _C_hecked _M_odule
3-
message_well <- function(title, contents, color = "f5f5f5") { # repeats #iewahg
4-
style <- sprintf(r"---(
3+
# 2025-03-21: [feature] report errors for all loaded datasets and [fix] dehardcode "PARAM" string and use `par` argument
4+
5+
message_well <- function(title, contents, color = "f5f5f5") {
6+
style <- sprintf("
57
padding: 0.5rem;
68
padding-left: 1rem;
79
margin-bottom: 20px;
@@ -10,7 +12,7 @@ CM <- local({ # _C_hecked _M_odule
1012
border-radius: 4px;
1113
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.05);
1214
box-shadow: inset 0 1px 1px rgba(0,0,0,.05);
13-
)---", color)
15+
", color)
1416

1517
res <- list(shiny::h3(title))
1618
if (length(contents)) res <- append(res, list(shiny::tags[["div"]](contents, style = style)))
@@ -47,16 +49,8 @@ CM <- local({ # _C_hecked _M_odule
4749
)
4850
}
4951

50-
err <- error_messages
51-
if (length(err)) {
52-
res[[length(res) + 1]] <-
53-
message_well("Module configuration errors", {
54-
tmp <- Map(function(x) htmltools::p(htmltools::HTML(paste("\u2022", x))), err)
55-
if (!is.null(preface)) {
56-
tmp <- append(list(htmltools::p(htmltools::HTML(preface))), tmp)
57-
}
58-
tmp
59-
}, color = "#f4d7d7")
52+
if (length(error_messages)) {
53+
res[[length(res) + 1]] <- message_well("Module configuration errors", error_messages, color = "#f4d7d7")
6054
}
6155

6256
return(res)
@@ -109,7 +103,6 @@ CM <- local({ # _C_hecked _M_odule
109103

110104
matched_args <- try(as.list(match.call(module)), silent = TRUE)
111105
error_message <- attr(matched_args, "condition")$message
112-
error_message_dataset_index <- NULL
113106
if (is.null(error_message)) {
114107
missing_args <- setdiff(mandatory_module_args, names(matched_args))
115108
if (length(missing_args)) {
@@ -143,31 +136,80 @@ CM <- local({ # _C_hecked _M_odule
143136
# - Here we also have access to the original datasets, which allows us to ensure call
144137
# correctness independent of filter state or operation in a single pass.
145138
# - "catch errors early"
146-
for (i_dataset in seq_along(afmm[["data"]])) {
139+
dataset_count <- length(afmm[["data"]])
140+
141+
res_by_dataset <- list()
142+
error_count_by_dataset <- integer(0)
143+
error_count <- 0
144+
for (i_dataset in seq_len(dataset_count)) {
147145
check_args <- append(
148146
list(
149147
afmm = afmm, # To check receiver_ids, among others
150148
datasets = afmm[["data"]][[i_dataset]] # Allows data checks prior to reactive time
151149
),
152150
args
153151
)
154-
res <- do.call(check_mod_fn, check_args)
155-
# NOTE: Stop when errors are found on a single dataset to avoid overwhelming users with repeat messages
156-
if (length(res[["errors"]])) { # NOTE: Not checking "warnings" because they are going away soon
157-
error_message_dataset_index <- i_dataset
158-
break
152+
info <- do.call(check_mod_fn, check_args)
153+
res_by_dataset[[i_dataset]] <- info
154+
error_count_by_dataset[[i_dataset]] <- length(info[["errors"]])
155+
error_count <- error_count + length(info[["errors"]])
156+
}
157+
158+
as_items <- function(x) htmltools::p(htmltools::HTML(paste("\u2022", x)))
159+
160+
if (error_count == 0) NULL
161+
else if (dataset_count == 1) {
162+
# single dataset
163+
res <- res_by_dataset[[1]]
164+
res[["errors"]] <- Map(as_items, res[["errors"]])
165+
} else {
166+
# multiple datasets
167+
168+
# FIXME(miguel): We don't do merge "warnings" here because it's a feature that goes unused and we will
169+
# remove it soon. We can't remove it _now_ because it would still require minor fixes
170+
# in at least five different packages and we want to roll a CM bugfix (unrelated to
171+
# the "multiple dataset" feature reporting) while avoiding cascading work.
172+
errors <- list()
173+
174+
dataset_names <- names(afmm[["datasets"]])
175+
176+
errors <- c(
177+
list(htmltools::p(htmltools::HTML(
178+
"Issues have been grouped by input dataset. Expand/collapse the elements below to inspect them:"
179+
)))
180+
)
181+
182+
details_extra <- "open"
183+
for (i_dataset in seq_len(dataset_count)){
184+
if (error_count_by_dataset[[i_dataset]] == 0) next
185+
186+
details_pre <- htmltools::HTML(
187+
sprintf('
188+
<details %s>
189+
<summary style="display:list-item"><b>%s</b></summary>
190+
', details_extra, names(afmm[["data"]]))[[i_dataset]]
191+
)
192+
details_extra <- ""
193+
194+
details_post <- htmltools::HTML("</details>")
195+
196+
errors <- c(errors, list(details_pre))
197+
198+
errors <- c(
199+
errors,
200+
list(htmltools::HTML("<div style='padding: 0.5rem; margin-bottom: 1rem; background-color: #FFFFFF55;
201+
border: 1px solid #AAAAAA; border-radius: 4px;'>")),
202+
Map(as_items, res_by_dataset[[i_dataset]][["errors"]]),
203+
list(htmltools::HTML("</div>"))
204+
)
205+
206+
errors <- c(errors, list(details_post))
207+
208+
res[["errors"]] <- errors
159209
}
160210
}
161211
}
162212

163-
if (!is.null(error_message_dataset_index) && length(afmm[["data"]]) > 1) {
164-
dataset_name <- names(afmm[["data"]])[[error_message_dataset_index]]
165-
res[["preface"]] <- paste(
166-
"This application has been configured with more than one dataset.",
167-
sprintf("The following error messages apply to the dataset named <b>%s</b>.<br>", dataset_name),
168-
"No error checking has been performed on datasets specified after it. <hr>"
169-
)
170-
}
171213
return(res)
172214
})
173215

@@ -599,7 +641,7 @@ CM <- local({ # _C_hecked _M_odule
599641
} else if (is.character(v)) {
600642
res <- sprintf('"%s"', unique(v))
601643
} else {
602-
browser()
644+
stop(sprintf('Unsuported class "%s" as argument to `list_values`', class(v)))
603645
}
604646

605647
res <- paste(res, collapse = ", ")
@@ -719,9 +761,9 @@ CM <- local({ # _C_hecked _M_odule
719761

720762
unique_cat_par_combinations <- unique(dataset[c(cat, par)])
721763
dup_mask <- duplicated(unique_cat_par_combinations[par])
722-
unique_repeat_params <- unique_cat_par_combinations[[par]][dup_mask]
723764

724-
ok <- assert(err, length(unique_repeat_params) == 0, {
765+
ok <- assert(err, !any(dup_mask), {
766+
unique_repeat_params <- unique_cat_par_combinations[[par]][dup_mask]
725767
dups <- df_to_string(
726768
data.frame(
727769
check.names = FALSE,
@@ -738,7 +780,7 @@ CM <- local({ # _C_hecked _M_odule
738780
sprintf('%s <- dv.explorer.parameter::prefix_repeat_parameters(%s, cat_var = "%s", par_var = "%s")',
739781
ds_value, ds_value, cat, par)
740782

741-
mask <- unique_cat_par_combinations[["PARAM"]] %in% unique_repeat_params
783+
mask <- unique_cat_par_combinations[[par]] %in% unique_repeat_params
742784
deduplicated_table <- df_to_string({
743785
cats <- unique_cat_par_combinations[mask, ][[cat]]
744786
pars <- unique_cat_par_combinations[mask, ][[par]]
@@ -764,21 +806,47 @@ CM <- local({ # _C_hecked _M_odule
764806
})
765807

766808
supposedly_unique <- dataset[c(sub, cat, par, vis)]
767-
dups <- duplicated(supposedly_unique)
809+
dup_mask <- duplicated(supposedly_unique)
768810

769-
ok <- ok && assert(err, !any(dups), {
811+
ok <- ok && assert(err, !any(dup_mask), {
770812
prefixes <- c(
771813
rep("Subject:", length(sub)), rep("Category:", length(cat)),
772814
rep("Parameter:", length(par)), rep("Visit:", length(vis))
773815
)
774816

775-
first_duplicates <- head(supposedly_unique[dups, ], 5)
817+
first_duplicates <- head(supposedly_unique[dup_mask, ], 5)
776818
names(first_duplicates) <- paste(prefixes, names(first_duplicates))
777819
dups <- df_to_string(first_duplicates)
820+
821+
unique_repeats <- unique(supposedly_unique[dup_mask, ])
822+
target <- unique_repeats[1, ]
823+
target_rows <- which(supposedly_unique[[sub]] == target[[sub]] & supposedly_unique[[cat]] == target[[cat]] &
824+
supposedly_unique[[par]] == target[[par]] & supposedly_unique[[vis]] == target[[vis]])
825+
826+
row_a <- dataset[target_rows[[1]], ]
827+
row_b <- dataset[target_rows[[2]], ]
828+
diff_cols <- character(0)
829+
for (col in names(row_a)) if (!identical(row_a[[col]], row_b[[col]])) diff_cols <- c(diff_cols, col)
830+
831+
col_diff_report <- "are identical."
832+
if (length(diff_cols)) {
833+
col_diff_report <- paste0(
834+
"have indeed identical subject, category, parameter and visit values, but differ in columns: ",
835+
paste(diff_cols, collapse = ", "), ".",
836+
"<pre>",
837+
df_to_string(dataset[target_rows[1:2], c(sub, cat, par, vis, diff_cols)]),
838+
"</pre>"
839+
)
840+
}
841+
778842
paste(
779843
sprintf("The dataset provided by `%s` (%s) contains repeated rows with identical subject, category, parameter", ds_name, ds_value),
780-
"and visit values. This module expects them to be unique. Here are the first few duplicates:",
781-
paste0("<pre>", dups, "</pre>")
844+
sprintf("and visit values. This module expects them to be unique. There are a total of %d duplicates.", sum(dup_mask)),
845+
"Here are the first few:",
846+
paste0("<pre>", dups, "</pre>"),
847+
sprintf("These findings can be partially confirmed by examining that rows <b>%d</b> and <b>%d</b> of that dataset",
848+
target_rows[[1]], target_rows[[2]]),
849+
col_diff_report
782850
)
783851
})
784852

R/TC.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# YT#VHaab92a03d738b9f7b5020f6fd244e065#VH6fdb3d19d5c72c6488f3bcfe86c03095#
1+
# YT#VH1202cc468f3cbf448dd25ff917fc4762#VH00000000000000000000000000000000#
22
TC <- local({ # _T_ype C_hecks
33
# basic types
44
T_logical <- function() list(kind = "logical")

R/utils-misc.R

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# TODO: Tag as shared snippet (also used in dv.bookman)
2+
DTH <- local({
3+
# _DT_ _H_elpers
4+
set_column_heading_hover_info <- function(title, text) {
5+
# Appends a 🛈 sign next to the title that shows a hover-on `text` message.
6+
# Requires DT::datatable(escape = FALSE)
7+
paste(
8+
title,
9+
htmltools::tags$i(
10+
class = "glyphicon glyphicon-info-sign",
11+
style = "color:#0072B2;",
12+
title = text
13+
) |> as.character()
14+
)
15+
}
16+
17+
return(list(
18+
set_column_heading_hover_info = set_column_heading_hover_info
19+
))
20+
})

tests/testthat/setup.R

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ vdoc <- local({
1010
specs <- vdoc[["specs"]]
1111
# validation (F)
1212

13-
# YT#VH27d48516d3cbdadbf92a7b5e0860b78e#VH19ec235e56cdd18f129215603abf0ca6#
13+
# YT#VH0bf15c0db690dfd3fac713f3c9b61f66#VH00000000000000000000000000000000#
1414

1515
#' Test harness for communication with `dv.papo`.
1616
#'
1717
#' @param mod Parameterized instance of the module to test. Should produce valid output and not trigger a `shiny::req`.
1818
#' @param data Data matching the previous parameterization.
1919
#' @param trigger_input_id Fully namespaced input ID that, when set to a subject ID value,
2020
#' should make the module send `dv.papo` a message.
21-
test_communication_with_papo <- function(mod, data, trigger_input_id) {
21+
test_communication_with_papo <- function(mod, data, trigger_input_id, papo_spec_id, papo_spec_text) {
2222
datasets <- shiny::reactive(data)
2323

2424
afmm <- list(
@@ -51,7 +51,7 @@ test_communication_with_papo <- function(mod, data, trigger_input_id) {
5151
app <- shiny::shinyApp(ui = app_ui, server = app_server)
5252

5353
testthat::test_that("module adheres to send_subject_id_to_papo protocol" |>
54-
vdoc[["add_spec"]](specs$jumping_feature), {
54+
vdoc[["add_spec"]](papo_spec_text, papo_spec_id), {
5555
app <- shinytest2::AppDriver$new(app, name = "test_send_subject_id_to_papo_protocol")
5656

5757
app$wait_for_idle()

tests/testthat/test-message_papo.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ mod <- dv.listings::mod_listings(
2929
)
3030

3131
trigger_input_id <- "mod-selected_subject_id"
32-
test_communication_with_papo(mod, data_list, trigger_input_id)
32+
test_communication_with_papo(mod, data_list, trigger_input_id, "jumping_feature", specs$jumping_feature)

0 commit comments

Comments
 (0)