Skip to content

Commit a3ee426

Browse files
committed
feat: Improve test error message outputs
1 parent f60dcfa commit a3ee426

File tree

1 file changed

+300
-3
lines changed

1 file changed

+300
-3
lines changed

test/utils/compare.js

Lines changed: 300 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,297 @@ function resetFile(pdfFile) {
8282
pdfFile = pdfFile.replace(/\/Producer \([^)]+\)/, "/Producer (jsPDF 0.0.0)");
8383
return pdfFile;
8484
}
85+
86+
function findRepeatedPattern(differences) {
87+
if (differences.length < 2) return null;
88+
89+
// Group differences by type
90+
const groups = differences.reduce((acc, diff) => {
91+
const type = getPdfLineType(diff.expected || diff.actual);
92+
if (!acc[type]) acc[type] = [];
93+
acc[type].push(diff);
94+
return acc;
95+
}, {});
96+
97+
// Find patterns within each group
98+
const patterns = [];
99+
for (const [type, diffs] of Object.entries(groups)) {
100+
if (diffs.length > 3) {
101+
// Check if all differences in this group follow a similar pattern
102+
const firstDiff = diffs[0];
103+
const isConsistent = diffs.every(diff =>
104+
(diff.actual === firstDiff.actual && diff.expected === firstDiff.expected) ||
105+
(type === 'text-pos' && diff.actual?.endsWith(firstDiff.actual?.split(' ').pop()) && diff.expected?.endsWith(firstDiff.expected?.split(' ').pop()))
106+
);
107+
108+
if (isConsistent) {
109+
patterns.push({
110+
type,
111+
count: diffs.length,
112+
sample: firstDiff,
113+
startLine: firstDiff.line,
114+
consistent: true
115+
});
116+
} else {
117+
patterns.push({
118+
type,
119+
count: diffs.length,
120+
sample: firstDiff,
121+
startLine: firstDiff.line,
122+
consistent: false
123+
});
124+
}
125+
}
126+
}
127+
128+
return patterns.length > 0 ? patterns : null;
129+
}
130+
131+
function getPdfLineType(line) {
132+
if (!line) return 'unknown';
133+
// PDF structure elements
134+
if (line.match(/^\d+ \d+ obj$/)) return 'obj-header';
135+
if (line.match(/^\d{10} \d{5} [fn] $/)) return 'xref-entry';
136+
if (line.match(/^endobj|stream|endstream$/)) return 'pdf-structure';
137+
138+
// Font-related elements
139+
if (line.startsWith('/BaseFont')) return 'font-def';
140+
if (line.match(/^\/F\d+ \d+ \d+ R$/)) return 'font-ref';
141+
if (line.match(/^\/F\d+ \d+ Tf$/)) return 'font-select';
142+
143+
// Text operations
144+
if (line.match(/^\d+\.\d+ \d+\.\d+ Td$/)) return 'text-pos';
145+
if (line.match(/^BT|ET$/)) return 'text-block';
146+
if (line.match(/^\d+\.\d+ TL$/)) return 'text-leading';
147+
if (line.match(/^\(\S+\) Tj$/)) return 'text-show';
148+
149+
// Graphics operations
150+
if (line.match(/^\d+\.\d+ \d+\.\d+ \d+\.\d+ (rg|RG)$/)) return 'color';
151+
if (line.match(/^[Qq]$/)) return 'graphics-state';
152+
if (line.match(/^\d+\.\d+ w$/)) return 'line-width';
153+
if (line.match(/^\d+\.\d+ \d+\.\d+ [ml]$/)) return 'path';
154+
if (line.match(/^[WSFfBb]\*?$/)) return 'path-op';
155+
if (line.match(/^[01] [JLj]$/)) return 'style';
156+
157+
// Length and metadata
158+
if (line.match(/^\/Length \d+$/)) return 'length';
159+
if (line.match(/^\/Producer|\/CreationDate|\/Creator/)) return 'metadata';
160+
161+
return 'unknown';
162+
}
163+
164+
function compareArrays(actual, expected) {
165+
let differences = [];
166+
let consecutiveDiffs = 0;
167+
let currentBlock = [];
168+
169+
for (let i = 0; i < Math.max(actual.length, expected.length); i++) {
170+
if (actual[i] !== expected[i]) {
171+
consecutiveDiffs++;
172+
currentBlock.push({
173+
line: i + 1,
174+
actual: actual[i],
175+
expected: expected[i]
176+
});
177+
} else {
178+
if (consecutiveDiffs > 0) {
179+
const patterns = findRepeatedPattern(currentBlock);
180+
if (patterns) {
181+
differences.push({
182+
patterns,
183+
startLine: currentBlock[0].line,
184+
count: currentBlock.length
185+
});
186+
} else if (currentBlock.length <= 3) {
187+
differences.push(...currentBlock);
188+
} else {
189+
differences.push({
190+
truncated: true,
191+
count: currentBlock.length,
192+
startLine: currentBlock[0].line,
193+
sample: currentBlock[0]
194+
});
195+
}
196+
currentBlock = [];
197+
}
198+
consecutiveDiffs = 0;
199+
}
200+
}
201+
202+
// Handle any remaining differences
203+
if (currentBlock.length > 0) {
204+
const patterns = findRepeatedPattern(currentBlock);
205+
if (patterns) {
206+
differences.push({
207+
patterns,
208+
startLine: currentBlock[0].line,
209+
count: currentBlock.length
210+
});
211+
} else if (currentBlock.length <= 3) {
212+
differences.push(...currentBlock);
213+
} else {
214+
differences.push({
215+
truncated: true,
216+
count: currentBlock.length,
217+
startLine: currentBlock[0].line,
218+
sample: currentBlock[0]
219+
});
220+
}
221+
}
222+
223+
return differences;
224+
}
225+
226+
const DIFFERENCE_DESCRIPTIONS = {
227+
// PDF structure
228+
'obj-header': 'Object header',
229+
'xref-entry': 'Cross-reference entry',
230+
'pdf-structure': 'PDF structure element',
231+
232+
// Font-related
233+
'font-def': 'Font definition',
234+
'font-ref': 'Font reference',
235+
'font-select': 'Font selection',
236+
237+
// Text operations
238+
'text-pos': 'Text position',
239+
'text-block': 'Text block marker',
240+
'text-leading': 'Text leading',
241+
'text-show': 'Text content',
242+
243+
// Graphics operations
244+
'color': 'Color setting',
245+
'graphics-state': 'Graphics state',
246+
'line-width': 'Line width',
247+
'path': 'Path command',
248+
'path-op': 'Path operation',
249+
'style': 'Style setting',
250+
251+
// Length and metadata
252+
'length': 'Content length',
253+
'metadata': 'Document metadata',
254+
255+
'unknown': 'Uncategorized'
256+
};
257+
258+
function formatDifferences(differences) {
259+
const MAX_DIFFERENCES_TO_SHOW = 10;
260+
261+
let message = '';
262+
let totalDiffs = 0;
263+
let shownDiffs = 0;
264+
265+
// First, count total differences
266+
for (const diff of differences) {
267+
if (diff.patterns) {
268+
totalDiffs += diff.count;
269+
} else if (diff.truncated) {
270+
totalDiffs += diff.count;
271+
} else {
272+
totalDiffs++;
273+
}
274+
}
275+
276+
message += `\nTotal differences: ${totalDiffs}\n`;
277+
278+
// Group similar patterns together
279+
const patternGroups = new Map();
280+
281+
for (const diff of differences) {
282+
if (diff.patterns) {
283+
for (const pattern of diff.patterns) {
284+
if (pattern.consistent) {
285+
const key = `${pattern.type}:${pattern.sample.expected}:${pattern.sample.actual}`;
286+
if (!patternGroups.has(key)) {
287+
patternGroups.set(key, {
288+
type: pattern.type,
289+
sample: pattern.sample,
290+
count: 0,
291+
locations: []
292+
});
293+
}
294+
const group = patternGroups.get(key);
295+
group.count += pattern.count;
296+
group.locations.push(`${pattern.startLine}-${pattern.startLine + pattern.count - 1}`);
297+
}
298+
}
299+
}
300+
}
301+
302+
// Output grouped patterns first
303+
if (patternGroups.size > 0) {
304+
message += '\nConsistent patterns:\n';
305+
for (const group of patternGroups.values()) {
306+
if (shownDiffs >= MAX_DIFFERENCES_TO_SHOW) {
307+
message += `\n... and ${patternGroups.size - shownDiffs} more consistent patterns\n`;
308+
break;
309+
}
310+
message += ` - ${group.count} ${DIFFERENCE_DESCRIPTIONS[group.type]} differences:\n`;
311+
const actualStr = group.sample.actual === undefined ? '<missing>' : group.sample.actual;
312+
const expectedStr = group.sample.expected === undefined ? '<missing>' : group.sample.expected;
313+
message += ` Expected: "${expectedStr}"\n Actual: "${actualStr}"\n`;
314+
message += ` At lines: ${group.locations.join(', ')}\n`;
315+
shownDiffs++;
316+
}
317+
}
318+
319+
// Then output other differences
320+
let remainingDiffsToShow = MAX_DIFFERENCES_TO_SHOW - shownDiffs;
321+
let skippedDiffs = 0;
322+
323+
for (const diff of differences) {
324+
if (remainingDiffsToShow <= 0) {
325+
skippedDiffs++;
326+
continue;
327+
}
328+
329+
if (diff.patterns) {
330+
const inconsistentPatterns = diff.patterns.filter(p => !p.consistent);
331+
if (inconsistentPatterns.length > 0) {
332+
message += `\nFound ${diff.count} differences at line ${diff.startLine}:\n`;
333+
for (const pattern of inconsistentPatterns) {
334+
if (remainingDiffsToShow <= 0) {
335+
skippedDiffs++;
336+
continue;
337+
}
338+
message += ` - ${pattern.count} ${DIFFERENCE_DESCRIPTIONS[pattern.type]} differences, e.g.:\n`;
339+
const actualStr = pattern.sample.actual === undefined ? '<missing>' : pattern.sample.actual;
340+
const expectedStr = pattern.sample.expected === undefined ? '<missing>' : pattern.sample.expected;
341+
message += ` Expected: "${expectedStr}"\n Actual: "${actualStr}"\n`;
342+
remainingDiffsToShow--;
343+
}
344+
}
345+
} else if (diff.truncated) {
346+
message += `\n${diff.count} differences at line ${diff.startLine} (showing first):\n`;
347+
const actualStr = diff.sample.actual === undefined ? '<missing>' : diff.sample.actual;
348+
const expectedStr = diff.sample.expected === undefined ? '<missing>' : diff.sample.expected;
349+
const type = getPdfLineType(expectedStr || actualStr);
350+
message += ` ${DIFFERENCE_DESCRIPTIONS[type]}:\n`;
351+
message += ` Expected: "${expectedStr}"\n Actual: "${actualStr}"\n ... and ${diff.count - 1} more differences\n`;
352+
remainingDiffsToShow--;
353+
} else {
354+
const actualStr = diff.actual === undefined ? '<missing>' : diff.actual;
355+
const expectedStr = diff.expected === undefined ? '<missing>' : diff.expected;
356+
const type = getPdfLineType(expectedStr || actualStr);
357+
358+
if (actualStr === '<missing>') {
359+
message += `\nLine ${diff.line}: Extra ${DIFFERENCE_DESCRIPTIONS[type]}: "${expectedStr}"`;
360+
} else if (expectedStr === '<missing>') {
361+
message += `\nLine ${diff.line}: Extra ${DIFFERENCE_DESCRIPTIONS[type]}: "${actualStr}"`;
362+
} else {
363+
message += `\nLine ${diff.line} (${DIFFERENCE_DESCRIPTIONS[type]}):\n Expected: "${expectedStr}"\n Actual: "${actualStr}"`;
364+
}
365+
remainingDiffsToShow--;
366+
}
367+
}
368+
369+
if (skippedDiffs > 0) {
370+
message += `\n\n... and ${skippedDiffs} more differences not shown`;
371+
}
372+
373+
return message;
374+
}
375+
85376
/**
86377
* Find a better way to set this
87378
* @type {Boolean}
@@ -104,7 +395,13 @@ globalVar.comparePdf = function(actual, expectedFile, suite) {
104395
var expected = resetFile(pdf.replace(/^\s+|\s+$/g, ""));
105396
actual = resetFile(actual.replace(/^\s+|\s+$/g, ""));
106397

107-
expect(actual.replace(/[\r]/g, "").split("\n")).toEqual(
108-
expected.replace(/[\r]/g, "").split("\n")
109-
);
398+
const actualLines = actual.replace(/[\r]/g, "").split("\n");
399+
const expectedLines = expected.replace(/[\r]/g, "").split("\n");
400+
401+
const differences = compareArrays(actualLines, expectedLines);
402+
403+
if (differences.length > 0) {
404+
const message = formatDifferences(differences);
405+
fail(`PDF comparison failed:${message}`);
406+
}
110407
};

0 commit comments

Comments
 (0)