RS-21803: checkImageTag style attribute test - #72
Conversation
Adds scenarios for style and width attributes preceding src, attribute order independence, double-quoted and unquoted src values, a missing src attribute, surrounding text preservation, and a decoy substring "src=" match that is not silently treated as a valid link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace unfailable identical() check and network-dependent warning match in the src= decoy block with pinned real values (result "" and warning text including the captured "x"); pin echoed input in the missing-src syntax-error message to distinguish it from the tag-regex-miss branch; drop redundant duplicate expect_warning call; hoist repeated txt.style literal to a file-level constant and remove dangling duplicate txt.dq assignment; rename reused txt.dq in the double-quote block; document network dependency of success-path blocks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ions Addresses the code review of PR #72: - The five success-path blocks fetch a real image from wiki.q-researchsoftware.com. Without access to that host GET() errors, checkImageTag() warns and strips the tag, and the assertions failed rather than skipped. Each block now guards on a cached probe of the exact image it depends on. testthat's skip_if_offline() is deliberately not used: it calls skip_on_cran(), which would skip the blocks in any run where NOT_CRAN is unset, network or no. - Anchor the decoy-src warning on the extracted link, so a regression in the quote stripping (which would produce "x'") can no longer satisfy it. - Reference defect RS-23603 from the block that pins the unanchored src= match, per the convention used for the banding defects. - txt.dq was byte-identical to txt.sq and asserted nothing new; make it genuinely double-quoted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chschan
left a comment
There was a problem hiding this comment.
Reviewed the diff and ran it: 24 passed / 0 failed / 0 skipped here with network up, so the test-plan claim holds. DESCRIPTION correctly stays at 1.11.11 for a tests-only change, and the txt.dq fix is real — double quotes were genuinely never exercised before, and nchar == 84 still holds since both quote characters are one byte. No caller behaviour changes.
Approving with comments. Three worth doing before merge:
- The PR description is stale relative to the head commit. The "Network dependency" section says the blocks "fail offline. Documented in a comment at the top of the file" — but 3b8025a added
skipIfOffline(), so the behaviour is now skip, not fail. Please update the body. - The offline guard is incomplete and the header NOTE overstates it (inline at line 7).
- The decoy test's failure mode depends on DNS resolution (inline at line 117).
The rest are cleanups.
One minor note that can't be inlined (lines 43-47 fall between the two diff hunks, so GitHub won't take a comment there):
Unlike every other branch asserted in this file, the empty-link path never checks the return value - there's no res <- and no expect_equal. Worth pinning, because that branch returns via sub(imgtag, "", text) rather than the bare return("") the two syntax-error branches use, and the difference only shows up with surrounding text:
expect_warning(res <- checkImageTag("<img src=''>Some text"),
"an image tag with an empty link")
expect_equal(res, "Some text")Pre-existing and entirely optional for this PR.
| # order, quoting variants, surrounding text) fetch a real image from | ||
| # wiki.q-researchsoftware.com. Without network access to that host GET() errors, | ||
| # checkImageTag() warns and strips the tag, and those assertions would fail rather | ||
| # than skip - so each such block guards itself with skipIfOffline(). |
There was a problem hiding this comment.
This overstates what's guarded. The test_that("checkImageTag") block below is also network-dependent and has no skipIfOffline() — line 57 fetches exactly kImageLink, and lines 36/40 fetch a Dropbox URL.
Simulating an offline run (stubbed failing GET()): the five new blocks skip cleanly, but that block produces 3 hard failures — nchar(res) 0 vs 84 twice, plus the txt.withattr equality. So the file still fails offline rather than skipping.
Either guard that block too, or narrow this wording to name the blocks it actually covers.
(The skip_if_offline() / skip_on_cran() rationale in the second paragraph is a good catch and worth keeping.)
There was a problem hiding this comment.
Right — guarded that block rather than narrowing the wording (29bfde7). The probe now caches per link, skipIfOffline() takes the links a block needs, and the first block guards on c(kImageLink, kDropboxLink). Header NOTE rewritten to say what is actually covered, including that the empty-link assertions need no network but skip with the block they live in. Simulated offline (both constants pointed at an unreachable host): 6 skips, 6 passes, 0 failures — previously that block gave 3 hard failures.
| @@ -26,6 +56,64 @@ test_that("checkImageTag", | |||
| <img src=https://wiki.q-researchsoftware.com/images/c/cb/CokeZero.png width='45' height='100'></a>" | |||
| expect_error(res <- checkImageTag(txt.withattr), NA) | |||
There was a problem hiding this comment.
This is the unguarded one. It fetches the same kImageLink URL the probe already checks, so skipIfOffline() here is free and correct.
The Dropbox assertions at lines 36/40 need their own probe, or the header NOTE should say the file isn't fully offline-safe.
There was a problem hiding this comment.
Fixed in 29bfde7 — that block now calls skipIfOffline(c(kImageLink, kDropboxLink)), and the Dropbox URL is a kDropboxLink constant the probe checks alongside the wiki image, so neither needs its own wording caveat. Its two nchar(res) == 84 assertions are now full equality against paste0("<div>", txt, "</div>"), which both subsumes the byte count and stops the expected value drifting if the constant changes.
| expect_equal(res, paste0("<div>", kTxtStyle, "</div>")) | ||
| }) | ||
|
|
||
| test_that("checkImageTag: src location is independent of attribute order", |
There was a problem hiding this comment.
The name overstates what the function does — the decoy block at line 108 demonstrates it is not order-independent once an earlier attribute value contains src=.
Suggest "src is found when it is the first attribute".
There was a problem hiding this comment.
Renamed to "checkImageTag: src is found when it is the first attribute" in 29bfde7, with a comment pointing at the decoy block as the counter-example.
| test_that("checkImageTag: src location is independent of attribute order", | ||
| { | ||
| skipIfOffline() | ||
| txt.order <- "<img src='https://wiki.q-researchsoftware.com/images/c/cb/CokeZero.png' style='margin:0 auto;' width='50'>" |
There was a problem hiding this comment.
txt.order here (and txt.dq2 at line 79, txt.unquoted at line 87) hard-code the wiki URL as a literal, while the probe and kTxtStyle use kImageLink.
The guard is then correct only by coincidence of duplicated literals: change kImageLink (new host, moved image) and the probe reports "reachable" while these three blocks still GET the stale URL — turning the intended skip back into a hard failure, which is the exact failure mode this PR set out to remove.
paste0("<img src='", kImageLink, "' style='margin:0 auto;' width='50'>") etc.
There was a problem hiding this comment.
Good catch — all duplicated literals are gone in 29bfde7. txt.order, txt.dq2, txt.unquoted, txt.decoy and the txt.withattr fixture in the first block are now built with paste0(..., kImageLink, ...), so the probe and the requests can no longer diverge. The Dropbox URL got the same treatment via kDropboxLink.
| { | ||
| skipIfOffline() | ||
| txt.order <- "<img src='https://wiki.q-researchsoftware.com/images/c/cb/CokeZero.png' style='margin:0 auto;' width='50'>" | ||
| expect_error(res <- checkImageTag(txt.order), NA) |
There was a problem hiding this comment.
Inconsistent absence-assertion across structurally identical blocks: line 64 uses expect_warning(..., NA), but this line (and 80, 88, 104) uses only expect_error(..., NA), which says nothing about warnings.
A regression that makes checkImageTag warn-and-strip is pinned as a clear failure in the first block, but surfaces in these as a testthat warning plus a downstream equality failure — which reads like a network problem rather than a behaviour change.
expect_warning(..., NA) uniformly.
There was a problem hiding this comment.
Done in 29bfde7 — every success-path call now uses expect_warning(..., NA), including the two in the first block that previously only used expect_error(..., NA). An error still fails the test, so nothing is lost by dropping the error form.
| { | ||
| txt.nosrc <- "<img style='margin:0 auto;' width='50'>" | ||
| expect_warning(res <- checkImageTag(txt.nosrc), | ||
| "syntax error which has been removed: <img style='margin:0 auto;' width='50'>") |
There was a problem hiding this comment.
The PR description says this pins the echoed input "so it is distinguishable from the tag-regex-miss branch that emits a byte-identical syntax error prefix". It doesn't — both branches in checkimagetag.R warn with "...syntax error which has been removed: ", text, i.e. the same text, so for this input the two messages are byte-identical:
"<img style=1" -> syntax error which has been removed: <img style=1
"<img style='margin:0 auto;' width='50'>" -> syntax error which has been removed: <img style='margin:0 auto;' width='50'>
It happens not to matter today, since this input does match <img [^>]+> and so can only reach the src= branch. But if the tag regex is later loosened or tightened so this input stops matching, the test keeps passing while silently covering the other branch.
Distinguishing them needs two inputs — one that fails the tag regex, one that fails only src= — not a pinned echo. Either add the second input or drop the claim from the description.
There was a problem hiding this comment.
Correct — the echo distinguishes nothing, since both branches warn with the same text. Took the "add the second input" option in 29bfde7: the block is now "both syntax-error branches warn and return an empty string" and covers one input per branch — the existing <img style='margin:0 auto;' width='50'> matches <img [^>]+> and so can only reach the src= branch, and "<img style='margin:0 auto;' width='50'" (no closing >) misses the tag regex first. The comment states outright that no assertion on the warning can tell them apart. The stale claim is also removed from the PR description.
| # The warning is anchored on the extracted link so a regression in the quote | ||
| # stripping (which would give "x'") cannot satisfy it. | ||
| txt.decoy <- "<img alt='mysrc=x' src='https://wiki.q-researchsoftware.com/images/c/cb/CokeZero.png'>" | ||
| expect_warning(res <- checkImageTag(txt.decoy), "invalid link which has been removed: x$") |
There was a problem hiding this comment.
This assertion depends on DNS luck. The extracted link is the bare string x, and the block only passes because httr::GET("x") fails to resolve it. On a network with a DNS search suffix that makes a single-label x resolvable, or behind a proxy/captive portal that answers arbitrary hosts with 200, checkImageTag takes the success path, emits no warning, and this fails with a misleading "no warning" message rather than a pinned behaviour change.
Use an RFC 2606 reserved TLD so it's inert regardless of network:
txt.decoy <- paste0("<img alt='mysrc=http://x.invalid/' src='", kImageLink, "'>")
expect_warning(res <- checkImageTag(txt.decoy),
"invalid link which has been removed: http://x\.invalid/$")Note the escaped . — expect_warning matches its second argument as a regex.
(Confirmed RS-23603 exists and its summary matches this comment, so the ticket reference is sound.)
There was a problem hiding this comment.
Took your suggestion verbatim in 29bfde7, escaped dot included: txt.decoy <- paste0("<img alt='mysrc=http://x.invalid/' src='", kImageLink, "'>") with the warning anchored on "invalid link which has been removed: http://x\.invalid/$". Added a note that the block deliberately carries no skipIfOffline() — the decoy is matched first, so kImageLink is never fetched and the block is genuinely network-free. Confirmed in the simulated-offline run: it passes there.
chschan
left a comment
There was a problem hiding this comment.
Some minor comments, but otherwise LGTM
From chschan's review of PR #72: - The offline guard missed the first test_that block, which fetches both the Dropbox URL and the wiki image, so the file still failed offline rather than skipping. The probe now caches per link, skipIfOffline() takes the links a block needs, and that block guards on both. Header NOTE corrected to say what is actually guarded. - Replace the duplicated wiki URL literals with kImageLink in every block. The guard was previously correct only because the literals happened to match the probe's URL; changing kImageLink would have left those blocks fetching a stale URL while the probe reported reachable. - "src location is independent of attribute order" overstated the behaviour - the decoy block shows it is not. Renamed to "src is found when it is the first attribute". - Use expect_warning(..., NA) uniformly for the success-path calls. The expect_error(..., NA) form said nothing about warnings, so a warn-and-strip regression surfaced as a testthat warning plus a downstream equality failure rather than a clear one. - The two syntax-error branches emit a byte-identical message, so the pinned echo could not distinguish them. Cover them with one input each instead: the existing input reaches the src= branch, and an input with no closing ">" misses the tag regex first. - The decoy link was the bare string "x", so the branch depended on DNS failing to resolve a single-label host. Use http://x.invalid/ (RFC 2606), so it is inert on any network. - Pin the empty-link branch's return value, which returns sub(imgtag, "", text) rather than the bare "" of the syntax-error branches - visible only with surrounding text. Verified offline behaviour by pointing both link constants at an unreachable host: 6 skips, 6 passes, 0 failures. With network: 28 passing, 0 failures. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All seven inline comments plus the un-inlinable note are addressed in 29bfde7; PR description updated for the three stale claims (the "fails offline" wording, the syntax-error distinguishability claim, and the missing RS-23603 reference). The one non-obvious call: I guarded the first Offline verification (both link constants pointed at an unreachable host): 6 skips, 6 passes, 0 failures — previously the first block gave 3 hard failures. With network: 28 passing, 0 failures, 0 warnings. |
Summary
Adds unit test coverage for
checkImageTag's handling of thestyleattribute and its surrounding link-cleaning paths (R/checkimagetag.R) — 7 test blocks in the existingtest-checkimagetag.R.styleattribute before and aftersrc, exercising the two different quote/bracket cleaning paths rather than the same one twice.srcvalues, andsrcfollowed by trailing text.<img ...>that matches the tag regex but has nosrc=, and an input with no closing>that misses the tag regex first. The two warnings are byte-identical (each echoes the whole input), so no assertion on the message can tell them apart — separate inputs are what pins the branches.text, not the matchedimgtag.expect_equalrather than several substring greps.Also closes a pre-existing gap: the file's
txt.dqcase was single-quoted and identical totxt.sq, so double quotes were never actually tested. Thestyleliteral is now a single hoisted constant, replacing a dangling unused assignment.Note on the
src=regexThe last block pins current behaviour for a decoy input where an earlier attribute value contains
src=.regexpr("src=(\S+)", ...)is unanchored, soalt='mysrc=http://x.invalid/'is matched in preference to the realsrc, and a valid image is silently removed with a user-visibleinvalid link which has been removed: http://x.invalid/warning. The warning is anchored on the extracted link, so the assertion pins the decoy rather than merely a failed GET, and the decoy uses the RFC 2606 reserved.invalidTLD so it is inert regardless of what the network's DNS does.This looks like a genuine low-severity defect rather than intended behaviour: nothing in the function or its callers suggests the loose match is deliberate, and a boundary requirement such as
(?:^|\s)src=would be safe because the enclosing<img [^>]+>match guarantees whitespace before every attribute. Not fixed here — filed as RS-23603 (Bug, Severity D - Minor), which the test comment references. Separately, the trailing-whitespacesub()in the same function is unreachable, since(\S+)cannot capture whitespace — inert rather than wrong.Network dependency
The success-path blocks perform a live HTTP GET of a real image URL — the pre-existing convention in this file, not introduced here. Rather than failing offline, every block that needs a link now guards on
skipIfOffline(), which probes the exact links the block fetches (kImageLink, andkDropboxLinkfor the first block) once per link per run.testthat::skip_if_offline()is deliberately not used: it callsskip_on_cran(), which would skip these blocks in any run whereNOT_CRANis unset, network or no. Verified by pointing both link constants at an unreachable host — 6 skips, 6 passes, 0 failures.Jira: https://numbers.atlassian.net/browse/RS-21803
Stacked PR
Based on
RS-21803-colwidthsandcustomcss(#71). Merge order: #66, #67, #68, #69, #70, #71, then this. This is the last of the seven RS-21803 plans; it is the only one touchingcheckimagetag.Rrather thancreatecustomtable.R, so it shares no test file with the others.Test plan
28 passing, 0 failures, 0 warnings with network up; 6 passing and 6 skipped without it.
🤖 Generated with Claude Code