This commit is contained in:
2025-05-28 08:40:26 +00:00
parent e4c762658d
commit 32f8bc192a
24 changed files with 3350 additions and 5416 deletions

View File

@ -4,424 +4,472 @@ import * as plugins from '../../plugins.js';
import { CorpusLoader } from '../../helpers/corpus.loader.js';
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
tap.test('PARSE-01: Well-Formed XML Parsing - Parse valid XML documents correctly', async (t) => {
const performanceTracker = new PerformanceTracker('PARSE-01');
const corpusLoader = new CorpusLoader();
await t.test('Basic XML structure parsing', async () => {
performanceTracker.startOperation('basic-xml-parsing');
const testCases = [
{
name: 'Minimal invoice',
xml: '<?xml version="1.0" encoding="UTF-8"?>\n<invoice><id>TEST-001</id></invoice>',
expectedStructure: {
hasDeclaration: true,
rootElement: 'invoice',
hasChildren: true
}
},
{
name: 'Invoice with namespaces',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<ubl:Invoice xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<cbc:ID xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">TEST-002</cbc:ID>
tap.test('PARSE-01: Basic XML structure parsing', async () => {
const testCases = [
{
name: 'Minimal invoice',
xml: '<?xml version="1.0" encoding="UTF-8"?>\n<invoice><id>TEST-001</id></invoice>',
expectedId: null // Generic invoice element not recognized
},
{
name: 'Invoice with namespaces',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<ubl:Invoice xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>TEST-002</cbc:ID>
</ubl:Invoice>`,
expectedStructure: {
hasNamespaces: true,
namespaceCount: 2,
rootNamespace: 'ubl'
}
},
{
name: 'Complex nested structure',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<invoice>
<header>
<id>TEST-003</id>
<date>2024-01-01</date>
</header>
<body>
<lines>
<line number="1">
<description>Product A</description>
<amount>100.00</amount>
</line>
<line number="2">
<description>Product B</description>
<amount>200.00</amount>
</line>
</lines>
</body>
</invoice>`,
expectedStructure: {
maxDepth: 4,
lineCount: 2
}
},
{
name: 'Invoice with attributes',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<invoice version="1.0" format="UBL" schemaLocation="http://example.com/invoice.xsd">
<id type="commercial">TEST-004</id>
<amount currency="EUR" decimals="2">1000.00</amount>
</invoice>`,
expectedStructure: {
hasAttributes: true,
attributeCount: 5 // 3 on invoice, 1 on id, 2 on amount
}
}
];
for (const testCase of testCases) {
const startTime = performance.now();
try {
const invoice = new einvoice.EInvoice();
if (invoice.fromXmlString) {
await invoice.fromXmlString(testCase.xml);
console.log(`${testCase.name}: Parsed successfully`);
// Verify parsed data if available
if (invoice.data?.id) {
console.log(` Extracted ID: ${invoice.data.id}`);
}
} else {
console.log(`⚠️ ${testCase.name}: fromXmlString method not implemented`);
}
} catch (error) {
console.log(`${testCase.name}: Parsing failed - ${error.message}`);
}
performanceTracker.recordMetric('xml-parse', performance.now() - startTime);
}
performanceTracker.endOperation('basic-xml-parsing');
});
await t.test('Character data handling', async () => {
performanceTracker.startOperation('character-data');
const characterTests = [
{
name: 'Text content with special characters',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<invoice>
<supplier>Müller & Co. GmbH</supplier>
<description>Product with 50% discount & free shipping</description>
<note><![CDATA[Special offer: Buy 2 & get 1 free!]]></note>
</invoice>`
},
{
name: 'Mixed content',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<invoice>
<description>
This is a <bold>mixed</bold> content with <italic>inline</italic> elements.
</description>
</invoice>`
},
{
name: 'Whitespace preservation',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<invoice>
<address xml:space="preserve">
Line 1
Line 2
Line 3
</address>
</invoice>`
},
{
name: 'Empty elements',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<invoice>
<optional-field/>
<another-field></another-field>
<amount>0</amount>
</invoice>`
}
];
for (const test of characterTests) {
const startTime = performance.now();
try {
const invoice = new einvoice.EInvoice();
if (invoice.fromXmlString) {
await invoice.fromXmlString(test.xml);
console.log(`${test.name}: Character data handled correctly`);
} else {
console.log(`⚠️ ${test.name}: Cannot test without fromXmlString`);
}
} catch (error) {
console.log(`${test.name}: Failed - ${error.message}`);
}
performanceTracker.recordMetric('character-handling', performance.now() - startTime);
}
performanceTracker.endOperation('character-data');
});
await t.test('XML comments and processing instructions', async () => {
performanceTracker.startOperation('comments-pi');
const xmlWithComments = `<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="invoice.xsl"?>
<!-- This is a test invoice -->
<invoice>
<!-- Header section -->
<header>
<id>TEST-005</id>
<!-- TODO: Add more fields -->
</header>
<!-- Body section -->
<body>
<amount>100.00</amount>
</body>
<!-- End of invoice -->
</invoice>
<!-- Processing complete -->`;
const startTime = performance.now();
try {
const invoice = new einvoice.EInvoice();
if (invoice.fromXmlString) {
await invoice.fromXmlString(xmlWithComments);
console.log('✓ XML with comments and processing instructions parsed');
} else {
console.log('⚠️ Cannot test comments/PI without fromXmlString');
}
} catch (error) {
console.log(`✗ Comments/PI parsing failed: ${error.message}`);
}
performanceTracker.recordMetric('comments-pi', performance.now() - startTime);
performanceTracker.endOperation('comments-pi');
});
await t.test('Namespace handling', async () => {
performanceTracker.startOperation('namespace-handling');
const namespaceTests = [
{
name: 'Default namespace',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TEST-006</ID>
</Invoice>`
},
{
name: 'Multiple namespaces',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<ubl:Invoice
xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>TEST-007</cbc:ID>
expectedId: 'TEST-002'
},
{
name: 'XRechnung UBL invoice',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<ubl:Invoice xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID>TEST-003</cbc:ID>
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:Name>Test Supplier</cbc:Name>
<cac:PartyName>
<cbc:Name>Test Supplier</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:CityName>Berlin</cbc:CityName>
<cbc:PostalZone>10115</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
</cac:Party>
</cac:AccountingSupplierParty>
</ubl:Invoice>`
},
{
name: 'Namespace inheritance',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<root xmlns:ns1="http://example.com/ns1">
<ns1:parent>
<ns1:child>
<grandchild>Inherits ns1</grandchild>
</ns1:child>
</ns1:parent>
</root>`
}
];
for (const test of namespaceTests) {
const startTime = performance.now();
try {
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Test Customer</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:CityName>Munich</cbc:CityName>
<cbc:PostalZone>80331</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="EA">1</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Test Product</cbc:Name>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">100.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:LegalMonetaryTotal>
<cbc:TaxInclusiveAmount currencyID="EUR">119.00</cbc:TaxInclusiveAmount>
</cac:LegalMonetaryTotal>
</ubl:Invoice>`,
expectedId: 'TEST-003'
}
];
for (const testCase of testCases) {
const { result, metric } = await PerformanceTracker.track(
'xml-parsing',
async () => {
const invoice = new einvoice.EInvoice();
if (invoice.fromXmlString) {
await invoice.fromXmlString(test.xml);
console.log(`${test.name}: Namespace parsing successful`);
} else {
console.log(`⚠️ ${test.name}: Cannot test without fromXmlString`);
try {
await invoice.fromXmlString(testCase.xml);
return {
success: true,
id: invoice.id,
hasFrom: !!invoice.from,
hasTo: !!invoice.to,
itemCount: invoice.items?.length || 0
};
} catch (error) {
return {
success: false,
error: error.message
};
}
} catch (error) {
console.log(`${test.name}: Failed - ${error.message}`);
}
performanceTracker.recordMetric('namespace-parsing', performance.now() - startTime);
}
);
performanceTracker.endOperation('namespace-handling');
});
await t.test('Corpus well-formed XML parsing', async () => {
performanceTracker.startOperation('corpus-parsing');
console.log(`${testCase.name}: ${result.success ? '✓' : '✗'}`);
const xmlFiles = await corpusLoader.getFiles(/\.xml$/);
console.log(`\nTesting ${xmlFiles.length} XML files from corpus...`);
const results = {
total: 0,
success: 0,
failed: 0,
avgParseTime: 0
};
const sampleSize = Math.min(50, xmlFiles.length);
const sampledFiles = xmlFiles.slice(0, sampleSize);
let totalParseTime = 0;
for (const file of sampledFiles) {
results.total++;
const startTime = performance.now();
try {
const content = await plugins.fs.readFile(file.path, 'utf8');
const invoice = new einvoice.EInvoice();
if (invoice.fromXmlString) {
await invoice.fromXmlString(content);
results.success++;
} else {
// Fallback: just check if it's valid XML
if (content.includes('<?xml') && content.includes('>')) {
results.success++;
}
}
} catch (error) {
results.failed++;
console.log(` Failed: ${file.name} - ${error.message}`);
}
const parseTime = performance.now() - startTime;
totalParseTime += parseTime;
performanceTracker.recordMetric('file-parse', parseTime);
}
results.avgParseTime = totalParseTime / results.total;
console.log('\nCorpus Parsing Results:');
console.log(`Total files tested: ${results.total}`);
console.log(`Successfully parsed: ${results.success} (${(results.success/results.total*100).toFixed(1)}%)`);
console.log(`Failed to parse: ${results.failed}`);
console.log(`Average parse time: ${results.avgParseTime.toFixed(2)}ms`);
expect(results.success).toBeGreaterThan(results.total * 0.9); // Expect >90% success rate
performanceTracker.endOperation('corpus-parsing');
});
await t.test('DTD and entity references', async () => {
performanceTracker.startOperation('dtd-entities');
const xmlWithEntities = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE invoice [
<!ENTITY company "Test Company Ltd.">
<!ENTITY copy "&#169;">
<!ENTITY euro "&#8364;">
]>
<invoice>
<supplier>&company;</supplier>
<copyright>&copy; 2024 &company;</copyright>
<amount currency="EUR">&euro;1000.00</amount>
</invoice>`;
const startTime = performance.now();
try {
const invoice = new einvoice.EInvoice();
if (invoice.fromXmlString) {
await invoice.fromXmlString(xmlWithEntities);
console.log('✓ XML with DTD and entities parsed');
if (testCase.expectedId !== null) {
if (result.success) {
expect(result.id).toEqual(testCase.expectedId);
console.log(` ID: ${result.id}`);
console.log(` Has supplier: ${result.hasFrom}`);
console.log(` Has customer: ${result.hasTo}`);
console.log(` Item count: ${result.itemCount}`);
} else {
console.log('⚠️ Cannot test DTD/entities without fromXmlString');
console.log(` Error: ${result.error}`);
}
} catch (error) {
console.log(`⚠️ DTD/entity parsing: ${error.message}`);
// This might fail due to security restrictions, which is acceptable
}
performanceTracker.recordMetric('dtd-parsing', performance.now() - startTime);
performanceTracker.endOperation('dtd-entities');
});
await t.test('Large XML structure stress test', async () => {
performanceTracker.startOperation('large-xml-test');
// Generate a large but well-formed XML
const generateLargeXml = (lineCount: number): string => {
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<invoice>\n';
xml += ' <header><id>LARGE-001</id></header>\n';
xml += ' <lines>\n';
for (let i = 1; i <= lineCount; i++) {
xml += ` <line number="${i}">
<description>Product ${i}</description>
<quantity>1</quantity>
<price>10.00</price>
<amount>10.00</amount>
</line>\n`;
}
xml += ' </lines>\n';
xml += ` <total>${lineCount * 10}.00</total>\n`;
xml += '</invoice>';
return xml;
};
const testSizes = [10, 100, 1000];
for (const size of testSizes) {
const startTime = performance.now();
const largeXml = generateLargeXml(size);
try {
const invoice = new einvoice.EInvoice();
if (invoice.fromXmlString) {
await invoice.fromXmlString(largeXml);
const parseTime = performance.now() - startTime;
console.log(`✓ Parsed ${size} line items in ${parseTime.toFixed(2)}ms`);
console.log(` Parse rate: ${(size / parseTime * 1000).toFixed(0)} items/second`);
} else {
console.log(`⚠️ Cannot test large XML without fromXmlString`);
}
} catch (error) {
console.log(`✗ Failed with ${size} items: ${error.message}`);
}
performanceTracker.recordMetric(`large-xml-${size}`, performance.now() - startTime);
}
performanceTracker.endOperation('large-xml-test');
});
// Performance summary
console.log('\n' + performanceTracker.getSummary());
// Parsing best practices
console.log('\nXML Parsing Best Practices:');
console.log('1. Always validate XML declaration and encoding');
console.log('2. Handle namespaces correctly throughout the document');
console.log('3. Preserve significant whitespace when required');
console.log('4. Process comments and PIs appropriately');
console.log('5. Handle empty elements consistently');
console.log('6. Be cautious with DTD processing (security implications)');
console.log('7. Optimize for large documents with streaming when possible');
console.log(` Parse time: ${metric.duration.toFixed(2)}ms`);
}
});
tap.test('PARSE-01: Character encoding handling', async () => {
const encodingTests = [
{
name: 'UTF-8 with special characters',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<ubl:Invoice xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>UTF8-TEST</cbc:ID>
<cbc:Note>Special chars: äöü ñ € « » 中文</cbc:Note>
</ubl:Invoice>`,
expectedNote: 'Special chars: äöü ñ € « » 中文'
},
{
name: 'ISO-8859-1 declaration',
xml: `<?xml version="1.0" encoding="ISO-8859-1"?>
<ubl:Invoice xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:ID>ISO-TEST</cbc:ID>
<cbc:Note>Latin-1 chars: àèìòù</cbc:Note>
</ubl:Invoice>`,
expectedNote: 'Latin-1 chars: àèìòù'
}
];
for (const test of encodingTests) {
const { result } = await PerformanceTracker.track(
'encoding-test',
async () => {
const invoice = new einvoice.EInvoice();
try {
await invoice.fromXmlString(test.xml);
return {
success: true,
notes: invoice.notes,
id: invoice.id
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
);
console.log(`${test.name}: ${result.success ? '✓' : '✗'}`);
if (result.success) {
expect(result.notes).toBeDefined();
if (result.notes && result.notes.length > 0) {
expect(result.notes[0]).toEqual(test.expectedNote);
console.log(` Note preserved: ${result.notes[0]}`);
}
}
}
});
tap.test('PARSE-01: Namespace handling', async () => {
const namespaceTests = [
{
name: 'Multiple namespace declarations',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>NS-TEST-001</ram:ID>
</rsm:ExchangedDocument>
</rsm:CrossIndustryInvoice>`,
expectedFormat: einvoice.InvoiceFormat.FACTURX,
expectedId: 'NS-TEST-001'
},
{
name: 'Default namespace',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">DEFAULT-NS-TEST</ID>
</Invoice>`,
expectedFormat: einvoice.InvoiceFormat.UBL,
expectedId: 'DEFAULT-NS-TEST'
}
];
for (const test of namespaceTests) {
const { result } = await PerformanceTracker.track(
'namespace-test',
async () => {
const invoice = new einvoice.EInvoice();
try {
await invoice.fromXmlString(test.xml);
return {
success: true,
format: invoice.getFormat(),
id: invoice.id
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
);
console.log(`${test.name}: ${result.success ? '✓' : '✗'}`);
if (result.success) {
expect(result.format).toEqual(test.expectedFormat);
expect(result.id).toEqual(test.expectedId);
console.log(` Detected format: ${einvoice.InvoiceFormat[result.format]}`);
console.log(` ID: ${result.id}`);
}
}
});
tap.test('PARSE-01: Large XML file parsing', async () => {
// Generate a large invoice with many line items
const generateLargeInvoice = (lineCount: number): string => {
const lines = [];
for (let i = 1; i <= lineCount; i++) {
lines.push(`
<cac:InvoiceLine>
<cbc:ID>${i}</cbc:ID>
<cbc:InvoicedQuantity unitCode="EA">${i}</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">${(i * 10).toFixed(2)}</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Product ${i}</cbc:Name>
<cbc:Description>Description for product ${i} with some additional text to make it larger</cbc:Description>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">10.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>`);
}
return `<?xml version="1.0" encoding="UTF-8"?>
<ubl:Invoice xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
<cbc:ID>LARGE-INVOICE-${lineCount}</cbc:ID>
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Large Supplier Inc</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:CityName>Berlin</cbc:CityName>
<cbc:PostalZone>10115</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Large Customer Corp</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:CityName>Munich</cbc:CityName>
<cbc:PostalZone>80331</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
</cac:Party>
</cac:AccountingCustomerParty>
${lines.join('')}
</ubl:Invoice>`;
};
const sizes = [10, 100, 1000];
for (const size of sizes) {
const xml = generateLargeInvoice(size);
const xmlSize = Buffer.byteLength(xml, 'utf-8') / 1024; // KB
const { result, metric } = await PerformanceTracker.track(
`parse-${size}-lines`,
async () => {
const invoice = new einvoice.EInvoice();
try {
await invoice.fromXmlString(xml);
return {
success: true,
itemCount: invoice.items?.length || 0,
memoryUsed: metric?.memory?.used || 0
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
);
console.log(`Parse ${size} line items (${xmlSize.toFixed(1)}KB): ${result.success ? '✓' : '✗'}`);
if (result.success) {
expect(result.itemCount).toEqual(size);
console.log(` Items parsed: ${result.itemCount}`);
console.log(` Parse time: ${metric.duration.toFixed(2)}ms`);
console.log(` Memory used: ${(metric.memory.used / 1024 / 1024).toFixed(2)}MB`);
console.log(` Speed: ${(xmlSize / metric.duration * 1000).toFixed(2)}KB/s`);
}
}
});
tap.test('PARSE-01: Real corpus file parsing', async () => {
// Try to load some real files from the corpus
const testFiles = [
{ category: 'UBL_XMLRECHNUNG', file: 'XRECHNUNG_Einfach.ubl.xml' },
{ category: 'CII_XMLRECHNUNG', file: 'XRECHNUNG_Einfach.cii.xml' },
{ category: 'ZUGFERDV2_CORRECT', file: null } // Will use first available
];
for (const testFile of testFiles) {
try {
let xmlContent: string;
if (testFile.file) {
xmlContent = await CorpusLoader.loadTestFile(testFile.category, testFile.file);
} else {
const files = await CorpusLoader.getCorpusFiles(testFile.category);
if (files.length > 0) {
xmlContent = await CorpusLoader.loadTestFile(testFile.category, files[0]);
} else {
console.log(`No files found in category ${testFile.category}`);
continue;
}
}
const { result, metric } = await PerformanceTracker.track(
'corpus-parsing',
async () => {
const invoice = new einvoice.EInvoice();
try {
await invoice.fromXmlString(xmlContent);
return {
success: true,
format: invoice.getFormat(),
id: invoice.id,
hasData: !!invoice.from && !!invoice.to && invoice.items?.length > 0
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
);
console.log(`${testFile.category}/${testFile.file || 'first-file'}: ${result.success ? '✓' : '✗'}`);
if (result.success) {
console.log(` Format: ${einvoice.InvoiceFormat[result.format]}`);
console.log(` ID: ${result.id}`);
console.log(` Has complete data: ${result.hasData}`);
console.log(` Parse time: ${metric.duration.toFixed(2)}ms`);
} else {
console.log(` Error: ${result.error}`);
}
} catch (error) {
console.log(`Failed to load ${testFile.category}/${testFile.file}: ${error.message}`);
}
}
});
tap.test('PARSE-01: Error recovery', async () => {
const errorCases = [
{
name: 'Empty XML',
xml: '',
expectError: true
},
{
name: 'Invalid XML syntax',
xml: '<?xml version="1.0"?><invoice><id>TEST</id><invoice>',
expectError: true
},
{
name: 'Non-invoice XML',
xml: '<?xml version="1.0"?><root><data>test</data></root>',
expectError: true
},
{
name: 'Missing mandatory fields',
xml: `<?xml version="1.0"?>
<ubl:Invoice xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<!-- Missing ID and other required fields -->
</ubl:Invoice>`,
expectError: true
}
];
for (const testCase of errorCases) {
const { result } = await PerformanceTracker.track(
'error-recovery',
async () => {
const invoice = new einvoice.EInvoice();
try {
await invoice.fromXmlString(testCase.xml);
return { success: true };
} catch (error) {
return {
success: false,
error: error.message,
errorType: error.constructor.name
};
}
}
);
console.log(`${testCase.name}: ${testCase.expectError ? (result.success ? '✗' : '✓') : (result.success ? '✓' : '✗')}`);
if (testCase.expectError) {
expect(result.success).toBeFalse();
console.log(` Error type: ${result.errorType}`);
console.log(` Error message: ${result.error}`);
} else {
expect(result.success).toBeTrue();
}
}
});
tap.test('PARSE-01: Performance summary', async () => {
const stats = PerformanceTracker.getStats('xml-parsing');
if (stats) {
console.log('\nPerformance Summary:');
console.log(` Total parses: ${stats.count}`);
console.log(` Average time: ${stats.avg.toFixed(2)}ms`);
console.log(` Min time: ${stats.min.toFixed(2)}ms`);
console.log(` Max time: ${stats.max.toFixed(2)}ms`);
console.log(` P95 time: ${stats.p95.toFixed(2)}ms`);
// Check against thresholds
expect(stats.avg).toBeLessThan(50); // 50ms average for small files
expect(stats.p95).toBeLessThan(100); // 100ms for 95th percentile
}
});
// Run the tests
tap.start();