From a3ee426e9c8a7c1e4184e2ce43ebf8dd055426c7 Mon Sep 17 00:00:00 2001 From: James Hall Date: Tue, 24 Dec 2024 14:25:04 +0000 Subject: [PATCH 1/2] feat: Improve test error message outputs --- test/utils/compare.js | 303 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 300 insertions(+), 3 deletions(-) diff --git a/test/utils/compare.js b/test/utils/compare.js index e68e17f0e..cbdc64b47 100644 --- a/test/utils/compare.js +++ b/test/utils/compare.js @@ -82,6 +82,297 @@ function resetFile(pdfFile) { pdfFile = pdfFile.replace(/\/Producer \([^)]+\)/, "/Producer (jsPDF 0.0.0)"); return pdfFile; } + +function findRepeatedPattern(differences) { + if (differences.length < 2) return null; + + // Group differences by type + const groups = differences.reduce((acc, diff) => { + const type = getPdfLineType(diff.expected || diff.actual); + if (!acc[type]) acc[type] = []; + acc[type].push(diff); + return acc; + }, {}); + + // Find patterns within each group + const patterns = []; + for (const [type, diffs] of Object.entries(groups)) { + if (diffs.length > 3) { + // Check if all differences in this group follow a similar pattern + const firstDiff = diffs[0]; + const isConsistent = diffs.every(diff => + (diff.actual === firstDiff.actual && diff.expected === firstDiff.expected) || + (type === 'text-pos' && diff.actual?.endsWith(firstDiff.actual?.split(' ').pop()) && diff.expected?.endsWith(firstDiff.expected?.split(' ').pop())) + ); + + if (isConsistent) { + patterns.push({ + type, + count: diffs.length, + sample: firstDiff, + startLine: firstDiff.line, + consistent: true + }); + } else { + patterns.push({ + type, + count: diffs.length, + sample: firstDiff, + startLine: firstDiff.line, + consistent: false + }); + } + } + } + + return patterns.length > 0 ? patterns : null; +} + +function getPdfLineType(line) { + if (!line) return 'unknown'; + // PDF structure elements + if (line.match(/^\d+ \d+ obj$/)) return 'obj-header'; + if (line.match(/^\d{10} \d{5} [fn] $/)) return 'xref-entry'; + if (line.match(/^endobj|stream|endstream$/)) return 'pdf-structure'; + + // Font-related elements + if (line.startsWith('/BaseFont')) return 'font-def'; + if (line.match(/^\/F\d+ \d+ \d+ R$/)) return 'font-ref'; + if (line.match(/^\/F\d+ \d+ Tf$/)) return 'font-select'; + + // Text operations + if (line.match(/^\d+\.\d+ \d+\.\d+ Td$/)) return 'text-pos'; + if (line.match(/^BT|ET$/)) return 'text-block'; + if (line.match(/^\d+\.\d+ TL$/)) return 'text-leading'; + if (line.match(/^\(\S+\) Tj$/)) return 'text-show'; + + // Graphics operations + if (line.match(/^\d+\.\d+ \d+\.\d+ \d+\.\d+ (rg|RG)$/)) return 'color'; + if (line.match(/^[Qq]$/)) return 'graphics-state'; + if (line.match(/^\d+\.\d+ w$/)) return 'line-width'; + if (line.match(/^\d+\.\d+ \d+\.\d+ [ml]$/)) return 'path'; + if (line.match(/^[WSFfBb]\*?$/)) return 'path-op'; + if (line.match(/^[01] [JLj]$/)) return 'style'; + + // Length and metadata + if (line.match(/^\/Length \d+$/)) return 'length'; + if (line.match(/^\/Producer|\/CreationDate|\/Creator/)) return 'metadata'; + + return 'unknown'; +} + +function compareArrays(actual, expected) { + let differences = []; + let consecutiveDiffs = 0; + let currentBlock = []; + + for (let i = 0; i < Math.max(actual.length, expected.length); i++) { + if (actual[i] !== expected[i]) { + consecutiveDiffs++; + currentBlock.push({ + line: i + 1, + actual: actual[i], + expected: expected[i] + }); + } else { + if (consecutiveDiffs > 0) { + const patterns = findRepeatedPattern(currentBlock); + if (patterns) { + differences.push({ + patterns, + startLine: currentBlock[0].line, + count: currentBlock.length + }); + } else if (currentBlock.length <= 3) { + differences.push(...currentBlock); + } else { + differences.push({ + truncated: true, + count: currentBlock.length, + startLine: currentBlock[0].line, + sample: currentBlock[0] + }); + } + currentBlock = []; + } + consecutiveDiffs = 0; + } + } + + // Handle any remaining differences + if (currentBlock.length > 0) { + const patterns = findRepeatedPattern(currentBlock); + if (patterns) { + differences.push({ + patterns, + startLine: currentBlock[0].line, + count: currentBlock.length + }); + } else if (currentBlock.length <= 3) { + differences.push(...currentBlock); + } else { + differences.push({ + truncated: true, + count: currentBlock.length, + startLine: currentBlock[0].line, + sample: currentBlock[0] + }); + } + } + + return differences; +} + +const DIFFERENCE_DESCRIPTIONS = { + // PDF structure + 'obj-header': 'Object header', + 'xref-entry': 'Cross-reference entry', + 'pdf-structure': 'PDF structure element', + + // Font-related + 'font-def': 'Font definition', + 'font-ref': 'Font reference', + 'font-select': 'Font selection', + + // Text operations + 'text-pos': 'Text position', + 'text-block': 'Text block marker', + 'text-leading': 'Text leading', + 'text-show': 'Text content', + + // Graphics operations + 'color': 'Color setting', + 'graphics-state': 'Graphics state', + 'line-width': 'Line width', + 'path': 'Path command', + 'path-op': 'Path operation', + 'style': 'Style setting', + + // Length and metadata + 'length': 'Content length', + 'metadata': 'Document metadata', + + 'unknown': 'Uncategorized' +}; + +function formatDifferences(differences) { + const MAX_DIFFERENCES_TO_SHOW = 10; + + let message = ''; + let totalDiffs = 0; + let shownDiffs = 0; + + // First, count total differences + for (const diff of differences) { + if (diff.patterns) { + totalDiffs += diff.count; + } else if (diff.truncated) { + totalDiffs += diff.count; + } else { + totalDiffs++; + } + } + + message += `\nTotal differences: ${totalDiffs}\n`; + + // Group similar patterns together + const patternGroups = new Map(); + + for (const diff of differences) { + if (diff.patterns) { + for (const pattern of diff.patterns) { + if (pattern.consistent) { + const key = `${pattern.type}:${pattern.sample.expected}:${pattern.sample.actual}`; + if (!patternGroups.has(key)) { + patternGroups.set(key, { + type: pattern.type, + sample: pattern.sample, + count: 0, + locations: [] + }); + } + const group = patternGroups.get(key); + group.count += pattern.count; + group.locations.push(`${pattern.startLine}-${pattern.startLine + pattern.count - 1}`); + } + } + } + } + + // Output grouped patterns first + if (patternGroups.size > 0) { + message += '\nConsistent patterns:\n'; + for (const group of patternGroups.values()) { + if (shownDiffs >= MAX_DIFFERENCES_TO_SHOW) { + message += `\n... and ${patternGroups.size - shownDiffs} more consistent patterns\n`; + break; + } + message += ` - ${group.count} ${DIFFERENCE_DESCRIPTIONS[group.type]} differences:\n`; + const actualStr = group.sample.actual === undefined ? '' : group.sample.actual; + const expectedStr = group.sample.expected === undefined ? '' : group.sample.expected; + message += ` Expected: "${expectedStr}"\n Actual: "${actualStr}"\n`; + message += ` At lines: ${group.locations.join(', ')}\n`; + shownDiffs++; + } + } + + // Then output other differences + let remainingDiffsToShow = MAX_DIFFERENCES_TO_SHOW - shownDiffs; + let skippedDiffs = 0; + + for (const diff of differences) { + if (remainingDiffsToShow <= 0) { + skippedDiffs++; + continue; + } + + if (diff.patterns) { + const inconsistentPatterns = diff.patterns.filter(p => !p.consistent); + if (inconsistentPatterns.length > 0) { + message += `\nFound ${diff.count} differences at line ${diff.startLine}:\n`; + for (const pattern of inconsistentPatterns) { + if (remainingDiffsToShow <= 0) { + skippedDiffs++; + continue; + } + message += ` - ${pattern.count} ${DIFFERENCE_DESCRIPTIONS[pattern.type]} differences, e.g.:\n`; + const actualStr = pattern.sample.actual === undefined ? '' : pattern.sample.actual; + const expectedStr = pattern.sample.expected === undefined ? '' : pattern.sample.expected; + message += ` Expected: "${expectedStr}"\n Actual: "${actualStr}"\n`; + remainingDiffsToShow--; + } + } + } else if (diff.truncated) { + message += `\n${diff.count} differences at line ${diff.startLine} (showing first):\n`; + const actualStr = diff.sample.actual === undefined ? '' : diff.sample.actual; + const expectedStr = diff.sample.expected === undefined ? '' : diff.sample.expected; + const type = getPdfLineType(expectedStr || actualStr); + message += ` ${DIFFERENCE_DESCRIPTIONS[type]}:\n`; + message += ` Expected: "${expectedStr}"\n Actual: "${actualStr}"\n ... and ${diff.count - 1} more differences\n`; + remainingDiffsToShow--; + } else { + const actualStr = diff.actual === undefined ? '' : diff.actual; + const expectedStr = diff.expected === undefined ? '' : diff.expected; + const type = getPdfLineType(expectedStr || actualStr); + + if (actualStr === '') { + message += `\nLine ${diff.line}: Extra ${DIFFERENCE_DESCRIPTIONS[type]}: "${expectedStr}"`; + } else if (expectedStr === '') { + message += `\nLine ${diff.line}: Extra ${DIFFERENCE_DESCRIPTIONS[type]}: "${actualStr}"`; + } else { + message += `\nLine ${diff.line} (${DIFFERENCE_DESCRIPTIONS[type]}):\n Expected: "${expectedStr}"\n Actual: "${actualStr}"`; + } + remainingDiffsToShow--; + } + } + + if (skippedDiffs > 0) { + message += `\n\n... and ${skippedDiffs} more differences not shown`; + } + + return message; +} + /** * Find a better way to set this * @type {Boolean} @@ -104,7 +395,13 @@ globalVar.comparePdf = function(actual, expectedFile, suite) { var expected = resetFile(pdf.replace(/^\s+|\s+$/g, "")); actual = resetFile(actual.replace(/^\s+|\s+$/g, "")); - expect(actual.replace(/[\r]/g, "").split("\n")).toEqual( - expected.replace(/[\r]/g, "").split("\n") - ); + const actualLines = actual.replace(/[\r]/g, "").split("\n"); + const expectedLines = expected.replace(/[\r]/g, "").split("\n"); + + const differences = compareArrays(actualLines, expectedLines); + + if (differences.length > 0) { + const message = formatDifferences(differences); + fail(`PDF comparison failed:${message}`); + } }; From 02b34cef00610f6b1a4ec67f241bd358567cc287 Mon Sep 17 00:00:00 2001 From: James Hall Date: Tue, 24 Dec 2024 14:42:02 +0000 Subject: [PATCH 2/2] feat: Output debug 'actual' PDFs --- .gitignore | 1 + test/actual/.gitkeep | 0 test/utils/compare.js | 7 ++++++- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 test/actual/.gitkeep diff --git a/.gitignore b/.gitignore index aa6c5ffec..22d2b4919 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules/ coverage/ npm-debug.log +test/actual/*.pdf diff --git a/test/actual/.gitkeep b/test/actual/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/test/utils/compare.js b/test/utils/compare.js index cbdc64b47..dfbe34ca3 100644 --- a/test/utils/compare.js +++ b/test/utils/compare.js @@ -401,7 +401,12 @@ globalVar.comparePdf = function(actual, expectedFile, suite) { const differences = compareArrays(actualLines, expectedLines); if (differences.length > 0) { + // Save the actual PDF for debugging + globalVar.sendReference( + "/test/actual/" + expectedFile, + resetFile(actual) + ); const message = formatDifferences(differences); - fail(`PDF comparison failed:${message}`); + fail(`PDF comparison failed:${message}\nActual PDF saved to: test/actual/${expectedFile}`); } };