diff --git a/NAMESPACE b/NAMESPACE index 5ec57a978e..6b0e0f583e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -28,6 +28,7 @@ export(trigger_results) export(trigger_tests) export(use_manual_app) export(use_shinyjster) +export(view_image_diffs) export(view_test_images) export(write_sysinfo) importFrom(stats,setNames) diff --git a/R/view-image-diffs.R b/R/view-image-diffs.R new file mode 100644 index 0000000000..48f57b2321 --- /dev/null +++ b/R/view-image-diffs.R @@ -0,0 +1,151 @@ +# TODO +# - Make method to check all images in git status? +# * No. This requires too many permissions. Skipping for now. + + +setup_gha_image_diffs <- function( + ..., + min_diff = 3, + repo_dir = ".", + # location to save images + out_dir = tempfile(), + sha = git_sha(repo_dir) + # # location of repo for comparisons + # vanilla_repo_dir = "../z-shinycoreci.nosync" +) { + ellipsis::check_dots_empty() + repo_dir <- normalizePath(repo_dir) + # vanilla_repo_dir <- normalizePath(vanilla_repo_dir) + force(sha) + withr::local_dir(repo_dir) + + out_dir <- "inst/img-diff-app" + dir.create(out_dir, recursive = TRUE, showWarnings = FALSE) + + + proc_locs <- as.list(Sys.which(c("git", "gm"))) + if (proc_locs$git == "") stop("Please install git") + if (proc_locs$gm == "") stop("Please install graphicsmagick via `brew install graphicsmagick` or `apt-get install graphicsmagick`") + + all_files <- system("git diff --name-only", intern = TRUE) + png_files <- all_files[fs::path_ext(all_files) == "png" & grepl("^inst/apps/", all_files)] + + tmp_img_path <- file.path(out_dir, "tmp_original_image.png") + # Extract original image so that two files can be compared + get_original_file <- function(png_file) { + system(paste0("git show HEAD:", png_file, " > ", tmp_img_path)) + tmp_img_path + } + on.exit(if (file.exists(tmp_img_path)) unlink(tmp_img_path)) + + message("\nFinding images...") + p <- progress::progress_bar$new( + format = "[:current/:total;:eta] :name", + total = length(png_files), + show_after = 0, + clear = FALSE + ) + diffs <- lapply(png_files, function(png_file) { + p$tick(tokens = list(name = png_file)) + # Compare images + shinytest2::screenshot_max_difference(png_file, get_original_file(png_file)) + }) + # diffs <- as.list(seq_along(png_files)) + + diffs <- setNames(diffs, png_files) + # hist(unlist(diffs)) + + diff_folder <- file.path(out_dir, "image_diffs") + if (dir.exists(diff_folder)) unlink(diff_folder, recursive = TRUE) + dir.create(diff_folder, showWarnings = FALSE, recursive = TRUE) + + bad_pngs <- names(diffs[diffs > min_diff]) + bad_diff_count <- unname(unlist(diffs[bad_pngs])) + + message("\nDiff images") + p <- progress::progress_bar$new( + format = "[:current;:total;:eta] :name", + total = length(bad_pngs), + show_after = 0, + clear = FALSE + ) + img_dt <- + lapply(bad_pngs, function(bad_png) { + p$tick(tokens = list(name = bad_png)) + san_path <- fs::path_sanitize(bad_png, "_") + new_png <- fs::file_copy( + bad_png, + file.path(diff_folder, fs::path_ext_set(san_path, ".new.png")), + overwrite = TRUE + ) + orig_png <- fs::file_copy( + get_original_file(bad_png), + file.path(diff_folder, fs::path_ext_set(san_path, ".old.png")), + overwrite = TRUE + ) + diff_png <- file.path(diff_folder, san_path) + system(paste0( + "gm compare ", new_png, " ", orig_png, " -highlight-style assign -file ", diff_png + )) + + dplyr::tibble( + diff_png = diff_png, + orig_png = orig_png, + new_png = new_png + ) + }) %>% + dplyr::bind_rows() + + png_dt <- dplyr::tibble( + file = bad_pngs, + diff_png = img_dt$diff_png, + orig_png = img_dt$orig_png, + new_png = img_dt$new_png, + diff_count = bad_diff_count + ) %>% + dplyr::mutate( + # inst/apps/041-dynamic-ui/tests/testthat/_snaps/linux-4.0/mytest/022.png + base = gsub("^inst/apps/", "", file), + app = gsub("/.*$", "", base), + snap = file.path(basename(dirname(base)), basename(base)), + platform_combo = basename(dirname(dirname(base))), + anchor = fs::path_sanitize(file, "_"), + ) %>% + dplyr::select(-base) %>% + tidyr::separate_wider_delim(cols = "platform_combo", names = c("platform", "Rver"), delim = "-", cols_remove = FALSE) + + data_path <- file.path(out_dir, "data.json") + writeLines( + jsonlite::serializeJSON(png_dt, pretty = TRUE), + data_path, + useBytes = TRUE + ) + + data_path +} + + + +#' @export +view_image_diffs <- function( + ..., + run_fix_snaps = TRUE, + run_setup = TRUE, + open_viewer = TRUE, + repo_dir = ".", + sha = git_sha(repo_dir) +) { + ellipsis::check_dots_empty() + if (run_fix_snaps) { + fix_snaps(ask_apps = FALSE, ask_branches = FALSE, repo_dir = repo_dir, sha = sha) + } + if (run_setup) { + setup_gha_image_diffs(repo_dir = repo_dir, sha = sha) + } + + withr::local_dir(repo_dir) + rmarkdown::render("inst/img-diff-app/diff.Rmd") + if (open_viewer) { + system("open inst/img-diff-app/diff.html") + } +} diff --git a/R/zzz.R b/R/zzz.R index 37650e30b8..0d9d80f6ec 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,5 +1,7 @@ +`%>%` <- NULL .onLoad <- function(...) { + `%>%` <<- dplyr::`%>%` apps_on_load() } diff --git a/inst/img-diff-app/.gitignore b/inst/img-diff-app/.gitignore new file mode 100644 index 0000000000..3097b7ff5f --- /dev/null +++ b/inst/img-diff-app/.gitignore @@ -0,0 +1,4 @@ +image_diffs +tmp_original_image.png +data.json +diff.html diff --git a/inst/img-diff-app/app.R b/inst/img-diff-app/app.R new file mode 100644 index 0000000000..8bf9c330f8 --- /dev/null +++ b/inst/img-diff-app/app.R @@ -0,0 +1,87 @@ +library(bslib) +library(shiny) + +print(getwd()) +png_dt <- jsonlite::unserializeJSON(paste0(readLines("data.json"), collapse = "\n")) + +ui <- bslib::page_sidebar( + shiny::uiOutput("title"), + "Diff: ", shiny::verbatimTextOutput("diff_count"), + shiny::imageOutput("diff_image"), + sidebar = bslib::sidebar( + style="white-space: nowrap;", + Map( + seq_len(nrow(png_dt)), + png_dt$app, + png_dt$snap, + png_dt$platform_combo, + f = function(i, app, snap, platform_combo) { + shiny::actionLink( + paste0("link_", i), + # paste0(app, " ", snap, " on ", platform_combo) + paste0(app, "/", platform_combo, "/", snap) + ) + } + ) + ) +) + +server <- function(input, output, session) { + bad_row <- reactiveVal(1) + + lapply(seq_len(nrow(png_dt)), function(i) { + diff_image <- png_dt$diff_file[i] + + observe({ + req(input[[paste0("link_", i)]]) # Force reactivity + bad_row(i) + }) + }) + + output$title <- shiny::renderUI({ + req(bad_row()) + info <- as.list(png_dt[bad_row(), ]) + shiny::tagList( + shiny::h1( + shiny::a( + target="_blank", + href=paste0("https://github.com/rstudio/shinycoreci/tree/main/inst/apps/", info$app), + info$app + ) + ), + shiny::p( + "Snap: ", + shiny::a( + target="_blank", + href=paste0("https://github.com/rstudio/shinycoreci/tree/main/", info$file), + paste0(tail(strsplit(info$file, "/")[[1]], 3), collapse = "/") + ) + ), + shiny::p( + "App: ", + shiny::a( + target="_blank", + href=paste0("https://testing-apps.shinyapps.io/", info$app), + "shinyapps.io" + ) + ), + + shiny::p( + "Diff: ", + shiny::code(info$diff_count) + ), + ) + }) + + output$diff_image <- shiny::renderImage({ + req(bad_row()) + list( + src = file.path("image_diffs", basename(png_dt$diff_file[bad_row()])), + contentType = "image/png", + width = "100%", + style="border: 1px solid black;" + ) + }, deleteFile = FALSE) +} + +shiny::shinyApp(ui, server) diff --git a/inst/img-diff-app/diff.Rmd b/inst/img-diff-app/diff.Rmd new file mode 100644 index 0000000000..b5775822cb --- /dev/null +++ b/inst/img-diff-app/diff.Rmd @@ -0,0 +1,234 @@ +--- +title: Image diffs +output: + html_document: + self_contained: true + style: style.css +--- + +# Latest shinytest2 snapshot diffs + +```{css, echo = FALSE} +/* barret */ +body .main-container { + max-width: 100%; + margin-left: 30px; + margin-right: 30px; +} +``` + + +```{r, include=FALSE} +# library(bslib) +library(shiny) + +png_dt <- jsonlite::unserializeJSON(paste0(readLines("data.json"), collapse = "\n")) +png_dt <- png_dt[order(png_dt$diff_count, decreasing = TRUE), ] +``` + + +```{r, echo = FALSE} +library(gt) +gt_dt <- png_dt +gt_dt$diff_img_w_anchor <- Map( + gt_dt$anchor, + gt_dt$diff_png, + f = function(anchor, diff_png) { + gt::html(as.character(shiny::a( + href=paste0("#", anchor), + shiny::img( + src = file.path("image_diffs", basename(diff_png)), + height = 100 + ) + ))) + } +) +gt_dt$app <- Map( + gt_dt$app, + gt_dt$anchor, + f = function(app, anchor) { + gt::html(as.character(shiny::a( + href=paste0("https://github.com/rstudio/shinycoreci/tree/main/inst/apps/", app), + app + ))) + } +) +gt_dt$snap <- Map( + gt_dt$snap, + gt_dt$anchor, + f = function(snap, anchor) { + gt::html(as.character(shiny::a( + href=paste0("#", anchor), + snap + ))) + } +) +gt_dt %>% + gt() %>% + # gt::tab_header( + # title = "Latest shinytest2 snapshot diffs", + # subtitle = "Ordered by number of diffs" + # ) %>% + gt::cols_label( + app = "App", + snap = "Snap", + platform_combo = "Platform", + diff_count = "Diff count", + diff_img_w_anchor = "Diff file" + ) %>% + gt::tab_spanner( + label = "App", + columns = c(app, snap, platform_combo) + ) %>% + gt::tab_spanner( + label = "Diff", + columns = c(diff_count, diff_img_w_anchor) + ) %>% + gt::tab_options( + table.width = "100%", + column_labels.font.size = 14, + column_labels.font.weight = "bold", + heading.title.font.size = 18, + heading.title.font.weight = "bold", + heading.subtitle.font.size = 16, + heading.subtitle.font.weight = "bold" + ) %>% + # gt::tab_style( + # style = gt::cell_text(weight = "bold"), + # locations = gt::cells_body( + # columns = c(app, snap, platform_combo, diff_count, diff_file) + # ) + # ) %>% + # gt::tab_style( + # style = gt::cell_text(weight = "normal"), + # locations = gt::cells_body( + # columns = c(diff_count, diff_file) + # ) + # ) %>% + # gt::tab_style( + # style = gt::cell_text(weight = "normal"), + # locations = gt::cells_body( + # columns = c(app, snap, platform_combo) + # ) + # ) %>% + # gt::tab_style( + # style = gt::cell_text(weight = "normal"), + # locations = gt::cells_body( + # columns = c(diff_count, diff_img_w_anchor) + # ) + # ) %>% + # text_transform( + # locations = cells_body(c(diff_file)), + # fn = function(x) { + # # loop over the elements of the column + # Map( + # x, + # f = function(.x) { + + # shiny::a( + # href = + # local_image( + # filename = file.path("image_diffs", basename(.x)), + # height = 100 + # ) + # ) + # } + # ) + # } + # ) %>% + gt::cols_hide( + columns = c(anchor, platform, Rver, file, diff_file, diff_png, orig_png, new_png) + ) %>% + gt::cols_move_to_end( + columns = c(diff_count) + ) +``` + +```{r, results='asis', echo = FALSE} +shiny::tagList(Map( + seq_len(nrow(png_dt)), + png_dt$app, + png_dt$anchor, + png_dt$file, + png_dt$diff_count, + png_dt$diff_png, + png_dt$orig_png, + png_dt$new_png, + f = function(i, app, anchor, file, diff_count, diff_png, orig_png, new_png) { + shiny::tagList( + shiny::h1( + id= anchor, + shiny::a( + target="_blank", + href=paste0("https://github.com/rstudio/shinycoreci/tree/main/inst/apps/", app), + app + ), + shiny::code("/"), + shiny::a( + target="_blank", + href=paste0("https://github.com/rstudio/shinycoreci/tree/main/", file), + paste0(tail(strsplit(file, "/")[[1]], 3), collapse = "/") + ), + shiny::HTML(" — "), + shiny::code(diff_count) + ), + # shiny::p( + # "App: ", + # shiny::a( + # target="_blank", + # href=paste0("https://testing-apps.shinyapps.io/", app), + # "shinyapps.io" + # ) + # ), + # shiny::p( + # "Diff: ", + + # ), + shiny::div( + onclick="change_image(this);", + shiny::div("diff", style="position: absolute; bottom: 3px; right: 3px; color: grey;"), + shiny::img( + src = file.path("image_diffs", basename(diff_png)), + width = "100%", + style="border: 1px solid black;" + ) + ), + shiny::div( + onclick="change_image(this);", + shiny::div("old", style="position: absolute; bottom: 3px; right: 3px; color: grey;"), + shiny::img( + src = file.path("image_diffs", basename(orig_png)), + width = "100%", + style="border: 1px solid black;" + ) + ), + shiny::div( + onclick="change_image(this);", + shiny::div("new", style="position: absolute; bottom: 3px; right: 3px; color: grey;"), + shiny::img( + src = file.path("image_diffs", basename(new_png)), + width = "100%", + style="border: 1px solid black;" + ) + ) + ) + } +), +shiny::tags$script(shiny::HTML( + ' + function change_image(img) { + if (img.src.endsWidth(".new.png")) { + img.src = img.src.replace(".new.png", ".png"); + } else { + img.src = img.src.replace(".png", ".new.png"); + } + if (img.src.includes("diff")) { + img.src = img.src.replace("diff", "snap"); + } else { + img.src = img.src.replace("snap", "diff"); + } + } + ' +) ) +) +```