1# Copyright (C) 2021 Brodie Gaslam 2# 3# This file is part of "diffobj - Diffs for R Objects" 4# 5# This program is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 2 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# Go to <https://www.r-project.org/Licenses/GPL-2> for a copy of the license. 16 17# Capture output of print/show/str; unfortunately doesn't have superb handling 18# of errors during print/show call, though hopefully these are rare 19# 20# x is a quoted call to evaluate 21 22capture <- function(x, etc, err) { 23 capt.width <- etc@text.width 24 if(capt.width) { 25 opt.set <- try(width.old <- options(width=capt.width), silent=TRUE) 26 if(inherits(opt.set, "try-error")) { 27 warning( 28 "Unable to set desired width ", capt.width, ", (", 29 conditionMessage(attr(opt.set, "condition")), ");", 30 "proceeding with existing setting." 31 ) 32 } else on.exit(options(width.old)) 33 } 34 # Note, we use `tempfile` for capture as that appears much faster than normal 35 # capture without a file 36 37 capt.file <- tempfile() 38 on.exit(unlink(capt.file), add=TRUE) 39 res <- try({ 40 capture.output(eval(x, etc@frame), file=capt.file) 41 obj.out <- readLines(capt.file) 42 }) 43 if(inherits(res, "try-error")) 44 err( 45 "Failed attempting to get text representation of object: ", 46 conditionMessage(attr(res, "condition")) 47 ) 48 html_ent_sub(res, etc@style) 49} 50# capture normal prints, along with default prints to make sure that if we 51# do try to wrap an atomic vector print it is very likely to be in a format 52# we are familiar with and not affected by a non-default print method 53 54capt_print <- function(target, current, etc, err, extra){ 55 dots <- extra 56 # What about S4? 57 if(getRversion() >= "3.2.0") { 58 print.match <- try( 59 match.call( 60 get("print", envir=etc@frame, mode='function'), 61 as.call(c(list(quote(print), x=NULL), dots)), 62 envir=etc@frame 63 ) ) 64 } else { 65 # this may be sub-optimal, but match.call does not support the envir arg 66 # prior to this 67 # nocov start 68 print.match <- try( 69 match.call( 70 get("print", envir=etc@frame), 71 as.call(c(list(quote(print), x=NULL), dots)) 72 ) ) 73 # nocov end 74 } 75 if(inherits(print.match, "try-error")) 76 err("Unable to compose `print` call") 77 78 names(print.match)[[2L]] <- "" 79 tar.call <- cur.call <- print.match 80 81 if(length(dots)) { 82 if(!is.null(etc@tar.exp)) tar.call[[2L]] <- etc@tar.exp 83 if(!is.null(etc@cur.exp)) cur.call[[2L]] <- etc@cur.exp 84 etc@tar.banner <- deparse(tar.call)[[1L]] 85 etc@cur.banner <- deparse(cur.call)[[1L]] 86 } 87 tar.call.q <- if(is.call(target) || is.symbol(target)) 88 call("quote", target) else target 89 cur.call.q <- if(is.call(current) || is.symbol(current)) 90 call("quote", current) else current 91 92 if(!is.null(target)) tar.call[[2L]] <- tar.call.q 93 if(!is.null(current)) cur.call[[2L]] <- cur.call.q 94 95 # If dimensioned object, and in auto-mode, switch to side by side if stuff is 96 # narrow enough to fit 97 98 if((!is.null(dim(target)) || !is.null(dim(current)))) { 99 cur.capt <- capture(cur.call, etc, err) 100 tar.capt <- capture(tar.call, etc, err) 101 etc <- set_mode(etc, tar.capt, cur.capt) 102 } else { 103 etc <- if(etc@mode == "auto") sideBySide(etc) else etc 104 cur.capt <- capture(cur.call, etc, err) 105 tar.capt <- capture(tar.call, etc, err) 106 } 107 if(isTRUE(etc@guides)) etc@guides <- guidesPrint 108 if(isTRUE(etc@trim)) etc@trim <- trimPrint 109 110 diff.out <- line_diff(target, current, tar.capt, cur.capt, etc=etc, warn=TRUE) 111 diff.out@capt.mode <- "print" 112 diff.out 113} 114# Tries various different `str` settings to get the best possible output 115 116capt_str <- function(target, current, etc, err, extra){ 117 # Match original call and managed dots, in particular wrt to the 118 # `max.level` arg 119 dots <- extra 120 frame <- etc@frame 121 line.limit <- etc@line.limit 122 if("object" %in% names(dots)) 123 err("You may not specify `object` as part of `extra`") 124 125 if(getRversion() < "3.2.0") { 126 # nocov start 127 str.match <- match.call( 128 str_tpl, 129 call=as.call(c(list(quote(str), object=NULL), dots)) 130 ) 131 # nocov end 132 } else { 133 str.match <- match.call( 134 str_tpl, 135 call=as.call(c(list(quote(str), object=NULL), dots)), envir=etc@frame 136 ) 137 } 138 names(str.match)[[2L]] <- "" 139 140 # Handle auto mode (side by side always for `str`) 141 142 if(etc@mode == "auto") etc <- sideBySide(etc) 143 144 # Utility function; defining in body so it has access to `err` 145 146 eval_try <- function(match.list, index, envir) 147 tryCatch( 148 eval(match.list[[index]], envir=envir), 149 error=function(e) 150 err("Error evaluating `", index, "` arg: ", conditionMessage(e)) 151 ) 152 # Setup / process extra args 153 154 auto.mode <- FALSE 155 max.level.supplied <- FALSE 156 if( 157 max.level.pos <- match("max.level", names(str.match), nomatch=0L) 158 ) { 159 # max.level specified in call; check for special 'auto' case 160 max.level.eval <- eval_try(str.match, "max.level", etc@frame) 161 if(identical(max.level.eval, "auto")) { 162 auto.mode <- TRUE 163 str.match[["max.level"]] <- NA 164 } else { 165 max.level.supplied <- TRUE 166 } 167 } else { 168 str.match[["max.level"]] <- NA 169 auto.mode <- TRUE 170 max.level.pos <- length(str.match) 171 max.level.supplied <- FALSE 172 } 173 # Was wrap specified in strict width mode? Not sure this is correct any more; 174 # should probably be looking at extra args. 175 176 wrap <- FALSE 177 if("strict.width" %in% names(str.match)) { 178 res <- eval_try(str.match, "strict.width", etc@frame) 179 wrap <- is.character(res) && length(res) == 1L && !is.na(res) && 180 nzchar(res) && identical(res, substr("wrap", 1L, nchar(res))) 181 } 182 if(auto.mode) { 183 msg <- 184 "Specifying `%s` may cause `str` output level folding to be incorrect" 185 if("comp.str" %in% names(str.match)) warning(sprintf(msg, "comp.str")) 186 if("indent.str" %in% names(str.match)) warning(sprintf(msg, "indent.str")) 187 } 188 # don't want to evaluate target and current more than once, so can't eval 189 # tar.exp/cur.exp, so instead run call with actual object 190 191 tar.call <- cur.call <- str.match 192 193 tar.call.q <- if(is.call(target) || is.symbol(target)) 194 call("quote", target) else target 195 cur.call.q <- if(is.call(current) || is.symbol(current)) 196 call("quote", current) else current 197 198 if(!is.null(target)) tar.call[[2L]] <- tar.call.q 199 if(!is.null(current)) cur.call[[2L]] <- cur.call.q 200 201 # Run str 202 203 capt.width <- etc@text.width 204 has.diff <- has.diff.prev <- FALSE 205 206 # we used to strip_hz_control here, but shouldn't have to since handled by 207 # line_diff 208 209 tar.capt <- capture(tar.call, etc, err) 210 tar.lvls <- str_levels(tar.capt, wrap=wrap) 211 cur.capt <- capture(cur.call, etc, err) 212 cur.lvls <- str_levels(cur.capt, wrap=wrap) 213 214 prev.lvl.hi <- lvl <- max.depth <- max(tar.lvls, cur.lvls) 215 prev.lvl.lo <- 0L 216 first.loop <- TRUE 217 safety <- 0L 218 warn <- TRUE 219 220 if(isTRUE(etc@guides)) etc@guides <- guidesStr 221 if(isTRUE(etc@trim)) etc@trim <- trimStr 222 223 tar.str <- tar.capt 224 cur.str <- cur.capt 225 226 diff.obj <- diff.obj.full <- line_diff( 227 target, current, tar.str, cur.str, etc=etc, warn=warn 228 ) 229 if(!max.level.supplied) { 230 repeat{ 231 if((safety <- safety + 1L) > max.depth && !first.loop) 232 # nocov start 233 stop( 234 "Logic Error: exceeded list depth when comparing structures; contact ", 235 "maintainer." 236 ) 237 # nocov end 238 if(!first.loop) { 239 tar.str <- tar.capt[tar.lvls <= lvl] 240 cur.str <- cur.capt[cur.lvls <= lvl] 241 242 diff.obj <- line_diff( 243 target, current, tar.str, cur.str, etc=etc, warn=warn 244 ) 245 } 246 if(diff.obj@hit.diffs.max) warn <- FALSE 247 has.diff <- suppressWarnings(any(diff.obj)) 248 249 # If there are no differences reducing levels isn't going to help to 250 # find one; additionally, if not in auto.mode we should not be going 251 # through this process 252 253 if(first.loop && !has.diff) break 254 first.loop <- FALSE 255 256 if(line.limit[[1L]] < 1L) break 257 258 line.len <- diff_line_len( 259 diff.obj@diffs, etc=etc, tar.capt=tar.str, cur.capt=cur.str 260 ) 261 # We need a higher level if we don't have diffs 262 263 if(!has.diff && prev.lvl.hi - lvl > 1L) { 264 prev.lvl.lo <- lvl 265 lvl <- lvl + as.integer((prev.lvl.hi - lvl) / 2) 266 tar.call[[max.level.pos]] <- lvl 267 cur.call[[max.level.pos]] <- lvl 268 next 269 } else if(!has.diff) { 270 diff.obj <- diff.obj.full 271 lvl <- NULL 272 break 273 } 274 # If we have diffs, need to check whether we should try to reduce lines 275 # to get under line limit 276 277 if(line.len <= line.limit[[1L]]) { 278 # We fit, nothing else to do 279 break 280 } 281 if(lvl - prev.lvl.lo > 1L) { 282 prev.lvl.hi <- lvl 283 lvl <- lvl - as.integer((lvl - prev.lvl.lo) / 2) 284 tar.call[[max.level.pos]] <- lvl 285 cur.call[[max.level.pos]] <- lvl 286 next 287 } 288 # Couldn't get under limit, so use first run results 289 290 diff.obj <- diff.obj.full 291 lvl <- NULL 292 break 293 } 294 } else { 295 tar.str <- tar.capt[tar.lvls <= max.level.eval] 296 cur.str <- cur.capt[cur.lvls <= max.level.eval] 297 298 lvl <- max.level.eval 299 diff.obj <- line_diff(target, current, tar.str, cur.str, etc=etc, warn=warn) 300 } 301 if(auto.mode && !is.null(lvl) && lvl < max.depth) { 302 str.match[[max.level.pos]] <- lvl 303 } else if (!max.level.supplied || is.null(lvl)) { 304 str.match[[max.level.pos]] <- NULL 305 } 306 tar.call <- cur.call <- str.match 307 if(!is.null(etc@tar.exp)) tar.call[[2L]] <- etc@tar.exp 308 if(!is.null(etc@cur.exp)) cur.call[[2L]] <- etc@cur.exp 309 if(is.null(etc@tar.banner)) 310 diff.obj@etc@tar.banner <- deparse(tar.call)[[1L]] 311 if(is.null(etc@cur.banner)) 312 diff.obj@etc@cur.banner <- deparse(cur.call)[[1L]] 313 314 # Track total differences in fully expanded view so we can report hidden 315 # diffs when folding levels 316 317 diff.obj@diff.count.full <- count_diffs(diff.obj.full@diffs) 318 diff.obj@capt.mode <- "str" 319 diff.obj 320} 321capt_chr <- function(target, current, etc, err, extra){ 322 tar.capt <- if(!is.character(target)) 323 do.call(as.character, c(list(target), extra), quote=TRUE) else target 324 cur.capt <- if(!is.character(current)) 325 do.call(as.character, c(list(current), extra), quote=TRUE) else current 326 327 # technically possible to have a character method that doesn't return a 328 # character object... 329 330 if((tt <- typeof(tar.capt)) != 'character') 331 stop("Coercion of `target` did not produce character object (", tt, ").") 332 if((tc <- typeof(cur.capt)) != 'character') 333 stop("Coercion of `current` did not produce character object (", tc, ").") 334 335 # drop attributes 336 337 tar.capt <- c(tar.capt) 338 cur.capt <- c(cur.capt) 339 340 if(anyNA(tar.capt)) tar.capt[is.na(tar.capt)] <- "NA" 341 if(anyNA(cur.capt)) cur.capt[is.na(cur.capt)] <- "NA" 342 343 etc <- set_mode(etc, tar.capt, cur.capt) 344 if(isTRUE(etc@guides)) etc@guides <- guidesChr 345 if(isTRUE(etc@trim)) etc@trim <- trimChr 346 347 diff.out <- line_diff( 348 target, current, html_ent_sub(tar.capt, etc@style), 349 html_ent_sub(cur.capt, etc@style), etc=etc 350 ) 351 diff.out@capt.mode <- "chr" 352 diff.out 353} 354capt_deparse <- function(target, current, etc, err, extra){ 355 dep.try <- try({ 356 tar.capt <- do.call(deparse, c(list(target), extra), quote=TRUE) 357 cur.capt <- do.call(deparse, c(list(current), extra), quote=TRUE) 358 }) 359 if(inherits(dep.try, "try-error")) 360 err("Error attempting to deparse object(s)") 361 362 etc <- set_mode(etc, tar.capt, cur.capt) 363 if(isTRUE(etc@guides)) etc@guides <- guidesDeparse 364 if(isTRUE(etc@trim)) etc@trim <- trimDeparse 365 366 diff.out <- line_diff( 367 target, current, html_ent_sub(tar.capt, etc@style), 368 html_ent_sub(cur.capt, etc@style), etc=etc 369 ) 370 diff.out@capt.mode <- "deparse" 371 diff.out 372} 373capt_file <- function(target, current, etc, err, extra) { 374 tar.capt <- try(do.call(readLines, c(list(target), extra), quote=TRUE)) 375 if(inherits(tar.capt, "try-error")) err("Unable to read `target` file.") 376 cur.capt <- try(do.call(readLines, c(list(current), extra), quote=TRUE)) 377 if(inherits(cur.capt, "try-error")) err("Unable to read `current` file.") 378 379 etc <- set_mode(etc, tar.capt, cur.capt) 380 if(isTRUE(etc@guides)) etc@guides <- guidesFile 381 if(isTRUE(etc@trim)) etc@trim <- trimFile 382 383 diff.out <- line_diff( 384 tar.capt, cur.capt, html_ent_sub(tar.capt, etc@style), 385 html_ent_sub(cur.capt, etc@style), etc=etc 386 ) 387 diff.out@capt.mode <- "file" 388 diff.out 389} 390capt_csv <- function(target, current, etc, err, extra){ 391 tar.df <- try(do.call(read.csv, c(list(target), extra), quote=TRUE)) 392 if(inherits(tar.df, "try-error")) err("Unable to read `target` file.") 393 if(!is.data.frame(tar.df)) 394 err("`target` file did not produce a data frame when read") # nocov 395 cur.df <- try(do.call(read.csv, c(list(current), extra), quote=TRUE)) 396 if(inherits(cur.df, "try-error")) err("Unable to read `current` file.") 397 if(!is.data.frame(cur.df)) 398 err("`current` file did not produce a data frame when read") # nocov 399 400 capt_print(tar.df, cur.df, etc, err, extra) 401} 402# Sets mode to "unified" if stuff is too wide to fit side by side without 403# wrapping otherwise sets it in "sidebyside" 404 405set_mode <- function(etc, tar.capt, cur.capt) { 406 stopifnot(is(etc, "Settings"), is.character(tar.capt), is.character(cur.capt)) 407 if(etc@mode == "auto") { 408 if( 409 any( 410 nchar2(cur.capt, sgr.supported=etc@sgr.supported) > etc@text.width.half 411 ) || 412 any( 413 nchar2(tar.capt, sgr.supported=etc@sgr.supported) > etc@text.width.half 414 ) 415 ) { 416 etc@mode <- "unified" 417 } } 418 if(etc@mode == "auto") etc <- sideBySide(etc) 419 etc 420} 421