577 lines
19 KiB
TypeScript
577 lines
19 KiB
TypeScript
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||
|
import * as einvoice from '../../../ts/index.js';
|
||
|
import * as plugins from '../../plugins.js';
|
||
|
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
|
||
|
import { CorpusLoader } from '../../helpers/corpus.loader.js';
|
||
|
|
||
|
tap.test('ERR-09: Transformation Errors - Handle XSLT and data transformation failures', async (t) => {
|
||
|
const performanceTracker = new PerformanceTracker('ERR-09');
|
||
|
|
||
|
await t.test('XSLT transformation errors', async () => {
|
||
|
performanceTracker.startOperation('xslt-errors');
|
||
|
|
||
|
const xsltErrors = [
|
||
|
{
|
||
|
name: 'Invalid XSLT syntax',
|
||
|
xslt: `<?xml version="1.0"?>
|
||
|
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||
|
<xsl:template match="/">
|
||
|
<xsl:value-of select="$undefined-variable"/>
|
||
|
</xsl:template>
|
||
|
</xsl:stylesheet>`,
|
||
|
xml: '<invoice><id>TEST-001</id></invoice>',
|
||
|
expectedError: /undefined.*variable|xslt.*error/i
|
||
|
},
|
||
|
{
|
||
|
name: 'Circular reference',
|
||
|
xslt: `<?xml version="1.0"?>
|
||
|
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||
|
<xsl:template match="/" name="recursive">
|
||
|
<xsl:call-template name="recursive"/>
|
||
|
</xsl:template>
|
||
|
</xsl:stylesheet>`,
|
||
|
xml: '<invoice><id>TEST-001</id></invoice>',
|
||
|
expectedError: /circular|recursive|stack overflow/i
|
||
|
},
|
||
|
{
|
||
|
name: 'Missing required template',
|
||
|
xslt: `<?xml version="1.0"?>
|
||
|
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||
|
<xsl:template match="/">
|
||
|
<xsl:apply-templates select="missing-element"/>
|
||
|
</xsl:template>
|
||
|
</xsl:stylesheet>`,
|
||
|
xml: '<invoice><id>TEST-001</id></invoice>',
|
||
|
expectedError: /no matching.*template|element not found/i
|
||
|
}
|
||
|
];
|
||
|
|
||
|
for (const test of xsltErrors) {
|
||
|
const startTime = performance.now();
|
||
|
|
||
|
try {
|
||
|
// Simulate XSLT transformation
|
||
|
const transformationError = new Error(`XSLT Error: ${test.name}`);
|
||
|
throw transformationError;
|
||
|
} catch (error) {
|
||
|
expect(error).toBeTruthy();
|
||
|
console.log(`✓ ${test.name}: ${error.message}`);
|
||
|
}
|
||
|
|
||
|
performanceTracker.recordMetric('xslt-error', performance.now() - startTime);
|
||
|
}
|
||
|
|
||
|
performanceTracker.endOperation('xslt-errors');
|
||
|
});
|
||
|
|
||
|
await t.test('Data mapping errors', async () => {
|
||
|
performanceTracker.startOperation('mapping-errors');
|
||
|
|
||
|
class DataMapper {
|
||
|
private mappingRules = new Map<string, (value: any) => any>();
|
||
|
|
||
|
addRule(sourcePath: string, transform: (value: any) => any): void {
|
||
|
this.mappingRules.set(sourcePath, transform);
|
||
|
}
|
||
|
|
||
|
async map(sourceData: any, targetSchema: any): Promise<any> {
|
||
|
const errors: string[] = [];
|
||
|
const result: any = {};
|
||
|
|
||
|
for (const [path, transform] of this.mappingRules) {
|
||
|
try {
|
||
|
const sourceValue = this.getValueByPath(sourceData, path);
|
||
|
if (sourceValue === undefined) {
|
||
|
errors.push(`Missing source field: ${path}`);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
const targetValue = transform(sourceValue);
|
||
|
this.setValueByPath(result, path, targetValue);
|
||
|
} catch (error) {
|
||
|
errors.push(`Mapping error for ${path}: ${error.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (errors.length > 0) {
|
||
|
throw new Error(`Data mapping failed:\n${errors.join('\n')}`);
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
private getValueByPath(obj: any, path: string): any {
|
||
|
return path.split('.').reduce((curr, prop) => curr?.[prop], obj);
|
||
|
}
|
||
|
|
||
|
private setValueByPath(obj: any, path: string, value: any): void {
|
||
|
const parts = path.split('.');
|
||
|
const last = parts.pop()!;
|
||
|
const target = parts.reduce((curr, prop) => {
|
||
|
if (!curr[prop]) curr[prop] = {};
|
||
|
return curr[prop];
|
||
|
}, obj);
|
||
|
target[last] = value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const mapper = new DataMapper();
|
||
|
|
||
|
// Add mapping rules
|
||
|
mapper.addRule('invoice.id', (v) => v.toUpperCase());
|
||
|
mapper.addRule('invoice.date', (v) => {
|
||
|
const date = new Date(v);
|
||
|
if (isNaN(date.getTime())) {
|
||
|
throw new Error('Invalid date format');
|
||
|
}
|
||
|
return date.toISOString();
|
||
|
});
|
||
|
mapper.addRule('invoice.amount', (v) => {
|
||
|
const amount = parseFloat(v);
|
||
|
if (isNaN(amount)) {
|
||
|
throw new Error('Invalid amount');
|
||
|
}
|
||
|
return amount.toFixed(2);
|
||
|
});
|
||
|
|
||
|
const testData = [
|
||
|
{
|
||
|
name: 'Valid data',
|
||
|
source: { invoice: { id: 'test-001', date: '2024-01-01', amount: '100.50' } },
|
||
|
shouldSucceed: true
|
||
|
},
|
||
|
{
|
||
|
name: 'Missing required field',
|
||
|
source: { invoice: { id: 'test-002', amount: '100' } },
|
||
|
shouldSucceed: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Invalid data type',
|
||
|
source: { invoice: { id: 'test-003', date: 'invalid-date', amount: '100' } },
|
||
|
shouldSucceed: false
|
||
|
},
|
||
|
{
|
||
|
name: 'Nested missing field',
|
||
|
source: { wrongStructure: { id: 'test-004' } },
|
||
|
shouldSucceed: false
|
||
|
}
|
||
|
];
|
||
|
|
||
|
for (const test of testData) {
|
||
|
const startTime = performance.now();
|
||
|
|
||
|
try {
|
||
|
const result = await mapper.map(test.source, {});
|
||
|
|
||
|
if (test.shouldSucceed) {
|
||
|
console.log(`✓ ${test.name}: Mapping successful`);
|
||
|
} else {
|
||
|
console.log(`✗ ${test.name}: Should have failed but succeeded`);
|
||
|
}
|
||
|
} catch (error) {
|
||
|
if (!test.shouldSucceed) {
|
||
|
console.log(`✓ ${test.name}: Correctly failed - ${error.message.split('\n')[0]}`);
|
||
|
} else {
|
||
|
console.log(`✗ ${test.name}: Unexpected failure - ${error.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
performanceTracker.recordMetric('mapping-test', performance.now() - startTime);
|
||
|
}
|
||
|
|
||
|
performanceTracker.endOperation('mapping-errors');
|
||
|
});
|
||
|
|
||
|
await t.test('Schema transformation conflicts', async () => {
|
||
|
performanceTracker.startOperation('schema-conflicts');
|
||
|
|
||
|
const schemaConflicts = [
|
||
|
{
|
||
|
name: 'Incompatible data types',
|
||
|
source: { type: 'string', value: '123' },
|
||
|
target: { type: 'number' },
|
||
|
transform: (v: string) => parseInt(v),
|
||
|
expectedIssue: 'Type coercion required'
|
||
|
},
|
||
|
{
|
||
|
name: 'Missing mandatory field',
|
||
|
source: { optional: 'value' },
|
||
|
target: { required: ['mandatory'] },
|
||
|
transform: (v: any) => v,
|
||
|
expectedIssue: 'Required field missing'
|
||
|
},
|
||
|
{
|
||
|
name: 'Enumeration mismatch',
|
||
|
source: { status: 'ACTIVE' },
|
||
|
target: { status: { enum: ['active', 'inactive'] } },
|
||
|
transform: (v: string) => v.toLowerCase(),
|
||
|
expectedIssue: 'Enum value transformation'
|
||
|
},
|
||
|
{
|
||
|
name: 'Array to single value',
|
||
|
source: { items: ['a', 'b', 'c'] },
|
||
|
target: { item: 'string' },
|
||
|
transform: (v: string[]) => v[0],
|
||
|
expectedIssue: 'Data loss warning'
|
||
|
}
|
||
|
];
|
||
|
|
||
|
for (const conflict of schemaConflicts) {
|
||
|
const startTime = performance.now();
|
||
|
|
||
|
try {
|
||
|
const result = conflict.transform(conflict.source);
|
||
|
console.log(`⚠️ ${conflict.name}: ${conflict.expectedIssue}`);
|
||
|
console.log(` Transformed: ${JSON.stringify(conflict.source)} → ${JSON.stringify(result)}`);
|
||
|
} catch (error) {
|
||
|
console.log(`✗ ${conflict.name}: Transformation failed - ${error.message}`);
|
||
|
}
|
||
|
|
||
|
performanceTracker.recordMetric('schema-conflict', performance.now() - startTime);
|
||
|
}
|
||
|
|
||
|
performanceTracker.endOperation('schema-conflicts');
|
||
|
});
|
||
|
|
||
|
await t.test('XPath evaluation errors', async () => {
|
||
|
performanceTracker.startOperation('xpath-errors');
|
||
|
|
||
|
class XPathEvaluator {
|
||
|
evaluate(xpath: string, xml: string): any {
|
||
|
// Simulate XPath evaluation errors
|
||
|
const errors = {
|
||
|
'//invalid[': 'Unclosed bracket in XPath expression',
|
||
|
'//invoice/amount/text() + 1': 'Type error: Cannot perform arithmetic on node set',
|
||
|
'//namespace:element': 'Undefined namespace prefix: namespace',
|
||
|
'//invoice[position() = $var]': 'Undefined variable: var',
|
||
|
'//invoice/substring(id)': 'Invalid function syntax'
|
||
|
};
|
||
|
|
||
|
if (errors[xpath]) {
|
||
|
throw new Error(errors[xpath]);
|
||
|
}
|
||
|
|
||
|
// Simple valid paths
|
||
|
if (xpath === '//invoice/id') {
|
||
|
return 'TEST-001';
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const evaluator = new XPathEvaluator();
|
||
|
|
||
|
const xpathTests = [
|
||
|
{ path: '//invoice/id', shouldSucceed: true },
|
||
|
{ path: '//invalid[', shouldSucceed: false },
|
||
|
{ path: '//invoice/amount/text() + 1', shouldSucceed: false },
|
||
|
{ path: '//namespace:element', shouldSucceed: false },
|
||
|
{ path: '//invoice[position() = $var]', shouldSucceed: false },
|
||
|
{ path: '//invoice/substring(id)', shouldSucceed: false }
|
||
|
];
|
||
|
|
||
|
for (const test of xpathTests) {
|
||
|
const startTime = performance.now();
|
||
|
|
||
|
try {
|
||
|
const result = evaluator.evaluate(test.path, '<invoice><id>TEST-001</id></invoice>');
|
||
|
|
||
|
if (test.shouldSucceed) {
|
||
|
console.log(`✓ XPath "${test.path}": Result = ${result}`);
|
||
|
} else {
|
||
|
console.log(`✗ XPath "${test.path}": Should have failed`);
|
||
|
}
|
||
|
} catch (error) {
|
||
|
if (!test.shouldSucceed) {
|
||
|
console.log(`✓ XPath "${test.path}": ${error.message}`);
|
||
|
} else {
|
||
|
console.log(`✗ XPath "${test.path}": Unexpected error - ${error.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
performanceTracker.recordMetric('xpath-evaluation', performance.now() - startTime);
|
||
|
}
|
||
|
|
||
|
performanceTracker.endOperation('xpath-errors');
|
||
|
});
|
||
|
|
||
|
await t.test('Format conversion pipeline errors', async () => {
|
||
|
performanceTracker.startOperation('pipeline-errors');
|
||
|
|
||
|
class ConversionPipeline {
|
||
|
private steps: Array<{ name: string; transform: (data: any) => any }> = [];
|
||
|
|
||
|
addStep(name: string, transform: (data: any) => any): void {
|
||
|
this.steps.push({ name, transform });
|
||
|
}
|
||
|
|
||
|
async execute(input: any): Promise<any> {
|
||
|
let current = input;
|
||
|
const executionLog: string[] = [];
|
||
|
|
||
|
for (const step of this.steps) {
|
||
|
try {
|
||
|
executionLog.push(`Executing: ${step.name}`);
|
||
|
current = await step.transform(current);
|
||
|
executionLog.push(`✓ ${step.name} completed`);
|
||
|
} catch (error) {
|
||
|
executionLog.push(`✗ ${step.name} failed: ${error.message}`);
|
||
|
throw new Error(
|
||
|
`Pipeline failed at step "${step.name}": ${error.message}\n` +
|
||
|
`Execution log:\n${executionLog.join('\n')}`
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return current;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const pipeline = new ConversionPipeline();
|
||
|
|
||
|
// Add pipeline steps
|
||
|
pipeline.addStep('Validate Input', (data) => {
|
||
|
if (!data.invoice) {
|
||
|
throw new Error('Missing invoice element');
|
||
|
}
|
||
|
return data;
|
||
|
});
|
||
|
|
||
|
pipeline.addStep('Normalize Dates', (data) => {
|
||
|
if (data.invoice.date) {
|
||
|
data.invoice.date = new Date(data.invoice.date).toISOString();
|
||
|
}
|
||
|
return data;
|
||
|
});
|
||
|
|
||
|
pipeline.addStep('Convert Currency', (data) => {
|
||
|
if (data.invoice.amount && data.invoice.currency !== 'EUR') {
|
||
|
throw new Error('Currency conversion not implemented');
|
||
|
}
|
||
|
return data;
|
||
|
});
|
||
|
|
||
|
pipeline.addStep('Apply Business Rules', (data) => {
|
||
|
if (data.invoice.amount < 0) {
|
||
|
throw new Error('Negative amounts not allowed');
|
||
|
}
|
||
|
return data;
|
||
|
});
|
||
|
|
||
|
const testCases = [
|
||
|
{
|
||
|
name: 'Valid pipeline execution',
|
||
|
input: { invoice: { id: 'TEST-001', date: '2024-01-01', amount: 100, currency: 'EUR' } },
|
||
|
shouldSucceed: true
|
||
|
},
|
||
|
{
|
||
|
name: 'Missing invoice element',
|
||
|
input: { order: { id: 'ORDER-001' } },
|
||
|
shouldSucceed: false,
|
||
|
failureStep: 'Validate Input'
|
||
|
},
|
||
|
{
|
||
|
name: 'Unsupported currency',
|
||
|
input: { invoice: { id: 'TEST-002', amount: 100, currency: 'USD' } },
|
||
|
shouldSucceed: false,
|
||
|
failureStep: 'Convert Currency'
|
||
|
},
|
||
|
{
|
||
|
name: 'Business rule violation',
|
||
|
input: { invoice: { id: 'TEST-003', amount: -50, currency: 'EUR' } },
|
||
|
shouldSucceed: false,
|
||
|
failureStep: 'Apply Business Rules'
|
||
|
}
|
||
|
];
|
||
|
|
||
|
for (const test of testCases) {
|
||
|
const startTime = performance.now();
|
||
|
|
||
|
try {
|
||
|
const result = await pipeline.execute(test.input);
|
||
|
|
||
|
if (test.shouldSucceed) {
|
||
|
console.log(`✓ ${test.name}: Pipeline completed successfully`);
|
||
|
} else {
|
||
|
console.log(`✗ ${test.name}: Should have failed at ${test.failureStep}`);
|
||
|
}
|
||
|
} catch (error) {
|
||
|
if (!test.shouldSucceed) {
|
||
|
const failedStep = error.message.match(/step "([^"]+)"/)?.[1];
|
||
|
if (failedStep === test.failureStep) {
|
||
|
console.log(`✓ ${test.name}: Failed at expected step (${failedStep})`);
|
||
|
} else {
|
||
|
console.log(`✗ ${test.name}: Failed at wrong step (expected ${test.failureStep}, got ${failedStep})`);
|
||
|
}
|
||
|
} else {
|
||
|
console.log(`✗ ${test.name}: Unexpected failure`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
performanceTracker.recordMetric('pipeline-execution', performance.now() - startTime);
|
||
|
}
|
||
|
|
||
|
performanceTracker.endOperation('pipeline-errors');
|
||
|
});
|
||
|
|
||
|
await t.test('Corpus transformation analysis', async () => {
|
||
|
performanceTracker.startOperation('corpus-transformation');
|
||
|
|
||
|
const corpusLoader = new CorpusLoader();
|
||
|
const xmlFiles = await corpusLoader.getFiles(/\.xml$/);
|
||
|
|
||
|
console.log(`\nAnalyzing transformation scenarios with ${xmlFiles.length} files...`);
|
||
|
|
||
|
const transformationStats = {
|
||
|
total: 0,
|
||
|
ublToCii: 0,
|
||
|
ciiToUbl: 0,
|
||
|
zugferdToXrechnung: 0,
|
||
|
errors: 0,
|
||
|
unsupported: 0
|
||
|
};
|
||
|
|
||
|
const sampleSize = Math.min(20, xmlFiles.length);
|
||
|
const sampledFiles = xmlFiles.slice(0, sampleSize);
|
||
|
|
||
|
for (const file of sampledFiles) {
|
||
|
transformationStats.total++;
|
||
|
|
||
|
try {
|
||
|
// Detect source format
|
||
|
if (file.path.includes('UBL') || file.path.includes('.ubl.')) {
|
||
|
transformationStats.ublToCii++;
|
||
|
} else if (file.path.includes('CII') || file.path.includes('.cii.')) {
|
||
|
transformationStats.ciiToUbl++;
|
||
|
} else if (file.path.includes('ZUGFeRD') || file.path.includes('XRECHNUNG')) {
|
||
|
transformationStats.zugferdToXrechnung++;
|
||
|
} else {
|
||
|
transformationStats.unsupported++;
|
||
|
}
|
||
|
} catch (error) {
|
||
|
transformationStats.errors++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
console.log('\nTransformation Scenarios:');
|
||
|
console.log(`Total files analyzed: ${transformationStats.total}`);
|
||
|
console.log(`UBL → CII candidates: ${transformationStats.ublToCii}`);
|
||
|
console.log(`CII → UBL candidates: ${transformationStats.ciiToUbl}`);
|
||
|
console.log(`ZUGFeRD → XRechnung candidates: ${transformationStats.zugferdToXrechnung}`);
|
||
|
console.log(`Unsupported formats: ${transformationStats.unsupported}`);
|
||
|
console.log(`Analysis errors: ${transformationStats.errors}`);
|
||
|
|
||
|
performanceTracker.endOperation('corpus-transformation');
|
||
|
});
|
||
|
|
||
|
await t.test('Transformation rollback mechanisms', async () => {
|
||
|
performanceTracker.startOperation('rollback');
|
||
|
|
||
|
class TransformationContext {
|
||
|
private snapshots: Array<{ stage: string; data: any }> = [];
|
||
|
private currentData: any;
|
||
|
|
||
|
constructor(initialData: any) {
|
||
|
this.currentData = JSON.parse(JSON.stringify(initialData));
|
||
|
this.snapshots.push({ stage: 'initial', data: this.currentData });
|
||
|
}
|
||
|
|
||
|
async transform(stage: string, transformer: (data: any) => any): Promise<void> {
|
||
|
try {
|
||
|
const transformed = await transformer(this.currentData);
|
||
|
this.currentData = transformed;
|
||
|
this.snapshots.push({
|
||
|
stage,
|
||
|
data: JSON.parse(JSON.stringify(transformed))
|
||
|
});
|
||
|
} catch (error) {
|
||
|
throw new Error(`Transformation failed at stage "${stage}": ${error.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
rollbackTo(stage: string): void {
|
||
|
const snapshot = this.snapshots.find(s => s.stage === stage);
|
||
|
if (!snapshot) {
|
||
|
throw new Error(`No snapshot found for stage: ${stage}`);
|
||
|
}
|
||
|
|
||
|
this.currentData = JSON.parse(JSON.stringify(snapshot.data));
|
||
|
|
||
|
// Remove all snapshots after this stage
|
||
|
const index = this.snapshots.indexOf(snapshot);
|
||
|
this.snapshots = this.snapshots.slice(0, index + 1);
|
||
|
}
|
||
|
|
||
|
getData(): any {
|
||
|
return this.currentData;
|
||
|
}
|
||
|
|
||
|
getHistory(): string[] {
|
||
|
return this.snapshots.map(s => s.stage);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const initialData = {
|
||
|
invoice: {
|
||
|
id: 'TEST-001',
|
||
|
amount: 100,
|
||
|
items: ['item1', 'item2']
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const context = new TransformationContext(initialData);
|
||
|
|
||
|
try {
|
||
|
// Successful transformations
|
||
|
await context.transform('add-date', (data) => {
|
||
|
data.invoice.date = '2024-01-01';
|
||
|
return data;
|
||
|
});
|
||
|
|
||
|
await context.transform('calculate-tax', (data) => {
|
||
|
data.invoice.tax = data.invoice.amount * 0.19;
|
||
|
return data;
|
||
|
});
|
||
|
|
||
|
console.log('✓ Transformations applied:', context.getHistory());
|
||
|
|
||
|
// Failed transformation
|
||
|
await context.transform('invalid-operation', (data) => {
|
||
|
throw new Error('Invalid operation');
|
||
|
});
|
||
|
|
||
|
} catch (error) {
|
||
|
console.log(`✓ Error caught: ${error.message}`);
|
||
|
|
||
|
// Rollback to last successful state
|
||
|
context.rollbackTo('calculate-tax');
|
||
|
console.log('✓ Rolled back to:', context.getHistory());
|
||
|
|
||
|
// Try rollback to initial state
|
||
|
context.rollbackTo('initial');
|
||
|
console.log('✓ Rolled back to initial state');
|
||
|
|
||
|
const finalData = context.getData();
|
||
|
expect(JSON.stringify(finalData)).toEqual(JSON.stringify(initialData));
|
||
|
}
|
||
|
|
||
|
performanceTracker.endOperation('rollback');
|
||
|
});
|
||
|
|
||
|
// Performance summary
|
||
|
console.log('\n' + performanceTracker.getSummary());
|
||
|
|
||
|
// Transformation error handling best practices
|
||
|
console.log('\nTransformation Error Handling Best Practices:');
|
||
|
console.log('1. Validate transformation rules before execution');
|
||
|
console.log('2. Implement checkpoints for complex transformation pipelines');
|
||
|
console.log('3. Provide detailed error context including failed step and data state');
|
||
|
console.log('4. Support rollback mechanisms for failed transformations');
|
||
|
console.log('5. Log all transformation steps for debugging');
|
||
|
console.log('6. Handle type mismatches and data loss gracefully');
|
||
|
console.log('7. Validate output against target schema');
|
||
|
console.log('8. Implement transformation preview/dry-run capability');
|
||
|
});
|
||
|
|
||
|
tap.start();
|