# Tests that malformed JSON from the server does not crash the device. # # The device receives JSON messages from the rendering server (resize, # metrics_response). If the server sends garbage, the device must # silently ignore it and continue producing correct output. # TCP mock server that injects malformed JSON at two points: # 1. Immediately after connection (drained by check_incoming on newPage) # 2. Before each metrics_response (drained by recv_metrics_response) start_mock_server_malformed = function() { skip_if_not_installed("callr") skip_if_not_installed("jsonlite") port_file = tempfile(pattern = "jgd-malformed-port-", fileext = ".txt") bg = callr::r_bg( function(port_file) { `%||%` = function(x, y) if (is.null(x)) y else x safe_write = function(conn, text) { tryCatch( { writeLines(text, conn); flush(conn) }, error = function(e) invisible(NULL) ) } server = NULL; port = NULL for (i in seq_len(20)) { candidate = sample(10000L:60000L, 1L) result = tryCatch(serverSocket(candidate), error = function(e) NULL) if (!is.null(result)) { server = result; port = candidate; break } } if (is.null(port)) stop("Could not find free port") on.exit(close(server), add = TRUE) writeLines(as.character(port), port_file) conn = socketAccept(server, blocking = TRUE, open = "r+") on.exit(close(conn), add = TRUE) # Inject malformed JSON immediately after connection safe_write(conn, c( "{not valid json at all", "totally not json", '{"type": "resize", "width": "not_a_number", "height": "bad"}', '{"type": "resize"}', "" )) messages = list() repeat { ready = socketSelect(list(conn), timeout = 5) if (!ready) next line = tryCatch(readLines(conn, n = 1), error = function(e) character(0)) if (length(line) == 0 || !nzchar(line)) next msg = tryCatch( jsonlite::fromJSON(line, simplifyVector = FALSE), error = function(e) NULL ) if (is.null(msg)) next messages = c(messages, list(msg)) if (identical(msg$type, "metrics_request")) { # Inject one malformed line before the valid response safe_write(conn, "{broken json") resp = if (identical(msg$kind, "strWidth")) { list(type = "metrics_response", id = msg$id, width = nchar(msg$str %||% "") * 8.0) } else { list(type = "metrics_response", id = msg$id, ascent = 10.0, descent = 3.0, width = 8.0) } safe_write(conn, jsonlite::toJSON(resp, auto_unbox = TRUE)) } if (identical(msg$type, "close")) break } messages }, args = list(port_file = port_file), supervise = TRUE ) port = NULL for (i in seq_len(50)) { if (file.exists(port_file)) { port_str = readLines(port_file, n = 1, warn = FALSE) if (length(port_str) > 0 && nzchar(port_str)) { port = as.integer(port_str); break } } Sys.sleep(0.1) } if (is.null(port)) { bg$kill(); skip("Mock server did not start in time") } list( bg = bg, socket_url = sprintf("tcp://127.0.0.1:%d", port), collect = function(timeout = 15000) { bg$wait(timeout) status = bg$get_exit_status() if (!is.null(status) && status != 0) { stop("Mock server exited with error: ", bg$read_error()) } if (is.null(status)) { bg$kill() skip("Mock server did not exit in time") } bg$get_result() }, cleanup = function() { if (bg$is_alive()) bg$kill() unlink(port_file) } ) } test_that("device survives malformed JSON and produces correct output", { skip_on_cran() server = start_mock_server_malformed() withr::defer(server$cleanup()) jgd(width = 4, height = 3, dpi = 72, socket = server$socket_url) plot.new() rect(0, 0, 1, 1, col = "red", border = "blue", lwd = 2) text(0.5, 0.5, "hi") dev.off() msgs = server$collect() frames = Filter(function(m) identical(m$type, "frame"), msgs) # Device produced at least one frame expect_true(length(frames) >= 1) # Frame metadata is intact frame = frames[[1]] expect_equal(frame$plot$version, 1) expect_match(frame$plot$sessionId, "^r-[0-9]+-[0-9]+$") # Ops are present and correct ops = unlist(lapply(frames, function(f) f$plot$ops), recursive = FALSE) op_types = vapply(ops, function(o) o$op, character(1)) expect_true("rect" %in% op_types) expect_true("text" %in% op_types) }) test_that("malformed JSON: device metadata matches snapshot", { skip_on_cran() server = start_mock_server_malformed() withr::defer(server$cleanup()) jgd(width = 4, height = 3, dpi = 72, socket = server$socket_url) plot.new() rect(0, 0, 1, 1, col = "red", border = "blue", lwd = 2) dev.off() msgs = server$collect() frame = Filter(function(m) identical(m$type, "frame"), msgs)[[1]] expect_json_snapshot(list( version = frame$plot$version, device = frame$plot$device )) }) test_that("malformed JSON: rect gc fields match snapshot", { skip_on_cran() server = start_mock_server_malformed() withr::defer(server$cleanup()) jgd(width = 4, height = 3, dpi = 72, socket = server$socket_url) plot.new() rect(0, 0, 1, 1, col = "red", border = "blue", lwd = 2) dev.off() msgs = server$collect() rect_ops = extract_ops_by_type(msgs, "rect") expect_json_snapshot(rect_ops[[1]]$gc) })