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