| # File src/library/tools/R/checktools.R |
| # Part of the R package, https://www.R-project.org |
| # |
| # Copyright (C) 2013-2018 The R Core Team |
| # |
| # This program is free software; you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation; either version 2 of the License, or |
| # (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| # GNU General Public License for more details. |
| # |
| # A copy of the GNU General Public License is available at |
| # https://www.R-project.org/Licenses/ |
| |
| ### ** check_packages_in_dir |
| |
| check_packages_in_dir <- |
| function(dir, |
| check_args = character(), check_args_db = list(), |
| reverse = NULL, |
| check_env = character(), |
| xvfb = FALSE, |
| Ncpus = getOption("Ncpus", 1L), |
| clean = TRUE, |
| ...) |
| { |
| owd <- getwd() |
| dir <- normalizePath(dir) |
| setwd(dir) |
| on.exit(setwd(owd)) |
| |
| .check_packages_in_dir_retval <- |
| function(dir, |
| pfiles, |
| pnames = character(), |
| rnames = character()) { |
| structure(pfiles, |
| dir = dir, |
| pnames = pnames, |
| rnames = rnames, |
| class = "check_packages_in_dir") |
| } |
| |
| pfiles <- Sys.glob("*.tar.gz") |
| if(!length(pfiles)) { |
| message("no packages to check") |
| return(.check_packages_in_dir_retval(dir, pfiles)) |
| } |
| |
| pnames <- sub("_.*", "", pfiles) |
| |
| os_type <- .Platform$OS.type |
| |
| ## Xvfb usage and options. |
| ## We do not use Xvfb on Windows. |
| ## Otherwise, if argument 'xvfb' is |
| ## * a logical, Xvfb is used only if identical to TRUE; |
| ## * something else, then as.character(xvfb) gives the Xvfb options. |
| xvfb_options <- "-screen 0 1280x1024x24" |
| if(os_type == "windows") |
| xvfb <- FALSE |
| else if(is.logical(xvfb)) { |
| if(!isTRUE(xvfb)) |
| xvfb <- FALSE |
| } else { |
| xvfb_options <- as.character(xvfb) |
| xvfb <- TRUE |
| } |
| |
| curl <- if(os_type == "windows") |
| sprintf("file:///%s", dir) |
| else |
| sprintf("file://%s", dir) |
| |
| libdir <- file.path(dir, "Library") |
| dir.create(libdir, showWarnings = FALSE) |
| outdir <- file.path(dir, "Outputs") |
| dir.create(outdir, showWarnings = FALSE) |
| |
| ## Determine packages using fake/no install for checking. |
| ## Handle these as follows: |
| ## * For packages using '--install=no', forward dependencies do not |
| ## need to installed, and reverse dependencies do not need to be |
| ## checked. |
| ## * For packages using '--install=fake', forward dependencies must |
| ## be available for checking, and checking reverse dependencies |
| ## makes sense (e.g, to spot missing Rd xrefs). |
| pnames_using_install_no <- character() |
| pnames_using_install_fake <- character() |
| check_args_db <- as.list(check_args_db) |
| if(length(check_args_db) && |
| !is.null(nms <- names(check_args_db))) { |
| args <- lapply(check_args_db, |
| function(e) |
| scan(text = e, what = character(), quiet = TRUE)) |
| pnames_using_install_no <- |
| nms[vapply(args, function(e) any(e == "--install=no"), NA)] |
| pnames_using_install_fake <- |
| nms[vapply(args, function(e) any(e == "--install=fake"), NA)] |
| } else { |
| ## If check_args_db has no names it is useless. |
| ## Perhaps complain? |
| check_args_db <- list() |
| } |
| |
| ## Build a package db from the source packages in the working |
| ## directory. |
| write_PACKAGES(dir, type = "source") |
| if(dir.exists(depdir <- file.path(dir, "Depends"))) { |
| write_PACKAGES(depdir, type = "source") |
| curl <- c(curl, paste0(curl, "/Depends")) |
| } |
| ## Determine packages available locally (for checking) and in the |
| ## repositories, and merge the information giving preference to the |
| ## former. |
| localones <- utils::available.packages(contriburl = curl, |
| type = "source") |
| curls <- utils::contrib.url(getOption("repos"), type = "source") |
| available <- utils::available.packages(contriburl = curls, |
| type = "source") |
| available <- rbind(localones, available) |
| available <- |
| available[!duplicated(available[, "Package"]), , drop = FALSE] |
| curls <- c(curl, curls) |
| |
| ## As of c52164, packages with OS_type different from the current |
| ## one are *always* checked with '--install=no'. |
| ## These packages are also filtered out by default (via the OS_type |
| ## filter) from the repository package computations. |
| ## Hence move packages in the install=fake list not listed by |
| ## available.packages() to the install=no list. |
| pnames_using_install_no <- |
| c(pnames_using_install_no, |
| setdiff(pnames_using_install_fake, available[, "Package"])) |
| pnames_using_install_fake <- |
| intersect(pnames_using_install_fake, available[, "Package"]) |
| |
| if(!is.null(reverse) && !isFALSE(reverse)) { |
| ## Determine and download reverse dependencies to be checked as |
| ## well. |
| |
| reverse <- as.list(reverse) |
| ## Merge with defaults, using partial name matching. |
| defaults <- list(which = c("Depends", "Imports", "LinkingTo"), |
| recursive = FALSE, |
| repos = getOption("repos")) |
| defaults0 <- defaults |
| pos <- pmatch(names(reverse), names(defaults), nomatch = 0L) |
| defaults[pos] <- reverse[pos > 0L] |
| |
| rnames <- if(is.list(defaults$which)) { |
| ## No recycling of repos for now. |
| defaults$recursive <- rep_len(as.list(defaults$recursive), |
| length(defaults$which)) |
| unlist(Map(function(w, r) |
| package_dependencies(setdiff(pnames, |
| pnames_using_install_no), |
| available, |
| which = w, |
| recursive = r, |
| reverse = TRUE), |
| defaults$which, |
| defaults$recursive), |
| use.names = FALSE) |
| } else { |
| package_dependencies(setdiff(pnames, |
| pnames_using_install_no), |
| available, |
| which = defaults$which, |
| recursive = defaults$recursive, |
| reverse = TRUE) |
| } |
| |
| add_recommended_maybe <- |
| config_val_to_logical(Sys.getenv("_R_TOOLS_C_P_I_D_ADD_RECOMMENDED_MAYBE_", |
| "FALSE")) |
| if(add_recommended_maybe) { |
| ## Add all recommended packages with any dependency on the |
| ## packages to be checked. |
| rnames <- |
| c(rnames, |
| names(Filter(length, |
| lapply(package_dependencies(.get_standard_package_names()$recommended, |
| available, |
| which = "all"), |
| intersect, |
| pnames)))) |
| } |
| |
| rnames <- intersect(unlist(rnames, use.names = FALSE), |
| available[, "Package"]) |
| rnames <- setdiff(rnames, pnames) |
| |
| pos <- match(rnames, available[, "Package"], nomatch = 0L) |
| if(!identical(defaults$repos, getOption("repos"))) { |
| pos <- split(pos[pos > 0L], available[pos, "Repository"]) |
| ## Only want the reverse dependencies for which Repository |
| ## starts with an entry in defaults$repos. |
| nms <- names(pos) |
| ind <- (rowSums(outer(nms, defaults$repos, startsWith)) > 0) |
| pos <- unlist(pos[ind], use.names = FALSE) |
| } |
| rnames <- available[pos, "Package"] |
| rfiles <- sprintf("%s_%s.tar.gz", |
| rnames, |
| available[pos, "Version"]) |
| |
| if(length(rfiles)) { |
| message("downloading reverse dependencies ...") |
| rfurls <- sprintf("%s/%s", |
| available[pos, "Repository"], |
| rfiles) |
| for(i in seq_along(rfiles)) { |
| message(sprintf("downloading %s ... ", rfiles[i]), |
| appendLF = FALSE) |
| status <- if(!utils::download.file(rfurls[i], rfiles[i], |
| quiet = TRUE)) |
| "ok" else "failed" |
| message(status) |
| } |
| message("") |
| } |
| |
| } else { |
| rfiles <- rnames <- character() |
| } |
| |
| pnames <- c(pnames, rnames) |
| |
| ## Install what is needed. |
| |
| if(xvfb) { |
| pid <- start_virtual_X11_fb(xvfb_options) |
| on.exit(close_virtual_X11_db(pid), add = TRUE) |
| } |
| |
| depends <- |
| package_dependencies(pnames, available, which = "most") |
| depends <- setdiff(unique(unlist(depends, use.names = FALSE)), |
| .get_standard_package_names()$base) |
| |
| ## Need to install depends which are not installed or installed but |
| ## old. |
| libs <- c(libdir, .libPaths()) |
| installed <- utils::installed.packages(libs)[, "Package"] |
| depends <- |
| c(setdiff(depends, installed), |
| intersect(intersect(depends, installed), |
| utils::old.packages(libs, |
| available = available)[, "Package"])) |
| if(length(depends)) { |
| message(paste(strwrap(sprintf("installing dependencies %s", |
| paste(sQuote(sort(depends)), |
| collapse = ", ")), |
| exdent = 2L), |
| collapse = "\n"), domain = NA) |
| ## <NOTE> |
| ## Ideally we would capture stdout and stderr in e.g. |
| ## outdir/install_stdout.txt |
| ## outdir/install_stderr.txt |
| ## But using several CPUs uses Make to install, which seems to |
| ## write to stdout/stderr "directly" ... so using sink() will |
| ## not work. Hence, use 'keep_outputs' to capture "outputs" |
| ## (combining install stdout and stderr into one file). |
| message("") |
| iflags <- as.list(rep.int("--fake", |
| length(pnames_using_install_fake))) |
| names(iflags) <- pnames_using_install_fake |
| tmpdir <- tempfile(tmpdir = outdir) |
| dir.create(tmpdir) |
| utils::install.packages(depends, lib = libdir, |
| contriburl = curls, |
| available = available, |
| dependencies = NA, |
| INSTALL_opts = iflags, |
| keep_outputs = tmpdir, |
| Ncpus = Ncpus, |
| type = "source") |
| outfiles <- Sys.glob(file.path(tmpdir, "*.out")) |
| file.rename(outfiles, |
| file.path(outdir, |
| sprintf("install_%s", |
| basename(outfiles)))) |
| unlink(tmpdir, recursive = TRUE) |
| message("") |
| ## </NOTE> |
| } |
| |
| ## Merge check_args and check_args_db into check_args_db used for |
| ## checking. |
| check_args <- if(is.list(check_args)) { |
| c(rep.int(list(check_args[[1L]]), length(pfiles)), |
| rep.int(list(check_args[[2L]]), length(rfiles))) |
| } else { |
| rep.int(list(check_args), length(pnames)) |
| } |
| check_args_db <- check_args_db[pnames] |
| check_args_db <- Map(c, check_args, check_args_db) |
| names(check_args_db) <- pnames |
| |
| check_env <- if(is.list(check_env)) { |
| c(rep.int(list(check_env[[1L]]), length(pfiles)), |
| rep.int(list(check_env[[2L]]), length(rfiles))) |
| } else { |
| rep.int(list(check_env), length(pnames)) |
| } |
| ## No user level check_env_db for now. |
| check_env_db <- as.list(check_env) |
| names(check_env_db) <- pnames |
| |
| pfiles <- c(pfiles, rfiles) |
| |
| check_package <- function(pfile, args_db = NULL, env_db = NULL) { |
| message(sprintf("checking %s ...", pfile)) |
| pname <- sub("_.*", "", basename(pfile)) |
| out <- file.path(outdir, |
| sprintf("check_%s_stdout.txt", pname)) |
| err <- file.path(outdir, |
| sprintf("check_%s_stderr.txt", pname)) |
| env <- c(check_env_db[[pname]], |
| sprintf("R_LIBS=%s", shQuote(libdir))) |
| lim <- get_timeout(Sys.getenv("_R_CHECK_ELAPSED_TIMEOUT_")) |
| system.time(system2(file.path(R.home("bin"), "R"), |
| c("CMD", |
| "check", |
| "--timings", |
| args_db[[pname]], |
| pfile), |
| stdout = out, |
| stderr = err, |
| env = env, |
| timeout = lim)) |
| } |
| |
| if(Ncpus > 1L) { |
| if(os_type != "windows") { |
| timings <- parallel::mclapply(pfiles, |
| check_package, |
| check_args_db, |
| check_env_db, |
| mc.cores = Ncpus) |
| } else { |
| cl <- parallel::makeCluster(Ncpus) |
| timings <- parallel::parLapply(cl, |
| pfiles, |
| check_package, |
| check_args_db, |
| check_env_db) |
| parallel::stopCluster(cl) |
| } |
| } else { |
| timings <- lapply(pfiles, |
| check_package, |
| check_args_db, |
| check_env_db) |
| } |
| |
| timings <- do.call(rbind, lapply(timings, summary)) |
| rownames(timings) <- pnames |
| utils::write.table(timings, "timings.tab") |
| |
| file.rename(sprintf("%s.Rcheck", rnames), |
| sprintf("rdepends_%s.Rcheck", rnames)) |
| |
| if(clean) { |
| file.remove(rfiles) |
| } else { |
| file.rename(rfiles, sprintf("rdepends_%s", rfiles)) |
| } |
| |
| .check_packages_in_dir_retval(dir, |
| pfiles, |
| setdiff(pnames, rnames), |
| rnames) |
| } |
| |
| ### ** print.check_packages_in_dir |
| |
| print.check_packages_in_dir <- |
| function(x, ...) |
| { |
| if(!length(x)) { |
| writeLines("No packages checked.") |
| return(invisible(x)) |
| } |
| |
| dir <- attr(x, "dir") |
| writeLines(c(strwrap(sprintf("Check results for packages in dir '%s':", |
| dir)), |
| sprintf("Package sources: %d, Reverse depends: %d", |
| length(attr(x, "pnames")), |
| length(attr(x, "rnames"))), |
| "Use summary() for more information.")) |
| invisible(x) |
| } |
| |
| ### ** summary.check_packages_in_dir |
| |
| summary.check_packages_in_dir <- |
| function(object, all = TRUE, full = FALSE, ...) |
| { |
| if(!length(object)) { |
| writeLines("No packages checked.") |
| return(invisible(object)) |
| } |
| |
| dir <- attr(object, "dir") |
| writeLines(c(strwrap(sprintf("Check results for packages in dir '%s':", |
| dir)), |
| "")) |
| details <- summarize_check_packages_in_dir_results(dir) |
| if(!full && details) { |
| writeLines("\nUse summary(full = TRUE) for details.") |
| } |
| invisible(object) |
| } |
| |
| ### ** start_virtual_X11_fb |
| |
| start_virtual_X11_fb <- |
| function(options) |
| { |
| ## Determine the display number from the options, or the PID of the |
| ## current R process (alternatively, could mimic xvfb-run). |
| args <- scan(text = options, what = character(), quiet = TRUE) |
| ind <- grepl("^:[[:digit:]]+$", args) |
| if(any(ind)) { |
| num <- args[ind][1L] |
| } else { |
| num <- paste0(":", Sys.getpid()) |
| options <- c(num, options) |
| } |
| |
| dis <- Sys.getenv("DISPLAY", unset = NA_character_) |
| |
| ## We need to start Xvfb with the given options and obtain its pid |
| ## so that we can terminate it when done checking. |
| ## This could be done via |
| ## system2("Xvfb", options, stdout = FALSE, stderr = FALSE, |
| ## wait = FALSE) |
| ## and then determine the pid as |
| ## pid <- scan(text = |
| ## grep(sprintf("Xvfb %s", num), |
| ## system2("ps", "auxw", stdout = TRUE), |
| ## value = TRUE, |
| ## fixed = TRUE), |
| ## what = character(), |
| ## quiet = TRUE)[2L] |
| ## A better approach (suggested by BDR) is to create a shell script |
| ## containing the call to start Xvfb in the background and display |
| ## the pid of this as available in the shell's $! parameter. |
| tf <- tempfile() |
| on.exit(unlink(tf)) |
| writeLines(c(paste(c(shQuote("Xvfb"), options, ">/dev/null 2>&1 &"), |
| collapse = " "), |
| "echo ${!}"), |
| tf) |
| pid <- system2("sh", tf, stdout = TRUE) |
| Sys.setenv("DISPLAY" = num) |
| |
| ## Propagate both pid and original setting of DISPLAY so that the |
| ## latter can be restored when Xvfb is closed. |
| attr(pid, "display") <- dis |
| pid |
| } |
| |
| ### ** close_virtual_X11_db |
| |
| close_virtual_X11_db <- |
| function(pid) |
| { |
| pskill(pid) |
| if(is.na(dis <- attr(pid, "display"))) |
| Sys.unsetenv("DISPLAY") |
| else |
| Sys.setenv("DISPLAY" = dis) |
| } |
| |
| ### ** R_check_outdirs |
| |
| R_check_outdirs <- |
| function(dir, all = FALSE, invert = FALSE) |
| { |
| dir <- normalizePath(dir) |
| outdirs <- dir(dir, pattern = "\\.Rcheck") |
| ind <- startsWith(basename(outdirs), "rdepends_") |
| ## Re-arrange to have reverse dependencies last if at all. |
| outdirs <- if(invert) |
| c(if(all) outdirs[!ind], outdirs[ind]) |
| else |
| c(outdirs[!ind], if(all) outdirs[ind]) |
| file.path(dir, outdirs) |
| } |
| |
| ### ** summarize_check_packages_in_dir_depends |
| |
| summarize_check_packages_in_dir_depends <- |
| function(dir, all = FALSE, which = c("Depends", "Imports", "LinkingTo")) |
| { |
| ## See tools::package_dependencies(): should perhaps separate out. |
| if(identical(which, "all")) |
| which <- c("Depends", "Imports", "LinkingTo", "Suggests", |
| "Enhances") |
| else if(identical(which, "most")) |
| which <- c("Depends", "Imports", "LinkingTo", "Suggests") |
| |
| for(d in R_check_outdirs(dir, all = all)) { |
| dfile <- Sys.glob(file.path(d, "00_pkg_src", "*", |
| "DESCRIPTION"))[1L] |
| if(file_test("-f", dfile)) { |
| meta <- .read_description(dfile) |
| package <- meta["Package"] |
| meta <- meta[match(which, names(meta), nomatch = 0L)] |
| if(length(meta)) { |
| writeLines(c(sprintf("Package: %s", package), |
| unlist(Map(function(tag, val) { |
| strwrap(sprintf("%s: %s", tag, val), |
| indent = 2L, exdent = 4L) |
| }, |
| names(meta), |
| meta)))) |
| } |
| } |
| } |
| |
| invisible() |
| } |
| |
| ### ** summarize_check_packages_in_dir_results |
| |
| summarize_check_packages_in_dir_results <- |
| function(dir, all = TRUE, full = FALSE, ...) |
| { |
| dir <- normalizePath(dir) |
| outdirs <- R_check_outdirs(dir, all = all) |
| logs <- file.path(outdirs, "00check.log") |
| logs <- logs[file_test("-f", logs)] |
| |
| results <- check_packages_in_dir_results(logs = logs, ...) |
| |
| writeLines("Check status summary:") |
| tab <- check_packages_in_dir_results_summary(results) |
| rownames(tab) <- paste0(" ", rownames(tab)) |
| print(tab) |
| writeLines("") |
| |
| writeLines("Check results summary:") |
| Map(function(p, r) { |
| writeLines(c(sprintf("%s ... %s", p, r$status), r$lines)) |
| }, |
| names(results), |
| results) |
| |
| if(full && |
| !all(as.character(unlist(lapply(results, `[[`, "status"))) == |
| "OK")) { |
| writeLines(c("", "Check results details:")) |
| details <- check_packages_in_dir_details(logs = logs, ...) |
| writeLines(paste(format(details), collapse = "\n\n")) |
| invisible(TRUE) |
| } else { |
| invisible(FALSE) |
| } |
| } |
| |
| ### ** summarize_check_packages_in_dir_timings |
| |
| summarize_check_packages_in_dir_timings <- |
| function(dir, all = FALSE, full = FALSE) |
| { |
| dir <- normalizePath(dir) |
| tfile <- file.path(dir, "timings.tab") |
| if(file_test("-f", tfile)) { |
| timings <- utils::read.table(tfile) |
| ## Should we store the information about reverse dependencies in |
| ## some place (rather than rely on the naming convention)? |
| if(!all) { |
| rdepends <- Sys.glob(file.path(dir, "rdepends_*.Rcheck")) |
| timings <- timings[is.na(match(rownames(timings), |
| sub("rdepends_(.*).Rcheck", |
| "\\1", |
| basename(rdepends)))), |
| ] |
| } |
| print(timings) |
| } |
| if(full) { |
| tfiles <- Sys.glob(file.path(R_check_outdirs(dir, all = all), |
| "*-Ex.timings")) |
| if(length(tfiles)) message("") |
| timings <- lapply(tfiles, utils::read.table, header = TRUE) |
| ## Order by CPU time. |
| timings <- lapply(timings, |
| function(x) |
| x[order(x$user, decreasing = TRUE), ]) |
| ## This looks silly, but we want a common alignment. |
| timings <- split(as.data.frame(lapply(do.call(rbind, timings), |
| format)), |
| rep.int(sub("\\.Rcheck$", "", |
| basename(dirname(tfiles))), |
| vapply(timings, nrow, 0L))) |
| invisible(Map(function(x, y) { |
| writeLines(sprintf("Example timings for package '%s':", x)) |
| cat(rbind(" ", t(as.matrix(y))), |
| sep = c(" ", " ", " ", " ", "\n")) |
| }, |
| names(timings), timings)) |
| } |
| |
| invisible() |
| } |
| |
| ### ** check_packages_in_dir_results |
| |
| ## <FIXME> |
| ## For new-style logs from successful check runs (a '* DONE' line |
| ## followed by a 'Status: ' line), we could simply get the status from |
| ## the 'Status: ' line. |
| ## Change to preferably rely on the new format eventually. |
| ## Note that check logs can end up incomplete in which case there is no |
| ## final status line ... |
| ## </FIXME> |
| |
| check_packages_in_dir_results <- |
| function(dir, logs = NULL, ...) |
| { |
| if(is.null(logs)) |
| logs <- Sys.glob(file.path(dir, "*.Rcheck", "00check.log")) |
| |
| ## <NOTE> |
| ## Perhaps make the individual non-OK check values more readily |
| ## available? |
| ## </NOTE> |
| |
| results <- lapply(logs, function(log, ...) { |
| lines <- read_check_log(log, ...) |
| ## See analyze_lines() inside analyze_check_log(): |
| re <- "^\\* (loading checks for arch|checking (examples|tests) \\.\\.\\.$)" |
| pos <- grep(re, lines, perl = TRUE, useBytes = TRUE) |
| if(length(pos <- pos[pos < length(lines)])) |
| lines <- lines[-pos] |
| re <- "^\\*\\*? ((checking|creating|running examples for arch|running tests for arch) .*) \\.\\.\\.( (\\[[^ ]*\\]))?( (NOTE|WARNING|ERROR)|)$" |
| m <- regexpr(re, lines, perl = TRUE, useBytes = TRUE) |
| ind <- (m > 0L) |
| ## Note that we use WARN instead of WARNING for the summary. |
| status <- |
| if(any(ind)) { |
| status <- sub(re, "\\6", lines[ind], |
| perl = TRUE, useBytes = TRUE) |
| if(any(status == "")) "FAIL" |
| else if(any(status == "ERROR")) "ERROR" |
| else if(any(status == "WARNING")) "WARN" |
| else "NOTE" |
| } else { |
| "OK" |
| } |
| list(status = status, lines = lines[ind]) |
| }, ...) |
| names(results) <- sub("\\.Rcheck$", "", basename(dirname(logs))) |
| |
| results |
| } |
| |
| ### ** check_packages_in_dir_results_summary |
| |
| check_packages_in_dir_results_summary <- |
| function(results) |
| { |
| if(!length(results)) return() |
| status <- vapply(results, `[[`, "", "status") |
| ind <- startsWith(names(results), "rdepends_") |
| tab <- table(ifelse(ind, "Reverse depends", "Source packages"), |
| status, deparse.level = 0L) |
| tab <- tab[match(c("Source packages", "Reverse depends"), |
| rownames(tab), nomatch = 0L), |
| match(c("FAIL", "ERROR", "WARN", "NOTE", "OK"), |
| colnames(tab), nomatch = 0L), |
| drop = FALSE] |
| names(dimnames(tab)) <- NULL |
| tab |
| } |
| |
| ### ** read_check_log |
| |
| read_check_log <- |
| function(log, drop = TRUE, ...) |
| { |
| lines <- readLines(log, warn = FALSE, ...) |
| |
| if(drop) { |
| ## Drop CRAN check status footer. |
| ## Ideally, we would have a more general mechanism to detect |
| ## footer information to be skipped (e.g., a line consisting of |
| ## a single non-printing control character?) |
| pos <- grep("^Current CRAN status:", lines, |
| perl = TRUE, useBytes = TRUE) |
| if(length(pos) && lines[pos <- (pos[1L] - 1L)] == "") { |
| lines <- lines[seq_len(pos - 1L)] |
| } |
| } |
| |
| ## <FIXME> |
| ## Remove eventually. |
| len <- length(lines) |
| end <- lines[len] |
| if(length(end) && |
| grepl(re <- "^(\\*.*\\.\\.\\.)(\\* elapsed time.*)$", end, |
| perl = TRUE, useBytes = TRUE)) { |
| lines <- c(lines[seq_len(len - 1L)], |
| sub(re, "\\1", end, perl = TRUE, useBytes = TRUE), |
| sub(re, "\\2", end, perl = TRUE, useBytes = TRUE)) |
| } |
| ## </FIXME |
| |
| lines |
| } |
| |
| ### ** analyze_check_log |
| |
| ## <FIXME> |
| ## New-style check logs should have a '* DONE' line followed by a |
| ## 'Status:' line. If not, a check failure occurred. |
| ## Change to fully rely on the new format eventually. |
| ## </FIXME> |
| |
| analyze_check_log <- |
| function(log, drop_ok = TRUE, ...) |
| { |
| make_results <- function(package, version, flags, chunks) |
| list(Package = package, Version = version, |
| Flags = flags, Chunks = chunks) |
| |
| ## Alternatives for left and right quotes. |
| lqa <- paste0("'|", intToUtf8(0x2018)) |
| rqa <- paste0("'|", intToUtf8(0x2019)) |
| ## Group when used ... |
| |
| if(is.character(drop_ok)) { |
| drop_ok_status_tags <- drop_ok |
| drop_ok <- TRUE |
| } else { |
| drop_ok_status_tags <- c("OK", "NONE", "SKIPPED") |
| } |
| |
| ## Start by reading in. |
| lines <- read_check_log(log, ...) |
| |
| ## Re-encode to UTF-8 using the session charset info. |
| ## All regexp computations will be done using perl = TRUE and |
| ## use useBytes = TRUE for matching and extracting ASCII content. |
| re <- "^\\* using session charset: " |
| pos <- grep(re, lines, perl = TRUE, useBytes = TRUE) |
| if(length(pos)) { |
| enc <- sub(re, "", lines[pos[1L]]) |
| lines <- iconv(lines, enc, "UTF-8", sub = "byte") |
| ## If the check log uses ASCII, there should be no non-ASCII |
| ## characters in the message lines: could check for this. |
| if(any(bad <- !validEnc(lines))) |
| lines[bad] <- iconv(lines[bad], to = "ASCII", sub = "byte") |
| } else { |
| ## In case of a fundamental immediate problem which renders |
| ## further checking pointless, we currently do not provide the |
| ## header information with the session charset. (Perhaps this |
| ## should be changed.) |
| if(!any(grepl("^\\* checking ", lines, |
| perl = TRUE, useBytes = TRUE))) |
| return() |
| } |
| |
| package <- "???" |
| version <- "" |
| |
| ## Get header. |
| header <- lines |
| re <- sprintf("^\\* this is package (%s)(.*)(%s) version (%s)(.*)(%s)$", |
| lqa, rqa, lqa, rqa) |
| pos <- grep(re, lines, perl = TRUE, useBytes = TRUE) |
| if(length(pos)) { |
| pos <- pos[1L] |
| txt <- lines[pos] |
| package <- sub(re, "\\2", txt, perl = TRUE, useBytes = TRUE) |
| version <- sub(re, "\\5", txt, perl = TRUE, useBytes = TRUE) |
| header <- lines[seq_len(pos - 1L)] |
| lines <- lines[-seq_len(pos)] |
| } else { |
| ## If there was no 'this is package %s version %s' line, then |
| ## either there was a fundamental immediate problem, or an error |
| ## in check_description(). In the latter case there should be a |
| ## line like |
| ## * checking for file '%s/DESCRIPTION' |
| ## with %s the package name implied by the invocation, but not |
| ## necessarily the one recorded in DESCRIPTION: let's use that |
| ## package name nevertheless, as it is better than nothing. |
| re <- sprintf("^\\* checking for file (%s)(.*)/DESCRIPTION(%s).*$", |
| lqa, rqa) |
| pos <- grep(re, lines, perl = TRUE, useBytes = TRUE) |
| if(length(pos)) { |
| pos <- pos[1L] |
| txt <- lines[pos] |
| package <- sub(re, "\\2", txt, perl = TRUE, useBytes = TRUE) |
| header <- lines[seq_len(pos - 1L)] |
| } else if(!any(grepl("^\\* checking ", lines, |
| perl = TRUE, useBytes = TRUE))) |
| return() |
| } |
| ## Get check options from header. |
| re <- sprintf("^\\* using options? (%s)(.*)(%s)$", lqa, rqa) |
| flags <- |
| if(length(pos <- grep(re, header, |
| perl = TRUE, useBytes = TRUE))) { |
| sub(re, "\\2", header[pos[1L]], |
| perl = TRUE, useBytes = TRUE) |
| } else "" |
| |
| ## Get footer. |
| len <- length(lines) |
| pos <- which(lines == "* DONE") |
| if(length(pos) && |
| ((pos <- pos[length(pos)]) < len) && |
| startsWith(lines[pos + 1L], "Status: ")) |
| lines <- lines[seq_len(pos - 1L)] |
| else { |
| ## Not really new style, or failure ... argh. |
| ## Some check systems explicitly record the elapsed time in the |
| ## last line: |
| if(grepl("^\\* elapsed time ", lines[len], |
| perl = TRUE, useBytes = TRUE)) { |
| lines <- lines[-len] |
| len <- len - 1L |
| while(grepl("^[[:space:]]*$", lines[len])) { |
| lines <- lines[-len] |
| len <- len - 1L |
| } |
| } |
| ## Summary footers. |
| if(startsWith(lines[len], "Status: ")) { |
| ## New-style status summary. |
| lines <- lines[-len] |
| len <- len - 1L |
| } else { |
| ## Old-style status summary. |
| num <- length(grep("^(NOTE|WARNING): There", |
| lines[c(len - 1L, len)])) |
| if(num > 0L) { |
| pos <- seq.int(len - num + 1L, len) |
| lines <- lines[-pos] |
| len <- len - num |
| } |
| } |
| if(lines[len] == "* DONE") |
| lines <- lines[-len] |
| } |
| |
| analyze_lines <- function(lines) { |
| ## Windows has |
| ## * loading checks for arch |
| ## * checking examples ... |
| ## * checking tests ... |
| ## headers: drop these (unless in the last line, where they |
| ## indicate failure). |
| re <- "^\\* (loading checks for arch|checking (examples|tests) \\.\\.\\.$)" |
| pos <- grep(re, lines, perl = TRUE, useBytes = TRUE) |
| if(length(pos <- pos[pos < length(lines)])) |
| lines <- lines[-pos] |
| ## We might still have |
| ## * package encoding: |
| ## entries for packages declaring a package encoding. |
| ## Hopefully all other log entries we still have are |
| ## * checking |
| ## * creating |
| ## ones ... apparently, with the exception of |
| ## ** running examples for arch |
| ## ** running tests for arch |
| ## So let's drop everything up to the first such entry. |
| re <- "^\\*\\*? ((checking|creating|running examples for arch|running tests for arch) .*) \\.\\.\\.( (\\[[^ ]*\\]))?( (.*)|)$" |
| ind <- grepl(re, lines, perl = TRUE, useBytes = TRUE) |
| csi <- cumsum(ind) |
| ind <- (csi > 0) |
| chunks <- |
| lapply(split(lines[ind], csi[ind]), |
| function(s) { |
| ## Note that setting |
| ## _R_CHECK_TEST_TIMING_=yes |
| ## _R_CHECK_VIGNETTE_TIMING_=yes |
| ## will result in a different chunk format ... |
| line <- s[1L] |
| check <- sub(re, "\\1", line, |
| perl = TRUE, useBytes = TRUE) |
| status <- sub(re, "\\6", line, |
| perl = TRUE, useBytes = TRUE) |
| if(status == "") status <- "FAIL" |
| list(check = check, |
| status = status, |
| output = paste(s[-1L], collapse = "\n")) |
| }) |
| |
| status <- vapply(chunks, `[[`, "", "status") |
| if(isTRUE(drop_ok) || |
| (is.na(drop_ok) |
| && all(is.na(match(c("ERROR", "FAIL"), status))))) |
| chunks <- chunks[is.na(match(status, drop_ok_status_tags))] |
| |
| chunks |
| } |
| |
| chunks <- analyze_lines(lines) |
| if(!length(chunks) && !isFALSE(drop_ok)) { |
| chunks <- list(list(check = "*", status = "OK", output = "")) |
| } |
| |
| make_results(package, version, flags, chunks) |
| } |
| |
| ### ** check_packages_in_dir_details |
| |
| check_packages_in_dir_details <- |
| function(dir, logs = NULL, drop_ok = TRUE, ...) |
| { |
| ## Build a data frame with columns |
| ## Package Version Check Status Output Flags |
| ## and some optimizations (in particular, Check Status Flags can be |
| ## factors). |
| |
| db_from_logs <- function(logs, drop_ok, ...) { |
| out <- lapply(logs, analyze_check_log, drop_ok, ...) |
| out <- out[lengths(out) > 0L] |
| if(!length(out)) |
| return(matrix(character(), ncol = 6L)) |
| chunks <- lapply(out, `[[`, "Chunks") |
| package <- sapply(out, `[[`, "Package") |
| lens <- lengths(chunks) |
| cbind(rep.int(package, lens), |
| rep.int(sapply(out, `[[`, "Version"), lens), |
| matrix(as.character(unlist(chunks)), ncol = 3L, |
| byrow = TRUE), |
| rep.int(sapply(out, `[[`, "Flags"), |
| lens)) |
| } |
| |
| if(is.null(logs)) { |
| if(inherits(dir, "check_packages_in_dir")) |
| dir <- attr(dir, "dir") |
| logs <- Sys.glob(file.path(dir, "*.Rcheck", "00check.log")) |
| } |
| |
| db <- db_from_logs(logs, drop_ok, ...) |
| colnames(db) <- c("Package", "Version", "Check", "Status", |
| "Output", "Flags") |
| |
| ## Now some cleanups. |
| |
| ## Alternatives for left and right quotes. |
| lqa <- paste0("'|", intToUtf8(0x2018)) |
| rqa <- paste0("'|", intToUtf8(0x2019)) |
| ## Group when used ... |
| |
| checks <- db[, "Check"] |
| checks <- sub(sprintf("checking whether package (%s).*(%s) can be installed", |
| lqa, rqa), |
| "checking whether package can be installed", |
| checks, perl = TRUE, useBytes = TRUE) |
| checks <- sub("creating .*-Ex.R", "checking examples creation", |
| checks, perl = TRUE, useBytes = TRUE) |
| checks <- sub("creating .*-manual\\.tex", "checking manual creation", |
| checks, perl = TRUE, useBytes = TRUE) |
| checks <- sub("checking .*-manual\\.tex", "checking manual", |
| checks, perl = TRUE, useBytes = TRUE) |
| checks <- sub(sprintf("checking package vignettes in (%s)inst/doc(%s)", |
| lqa, rqa), |
| "checking package vignettes", |
| checks, perl = TRUE, useBytes = TRUE) |
| checks <- sub("^checking *", "", |
| checks, perl = TRUE, useBytes = TRUE) |
| db[, "Check"] <- checks |
| ## In fact, for tabulation purposes it would even be more convenient |
| ## to shorten the check names ... |
| |
| db[, "Output"] <- |
| sub("[[:space:]]+$", "", db[, "Output"], perl = TRUE) |
| |
| db <- as.data.frame(db, stringsAsFactors = FALSE) |
| db$Check <- as.factor(db$Check) |
| db$Status <- as.factor(db$Status) |
| |
| class(db) <- c("check_details", "data.frame") |
| db |
| } |
| |
| format.check_details <- |
| function(x, ...) |
| { |
| flags <- x$Flags |
| flavor <- x$Flavor |
| paste0(sprintf("Package: %s %s\n", |
| x$Package, x$Version), |
| ifelse(nzchar(flavor), |
| sprintf("Flavor: %s\n", flavor), |
| ""), |
| ifelse(nzchar(flags), |
| sprintf("Flags: %s\n", flags), |
| ""), |
| sprintf("Check: %s, Result: %s\n", |
| x$Check, x$Status), |
| sprintf(" %s", |
| gsub("\n", "\n ", x$Output, perl = TRUE)) |
| ) |
| } |
| |
| print.check_details <- |
| function(x, ...) |
| { |
| writeLines(paste(format(x, ...), collapse = "\n\n")) |
| invisible(x) |
| } |
| |
| ### ** check_packages_in_dir_changes |
| |
| check_packages_in_dir_changes <- |
| function(dir, old, outputs = FALSE, sources = FALSE, ...) |
| { |
| dir <- if(inherits(dir, "check_packages_in_dir")) |
| dir <- attr(dir, "dir") |
| else |
| normalizePath(dir) |
| |
| outdirs <- R_check_outdirs(dir, all = sources, invert = TRUE) |
| logs <- file.path(outdirs, "00check.log") |
| logs <- logs[file_test("-f", logs)] |
| new <- check_packages_in_dir_details(logs = logs, drop_ok = FALSE, ...) |
| |
| ## Use |
| ## old = tools:::CRAN_check_details(FLAVOR) |
| ## to compare against the results/details of a CRAN check flavor. |
| |
| if(!inherits(old, "check_details")) |
| old <- check_packages_in_dir_details(old, drop_ok = FALSE, ...) |
| |
| check_details_changes(new, old, outputs) |
| } |
| |
| ### ** check_details_changes |
| |
| check_details_changes <- |
| function(new, old, outputs = FALSE) |
| { |
| check_details_changes_classes <- |
| c("check_details_changes", "data.frame") |
| |
| if(!inherits(new, "check_details")) stop("wrong class") |
| if(!inherits(old, "check_details")) stop("wrong class") |
| |
| ## Simplify matters by considering only "changes" in *available* |
| ## results/details. |
| |
| packages <- intersect(old$Package, new$Package) |
| |
| if(!length(packages)) { |
| db <- data.frame(Package = character(), |
| Check = character(), |
| Old = character(), |
| New = character(), |
| stringsAsFactors = FALSE) |
| class(db) <- check_details_changes_classes |
| return(db) |
| } |
| |
| db <- merge(old[!is.na(match(old$Package, packages)), ], |
| new[!is.na(match(new$Package, packages)), ], |
| by = c("Package", "Check"), all = TRUE) |
| |
| ## Complete possibly missing version information. |
| chunks <- |
| lapply(split(db, db$Package), |
| function(e) { |
| len <- nrow(e) |
| if(length(pos <- which(!is.na(e$Version.x)))) |
| e$Version.x <- |
| rep.int(e[pos[1L], "Version.x"], len) |
| if(length(pos <- which(!is.na(e$Version.y)))) |
| e$Version.y <- |
| rep.int(e[pos[1L], "Version.y"], len) |
| e |
| }) |
| db <- do.call(rbind, chunks) |
| |
| ## Drop checks that are OK in both versions |
| x.issue <- !is.na(match(db$Status.x, |
| c("ERROR","FAIL","NOTE","WARNING"))) |
| y.issue <- !is.na(match(db$Status.y, |
| c("ERROR","FAIL","NOTE","WARNING"))) |
| db <- db[x.issue | y.issue,] |
| |
| ## Even with the above simplification, missing entries do not |
| ## necessarily indicate "OK" (checks could have been skipped). |
| ## Hence leave as missing and show as empty in the diff. |
| ## An exception to this rule is made if we find an "ERROR" result |
| ## as this may explain skipped checks. |
| |
| sx <- as.character(db$Status.x) |
| sy <- as.character(db$Status.y) |
| if(outputs) { |
| ind <- nzchar(ox <- db$Output.x) |
| sx[ind] <- sprintf("%s\n %s", sx[ind], |
| gsub("\n", "\n ", ox[ind], fixed = TRUE)) |
| ind <- nzchar(oy <- db$Output.y) |
| sy[ind] <- sprintf("%s\n %s", sy[ind], |
| gsub("\n", "\n ", oy[ind], fixed = TRUE)) |
| } |
| sx[is.na(db$Status.x)] <- "" |
| sy[is.na(db$Status.y)] <- "" |
| ind <- if(outputs) |
| (.canonicalize_quotes(sx) != .canonicalize_quotes(sy)) |
| else |
| (sx != sy) |
| |
| db <- cbind(db[ind, ], Old = sx[ind], New = sy[ind], |
| stringsAsFactors = FALSE) |
| |
| ## Add information about possible version changes. |
| ind <- (db$Version.x != db$Version.y) |
| if(any(ind)) |
| db$Package[ind] <- |
| sprintf("%s [Old version: %s, New version: %s]", |
| db$Package[ind], |
| db$Version.x[ind], |
| db$Version.y[ind]) |
| |
| db <- db[c("Package", "Check", "Old", "New")] |
| |
| class(db) <- check_details_changes_classes |
| |
| db |
| } |
| |
| `[.check_details_changes` <- |
| function(x, i, j, drop = FALSE) |
| { |
| if(((na <- nargs() - (!missing(drop))) == 3L) |
| && (length(i) == 1L) |
| && any(!is.na(match(i, c("==", "!=", "<", "<=", ">", ">="))))) { |
| levels <- c("", "OK", "NOTE", "WARNING", "ERROR", "FAIL") |
| encode <- function(s) { |
| s <- sub("\n.*", "", s) |
| s[is.na(match(s, levels))] <- "" |
| ordered(s, levels) |
| } |
| old <- encode(x$Old) |
| new <- encode(x$New) |
| i <- do.call(i, list(old, new)) |
| } |
| NextMethod() |
| } |
| |
| format.check_details_changes <- |
| function(x, ...) |
| { |
| if(!nrow(x)) return(character()) |
| sprintf("Package: %s\nCheck: %s%s%s", |
| x$Package, |
| x$Check, |
| ifelse(nzchar(old <- x$Old), |
| sprintf("\nOld result: %s", old), |
| ""), |
| ifelse(nzchar(new <- x$New), |
| sprintf("\nNew result: %s", new), |
| "")) |
| } |
| |
| print.check_details_changes <- |
| function(x, ...) |
| { |
| if(length(y <- format(x))) |
| writeLines(paste(y, collapse = "\n\n")) |
| invisible(x) |
| } |
| |
| ### Local variables: *** |
| ### mode: outline-minor *** |
| ### outline-regexp: "### [*]+" *** |
| ### End: *** |