update
This commit is contained in:
651
test/suite/einvoice_edge-cases/test.edge-03.deep-nesting.ts
Normal file
651
test/suite/einvoice_edge-cases/test.edge-03.deep-nesting.ts
Normal file
@ -0,0 +1,651 @@
|
||||
import { tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from '../plugins.js';
|
||||
import { EInvoice } from '../../../ts/index.js';
|
||||
import { PerformanceTracker } from '../performance.tracker.js';
|
||||
|
||||
const performanceTracker = new PerformanceTracker('EDGE-03: Deeply Nested XML Structures');
|
||||
|
||||
tap.test('EDGE-03: Deeply Nested XML Structures - should handle extremely nested XML', async (t) => {
|
||||
const einvoice = new EInvoice();
|
||||
|
||||
// Test 1: Linear deep nesting
|
||||
const linearDeepNesting = await performanceTracker.measureAsync(
|
||||
'linear-deep-nesting',
|
||||
async () => {
|
||||
const testDepths = [10, 100, 1000, 5000, 10000];
|
||||
const results = [];
|
||||
|
||||
for (const depth of testDepths) {
|
||||
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||
|
||||
// Build deeply nested structure
|
||||
for (let i = 0; i < depth; i++) {
|
||||
xml += ' '.repeat(i) + `<Level${i}>\n`;
|
||||
}
|
||||
|
||||
xml += ' '.repeat(depth) + '<Data>Invoice Data</Data>\n';
|
||||
|
||||
// Close all tags
|
||||
for (let i = depth - 1; i >= 0; i--) {
|
||||
xml += ' '.repeat(i) + `</Level${i}>\n`;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const startMemory = process.memoryUsage();
|
||||
|
||||
try {
|
||||
const result = await einvoice.parseXML(xml);
|
||||
|
||||
const endTime = Date.now();
|
||||
const endMemory = process.memoryUsage();
|
||||
|
||||
results.push({
|
||||
depth,
|
||||
success: true,
|
||||
timeTaken: endTime - startTime,
|
||||
memoryUsed: endMemory.heapUsed - startMemory.heapUsed,
|
||||
hasData: !!result
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
depth,
|
||||
success: false,
|
||||
error: error.message,
|
||||
isStackOverflow: error.message.includes('stack') || error.message.includes('depth')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
linearDeepNesting.forEach(result => {
|
||||
if (result.depth <= 1000) {
|
||||
t.ok(result.success, `Depth ${result.depth} should be handled`);
|
||||
} else {
|
||||
t.ok(!result.success || result.isStackOverflow, `Extreme depth ${result.depth} should be limited`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 2: Recursive element nesting
|
||||
const recursiveElementNesting = await performanceTracker.measureAsync(
|
||||
'recursive-element-nesting',
|
||||
async () => {
|
||||
const createRecursiveStructure = (depth: number): string => {
|
||||
if (depth === 0) {
|
||||
return '<Amount>100.00</Amount>';
|
||||
}
|
||||
|
||||
return `<Item>
|
||||
<ID>ITEM-${depth}</ID>
|
||||
<SubItems>
|
||||
${createRecursiveStructure(depth - 1)}
|
||||
</SubItems>
|
||||
</Item>`;
|
||||
};
|
||||
|
||||
const testDepths = [5, 10, 20, 50];
|
||||
const results = [];
|
||||
|
||||
for (const depth of testDepths) {
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<ID>RECURSIVE-001</ID>
|
||||
<Items>
|
||||
${createRecursiveStructure(depth)}
|
||||
</Items>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const parsed = await einvoice.parseXML(xml);
|
||||
const endTime = Date.now();
|
||||
|
||||
// Count actual depth
|
||||
let actualDepth = 0;
|
||||
let current = parsed;
|
||||
while (current?.Items || current?.SubItems) {
|
||||
actualDepth++;
|
||||
current = current.Items || current.SubItems;
|
||||
}
|
||||
|
||||
results.push({
|
||||
requestedDepth: depth,
|
||||
actualDepth,
|
||||
success: true,
|
||||
timeTaken: endTime - startTime
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
requestedDepth: depth,
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
recursiveElementNesting.forEach(result => {
|
||||
t.ok(result.success || result.error, `Recursive depth ${result.requestedDepth} was processed`);
|
||||
});
|
||||
|
||||
// Test 3: Namespace nesting complexity
|
||||
const namespaceNesting = await performanceTracker.measureAsync(
|
||||
'namespace-nesting-complexity',
|
||||
async () => {
|
||||
const createNamespaceNesting = (depth: number): string => {
|
||||
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||
|
||||
// Create nested elements with different namespaces
|
||||
for (let i = 0; i < depth; i++) {
|
||||
xml += ' '.repeat(i) + `<ns${i}:Element xmlns:ns${i}="http://example.com/ns${i}">\n`;
|
||||
}
|
||||
|
||||
xml += ' '.repeat(depth) + '<Data>Content</Data>\n';
|
||||
|
||||
// Close all namespace elements
|
||||
for (let i = depth - 1; i >= 0; i--) {
|
||||
xml += ' '.repeat(i) + `</ns${i}:Element>\n`;
|
||||
}
|
||||
|
||||
return xml;
|
||||
};
|
||||
|
||||
const testDepths = [5, 10, 25, 50, 100];
|
||||
const results = [];
|
||||
|
||||
for (const depth of testDepths) {
|
||||
const xml = createNamespaceNesting(depth);
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const parsed = await einvoice.parseXML(xml);
|
||||
const endTime = Date.now();
|
||||
|
||||
results.push({
|
||||
depth,
|
||||
success: true,
|
||||
timeTaken: endTime - startTime,
|
||||
namespacesPreserved: true // Check if namespaces were preserved
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
depth,
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
namespaceNesting.forEach(result => {
|
||||
if (result.depth <= 50) {
|
||||
t.ok(result.success, `Namespace depth ${result.depth} should be handled`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test 4: Mixed content deep nesting
|
||||
const mixedContentNesting = await performanceTracker.measureAsync(
|
||||
'mixed-content-deep-nesting',
|
||||
async () => {
|
||||
const createMixedNesting = (depth: number): string => {
|
||||
let xml = '';
|
||||
|
||||
for (let i = 0; i < depth; i++) {
|
||||
xml += `<Level${i}>Text before `;
|
||||
}
|
||||
|
||||
xml += '<Value>Core Value</Value>';
|
||||
|
||||
for (let i = depth - 1; i >= 0; i--) {
|
||||
xml += ` text after</Level${i}>`;
|
||||
}
|
||||
|
||||
return xml;
|
||||
};
|
||||
|
||||
const testCases = [10, 50, 100, 500];
|
||||
const results = [];
|
||||
|
||||
for (const depth of testCases) {
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
<MixedContent>
|
||||
${createMixedNesting(depth)}
|
||||
</MixedContent>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const parsed = await einvoice.parseXML(xml);
|
||||
|
||||
results.push({
|
||||
depth,
|
||||
success: true,
|
||||
hasMixedContent: true
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
depth,
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
mixedContentNesting.forEach(result => {
|
||||
t.ok(result.success || result.error, `Mixed content depth ${result.depth} was handled`);
|
||||
});
|
||||
|
||||
// Test 5: Attribute-heavy deep nesting
|
||||
const attributeHeavyNesting = await performanceTracker.measureAsync(
|
||||
'attribute-heavy-nesting',
|
||||
async () => {
|
||||
const createAttributeNesting = (depth: number, attrsPerLevel: number): string => {
|
||||
let xml = '';
|
||||
|
||||
for (let i = 0; i < depth; i++) {
|
||||
xml += `<Element${i}`;
|
||||
|
||||
// Add multiple attributes at each level
|
||||
for (let j = 0; j < attrsPerLevel; j++) {
|
||||
xml += ` attr${j}="value${i}_${j}"`;
|
||||
}
|
||||
|
||||
xml += '>';
|
||||
}
|
||||
|
||||
xml += 'Content';
|
||||
|
||||
for (let i = depth - 1; i >= 0; i--) {
|
||||
xml += `</Element${i}>`;
|
||||
}
|
||||
|
||||
return xml;
|
||||
};
|
||||
|
||||
const testCases = [
|
||||
{ depth: 10, attrs: 10 },
|
||||
{ depth: 50, attrs: 5 },
|
||||
{ depth: 100, attrs: 3 },
|
||||
{ depth: 500, attrs: 1 }
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const test of testCases) {
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice>
|
||||
${createAttributeNesting(test.depth, test.attrs)}
|
||||
</Invoice>`;
|
||||
|
||||
const startTime = Date.now();
|
||||
const startMemory = process.memoryUsage();
|
||||
|
||||
try {
|
||||
await einvoice.parseXML(xml);
|
||||
|
||||
const endTime = Date.now();
|
||||
const endMemory = process.memoryUsage();
|
||||
|
||||
results.push({
|
||||
depth: test.depth,
|
||||
attributesPerLevel: test.attrs,
|
||||
totalAttributes: test.depth * test.attrs,
|
||||
success: true,
|
||||
timeTaken: endTime - startTime,
|
||||
memoryUsed: endMemory.heapUsed - startMemory.heapUsed
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
depth: test.depth,
|
||||
attributesPerLevel: test.attrs,
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
attributeHeavyNesting.forEach(result => {
|
||||
t.ok(result.success || result.error,
|
||||
`Attribute-heavy nesting (depth: ${result.depth}, attrs: ${result.attributesPerLevel}) was processed`);
|
||||
});
|
||||
|
||||
// Test 6: CDATA section nesting
|
||||
const cdataNesting = await performanceTracker.measureAsync(
|
||||
'cdata-section-nesting',
|
||||
async () => {
|
||||
const depths = [5, 10, 20, 50];
|
||||
const results = [];
|
||||
|
||||
for (const depth of depths) {
|
||||
let xml = '<?xml version="1.0" encoding="UTF-8"?><Invoice>';
|
||||
|
||||
// Create nested elements with CDATA
|
||||
for (let i = 0; i < depth; i++) {
|
||||
xml += `<Level${i}><![CDATA[Data at level ${i} with <special> characters & symbols]]>`;
|
||||
}
|
||||
|
||||
// Close all elements
|
||||
for (let i = depth - 1; i >= 0; i--) {
|
||||
xml += `</Level${i}>`;
|
||||
}
|
||||
|
||||
xml += '</Invoice>';
|
||||
|
||||
try {
|
||||
const parsed = await einvoice.parseXML(xml);
|
||||
|
||||
results.push({
|
||||
depth,
|
||||
success: true,
|
||||
cdataPreserved: true
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
depth,
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
cdataNesting.forEach(result => {
|
||||
t.ok(result.success, `CDATA nesting depth ${result.depth} should be handled`);
|
||||
});
|
||||
|
||||
// Test 7: Processing instruction nesting
|
||||
const processingInstructionNesting = await performanceTracker.measureAsync(
|
||||
'processing-instruction-nesting',
|
||||
async () => {
|
||||
const createPINesting = (depth: number): string => {
|
||||
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||
|
||||
for (let i = 0; i < depth; i++) {
|
||||
xml += `<?process-level-${i} instruction="value"?>\n`;
|
||||
xml += `<Level${i}>\n`;
|
||||
}
|
||||
|
||||
xml += '<Data>Content</Data>\n';
|
||||
|
||||
for (let i = depth - 1; i >= 0; i--) {
|
||||
xml += `</Level${i}>\n`;
|
||||
}
|
||||
|
||||
return xml;
|
||||
};
|
||||
|
||||
const depths = [10, 25, 50];
|
||||
const results = [];
|
||||
|
||||
for (const depth of depths) {
|
||||
const xml = createPINesting(depth);
|
||||
|
||||
try {
|
||||
const parsed = await einvoice.parseXML(xml);
|
||||
|
||||
results.push({
|
||||
depth,
|
||||
success: true,
|
||||
processingInstructionsHandled: true
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
depth,
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
processingInstructionNesting.forEach(result => {
|
||||
t.ok(result.success, `PI nesting depth ${result.depth} should be handled`);
|
||||
});
|
||||
|
||||
// Test 8: Real invoice format deep structures
|
||||
const realFormatDeepStructures = await performanceTracker.measureAsync(
|
||||
'real-format-deep-structures',
|
||||
async () => {
|
||||
const formats = ['ubl', 'cii'];
|
||||
const results = [];
|
||||
|
||||
for (const format of formats) {
|
||||
// Create deeply nested invoice structure
|
||||
let invoice;
|
||||
|
||||
if (format === 'ubl') {
|
||||
invoice = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
||||
<ID>DEEP-UBL-001</ID>
|
||||
<Note>
|
||||
<SubNote>
|
||||
<SubSubNote>
|
||||
<Content>
|
||||
<Detail>
|
||||
<SubDetail>
|
||||
<Information>Deeply nested note</Information>
|
||||
</SubDetail>
|
||||
</Detail>
|
||||
</Content>
|
||||
</SubSubNote>
|
||||
</SubNote>
|
||||
</Note>
|
||||
<InvoiceLine>
|
||||
<Item>
|
||||
<AdditionalItemProperty>
|
||||
<Value>
|
||||
<SubValue>
|
||||
<Detail>
|
||||
<SubDetail>
|
||||
<Information>Deep item property</Information>
|
||||
</SubDetail>
|
||||
</Detail>
|
||||
</SubValue>
|
||||
</Value>
|
||||
</AdditionalItemProperty>
|
||||
</Item>
|
||||
</InvoiceLine>
|
||||
</Invoice>`;
|
||||
} else {
|
||||
invoice = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
|
||||
<rsm:ExchangedDocument>
|
||||
<ram:ID>DEEP-CII-001</ram:ID>
|
||||
<ram:IncludedNote>
|
||||
<ram:Content>
|
||||
<ram:SubContent>
|
||||
<ram:Detail>
|
||||
<ram:SubDetail>
|
||||
<ram:Information>Deep CII structure</ram:Information>
|
||||
</ram:SubDetail>
|
||||
</ram:Detail>
|
||||
</ram:SubContent>
|
||||
</ram:Content>
|
||||
</ram:IncludedNote>
|
||||
</rsm:ExchangedDocument>
|
||||
</rsm:CrossIndustryInvoice>`;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = await einvoice.parseDocument(invoice);
|
||||
const validated = await einvoice.validate(parsed);
|
||||
|
||||
results.push({
|
||||
format,
|
||||
parsed: true,
|
||||
valid: validated?.isValid || false,
|
||||
deepStructureSupported: true
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
format,
|
||||
parsed: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
realFormatDeepStructures.forEach(result => {
|
||||
t.ok(result.parsed, `${result.format} deep structure should be parsed`);
|
||||
});
|
||||
|
||||
// Test 9: Stack overflow protection
|
||||
const stackOverflowProtection = await performanceTracker.measureAsync(
|
||||
'stack-overflow-protection',
|
||||
async () => {
|
||||
const extremeDepths = [10000, 50000, 100000];
|
||||
const results = [];
|
||||
|
||||
for (const depth of extremeDepths) {
|
||||
// Create extremely deep structure efficiently
|
||||
const parts = [];
|
||||
parts.push('<?xml version="1.0" encoding="UTF-8"?>');
|
||||
|
||||
// Opening tags
|
||||
for (let i = 0; i < Math.min(depth, 1000); i++) {
|
||||
parts.push(`<L${i}>`);
|
||||
}
|
||||
|
||||
parts.push('<Data>Test</Data>');
|
||||
|
||||
// Closing tags
|
||||
for (let i = Math.min(depth - 1, 999); i >= 0; i--) {
|
||||
parts.push(`</L${i}>`);
|
||||
}
|
||||
|
||||
const xml = parts.join('');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
await einvoice.parseXML(xml, { maxDepth: 1000 });
|
||||
|
||||
const endTime = Date.now();
|
||||
|
||||
results.push({
|
||||
depth,
|
||||
protected: true,
|
||||
method: 'depth-limit',
|
||||
timeTaken: endTime - startTime
|
||||
});
|
||||
} catch (error) {
|
||||
const endTime = Date.now();
|
||||
|
||||
results.push({
|
||||
depth,
|
||||
protected: true,
|
||||
method: error.message.includes('depth') ? 'depth-check' : 'stack-guard',
|
||||
timeTaken: endTime - startTime,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
stackOverflowProtection.forEach(result => {
|
||||
t.ok(result.protected, `Stack overflow protection active for depth ${result.depth}`);
|
||||
});
|
||||
|
||||
// Test 10: Performance impact of nesting
|
||||
const nestingPerformanceImpact = await performanceTracker.measureAsync(
|
||||
'nesting-performance-impact',
|
||||
async () => {
|
||||
const depths = [1, 10, 50, 100, 500, 1000];
|
||||
const results = [];
|
||||
|
||||
for (const depth of depths) {
|
||||
// Create invoice with specific nesting depth
|
||||
let xml = '<?xml version="1.0" encoding="UTF-8"?><Invoice>';
|
||||
|
||||
// Create structure at depth
|
||||
let current = xml;
|
||||
for (let i = 0; i < depth; i++) {
|
||||
current += `<Item${i}>`;
|
||||
}
|
||||
|
||||
current += '<ID>TEST</ID><Amount>100</Amount>';
|
||||
|
||||
for (let i = depth - 1; i >= 0; i--) {
|
||||
current += `</Item${i}>`;
|
||||
}
|
||||
|
||||
current += '</Invoice>';
|
||||
|
||||
// Measure parsing time
|
||||
const iterations = 10;
|
||||
const times = [];
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const startTime = process.hrtime.bigint();
|
||||
|
||||
try {
|
||||
await einvoice.parseXML(current);
|
||||
} catch (error) {
|
||||
// Ignore errors for performance testing
|
||||
}
|
||||
|
||||
const endTime = process.hrtime.bigint();
|
||||
times.push(Number(endTime - startTime) / 1000000); // Convert to ms
|
||||
}
|
||||
|
||||
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
|
||||
const minTime = Math.min(...times);
|
||||
const maxTime = Math.max(...times);
|
||||
|
||||
results.push({
|
||||
depth,
|
||||
avgTime,
|
||||
minTime,
|
||||
maxTime,
|
||||
complexity: avgTime / depth // Time per nesting level
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
// Verify performance doesn't degrade exponentially
|
||||
const complexities = nestingPerformanceImpact.map(r => r.complexity);
|
||||
const avgComplexity = complexities.reduce((a, b) => a + b, 0) / complexities.length;
|
||||
|
||||
nestingPerformanceImpact.forEach(result => {
|
||||
t.ok(result.complexity < avgComplexity * 10,
|
||||
`Nesting depth ${result.depth} has reasonable performance`);
|
||||
});
|
||||
|
||||
// Print performance summary
|
||||
performanceTracker.printSummary();
|
||||
});
|
||||
|
||||
// Run the test
|
||||
tap.start();
|
Reference in New Issue
Block a user