511 lines
15 KiB
JavaScript
511 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Integration tests for CLI functionality
|
|
* Run with: node tests/integration/cli.test.js
|
|
*/
|
|
|
|
const { strict: assert } = require('assert');
|
|
const { spawn, exec } = require('child_process');
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const os = require('os');
|
|
|
|
class CLITestRunner {
|
|
constructor() {
|
|
this.tests = [];
|
|
this.passed = 0;
|
|
this.failed = 0;
|
|
this.verbose = process.argv.includes('--verbose');
|
|
this.tempDir = null;
|
|
this.cliPath = path.join(__dirname, '../../bin/cli.js');
|
|
}
|
|
|
|
async setup() {
|
|
// Create temporary directory for test files
|
|
this.tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sublinear-test-'));
|
|
|
|
// Create test matrix files
|
|
await this.createTestMatrices();
|
|
}
|
|
|
|
async cleanup() {
|
|
if (this.tempDir) {
|
|
try {
|
|
await fs.rm(this.tempDir, { recursive: true, force: true });
|
|
} catch (error) {
|
|
console.warn('Failed to cleanup temp directory:', error.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
async createTestMatrices() {
|
|
// Create a simple 2x2 matrix in JSON format
|
|
const matrix2x2 = {
|
|
rows: 2,
|
|
cols: 2,
|
|
data: [2, 1, 1, 2],
|
|
format: 'dense'
|
|
};
|
|
|
|
await fs.writeFile(
|
|
path.join(this.tempDir, 'matrix2x2.json'),
|
|
JSON.stringify(matrix2x2, null, 2)
|
|
);
|
|
|
|
// Create corresponding vector
|
|
const vector2x2 = [3, 3];
|
|
await fs.writeFile(
|
|
path.join(this.tempDir, 'vector2x2.json'),
|
|
JSON.stringify(vector2x2, null, 2)
|
|
);
|
|
|
|
// Create a CSV matrix
|
|
const csvMatrix = '1,0,0\n0,1,0\n0,0,1';
|
|
await fs.writeFile(
|
|
path.join(this.tempDir, 'identity3x3.csv'),
|
|
csvMatrix
|
|
);
|
|
|
|
// Create Matrix Market format
|
|
const mtxMatrix = `%%MatrixMarket matrix coordinate real general
|
|
3 3 3
|
|
1 1 1.0
|
|
2 2 1.0
|
|
3 3 1.0`;
|
|
await fs.writeFile(
|
|
path.join(this.tempDir, 'identity3x3.mtx'),
|
|
mtxMatrix
|
|
);
|
|
|
|
// Create a larger sparse matrix in COO format
|
|
const sparseMatrix = {
|
|
rows: 5,
|
|
cols: 5,
|
|
entries: 8,
|
|
data: {
|
|
values: [4, -1, -1, 4, -1, -1, 4, -1],
|
|
rowIndices: [0, 0, 1, 1, 1, 2, 2, 2],
|
|
colIndices: [0, 1, 0, 1, 2, 1, 2, 3]
|
|
},
|
|
format: 'coo'
|
|
};
|
|
|
|
await fs.writeFile(
|
|
path.join(this.tempDir, 'sparse5x5.json'),
|
|
JSON.stringify(sparseMatrix, null, 2)
|
|
);
|
|
}
|
|
|
|
test(name, fn) {
|
|
this.tests.push({ name, fn });
|
|
}
|
|
|
|
async run() {
|
|
console.log('🧪 Running CLI Integration Tests');
|
|
console.log('================================\n');
|
|
|
|
await this.setup();
|
|
|
|
for (const { name, fn } of this.tests) {
|
|
try {
|
|
await fn();
|
|
this.passed++;
|
|
console.log(`✅ ${name}`);
|
|
} catch (error) {
|
|
this.failed++;
|
|
console.log(`❌ ${name}`);
|
|
if (this.verbose) {
|
|
console.log(` Error: ${error.message}`);
|
|
console.log(` Stack: ${error.stack}\n`);
|
|
} else {
|
|
console.log(` Error: ${error.message}\n`);
|
|
}
|
|
}
|
|
}
|
|
|
|
await this.cleanup();
|
|
this.printSummary();
|
|
return this.failed === 0;
|
|
}
|
|
|
|
printSummary() {
|
|
console.log('\n📊 Test Summary');
|
|
console.log('===============');
|
|
console.log(`✅ Passed: ${this.passed}`);
|
|
console.log(`❌ Failed: ${this.failed}`);
|
|
console.log(`📈 Total: ${this.tests.length}`);
|
|
console.log(`🎯 Success Rate: ${((this.passed / this.tests.length) * 100).toFixed(1)}%`);
|
|
}
|
|
|
|
// Helper method to execute CLI commands
|
|
async execCLI(args, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn('node', [this.cliPath, ...args], {
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
...options
|
|
});
|
|
|
|
let stdout = '';
|
|
let stderr = '';
|
|
|
|
child.stdout.on('data', (data) => {
|
|
stdout += data.toString();
|
|
});
|
|
|
|
child.stderr.on('data', (data) => {
|
|
stderr += data.toString();
|
|
});
|
|
|
|
child.on('close', (code) => {
|
|
resolve({
|
|
code,
|
|
stdout,
|
|
stderr
|
|
});
|
|
});
|
|
|
|
child.on('error', (error) => {
|
|
reject(error);
|
|
});
|
|
|
|
// Set timeout to prevent hanging tests
|
|
setTimeout(() => {
|
|
child.kill('SIGTERM');
|
|
reject(new Error('CLI command timed out'));
|
|
}, 30000);
|
|
});
|
|
}
|
|
}
|
|
|
|
const runner = new CLITestRunner();
|
|
|
|
// Basic CLI Tests
|
|
runner.test('CLI displays help message', async () => {
|
|
const result = await runner.execCLI(['--help']);
|
|
|
|
assert.equal(result.code, 0);
|
|
assert.ok(result.stdout.includes('Advanced Sublinear Time Sparse Linear System Solver'));
|
|
assert.ok(result.stdout.includes('solve'));
|
|
assert.ok(result.stdout.includes('serve'));
|
|
assert.ok(result.stdout.includes('benchmark'));
|
|
});
|
|
|
|
runner.test('CLI displays version', async () => {
|
|
const result = await runner.execCLI(['--version']);
|
|
|
|
// Version command might exit with 0 or display version in help
|
|
assert.ok(result.code === 0 || result.stdout.length > 0);
|
|
});
|
|
|
|
runner.test('CLI handles invalid command', async () => {
|
|
const result = await runner.execCLI(['invalid-command']);
|
|
|
|
// Should exit with non-zero code for invalid commands
|
|
assert.notEqual(result.code, 0);
|
|
});
|
|
|
|
// Solve Command Tests
|
|
runner.test('CLI solve command requires matrix file', async () => {
|
|
const result = await runner.execCLI(['solve']);
|
|
|
|
assert.notEqual(result.code, 0);
|
|
assert.ok(result.stderr.includes('required') || result.stdout.includes('required'));
|
|
});
|
|
|
|
runner.test('CLI solve command with valid matrix (should fail gracefully without WASM)', async () => {
|
|
const matrixFile = path.join(runner.tempDir, 'matrix2x2.json');
|
|
const result = await runner.execCLI(['solve', '-m', matrixFile]);
|
|
|
|
// This should fail because WASM isn't built, but it should fail gracefully
|
|
assert.notEqual(result.code, 0);
|
|
// Should show a helpful error message
|
|
assert.ok(result.stderr.length > 0 || result.stdout.includes('Error'));
|
|
});
|
|
|
|
runner.test('CLI solve command with output file specification', async () => {
|
|
const matrixFile = path.join(runner.tempDir, 'matrix2x2.json');
|
|
const outputFile = path.join(runner.tempDir, 'solution.json');
|
|
|
|
const result = await runner.execCLI([
|
|
'solve',
|
|
'-m', matrixFile,
|
|
'-o', outputFile
|
|
]);
|
|
|
|
// Should fail gracefully without WASM but show proper argument parsing
|
|
assert.notEqual(result.code, 0);
|
|
});
|
|
|
|
runner.test('CLI solve command with custom parameters', async () => {
|
|
const matrixFile = path.join(runner.tempDir, 'matrix2x2.json');
|
|
|
|
const result = await runner.execCLI([
|
|
'solve',
|
|
'-m', matrixFile,
|
|
'--method', 'cg',
|
|
'--tolerance', '1e-8',
|
|
'--max-iterations', '500'
|
|
]);
|
|
|
|
// Should fail without WASM but arguments should be parsed correctly
|
|
assert.notEqual(result.code, 0);
|
|
});
|
|
|
|
// Verify Command Tests
|
|
runner.test('CLI verify command requires all files', async () => {
|
|
const result = await runner.execCLI(['verify']);
|
|
|
|
assert.notEqual(result.code, 0);
|
|
// Should mention required arguments
|
|
assert.ok(result.stderr.includes('required') || result.stdout.includes('required'));
|
|
});
|
|
|
|
runner.test('CLI verify command argument parsing', async () => {
|
|
const matrixFile = path.join(runner.tempDir, 'matrix2x2.json');
|
|
const solutionFile = path.join(runner.tempDir, 'solution.json');
|
|
const vectorFile = path.join(runner.tempDir, 'vector2x2.json');
|
|
|
|
// Create a dummy solution file
|
|
await fs.writeFile(solutionFile, JSON.stringify([1, 1]));
|
|
|
|
const result = await runner.execCLI([
|
|
'verify',
|
|
'-m', matrixFile,
|
|
'-x', solutionFile,
|
|
'-b', vectorFile,
|
|
'--tolerance', '1e-6'
|
|
]);
|
|
|
|
// May fail on implementation details but arguments should parse
|
|
// We're mainly testing the CLI interface here
|
|
assert.ok(result.code !== undefined);
|
|
});
|
|
|
|
// Convert Command Tests
|
|
runner.test('CLI convert command requires input and output', async () => {
|
|
const result = await runner.execCLI(['convert']);
|
|
|
|
assert.notEqual(result.code, 0);
|
|
assert.ok(result.stderr.includes('required') || result.stdout.includes('required'));
|
|
});
|
|
|
|
runner.test('CLI convert command with format specification', async () => {
|
|
const inputFile = path.join(runner.tempDir, 'matrix2x2.json');
|
|
const outputFile = path.join(runner.tempDir, 'matrix2x2.csv');
|
|
|
|
const result = await runner.execCLI([
|
|
'convert',
|
|
'-i', inputFile,
|
|
'-o', outputFile,
|
|
'--format', 'csv'
|
|
]);
|
|
|
|
// This might work if conversion logic is implemented
|
|
// We're testing the interface
|
|
assert.ok(result.code !== undefined);
|
|
});
|
|
|
|
// Benchmark Command Tests
|
|
runner.test('CLI benchmark command with custom parameters', async () => {
|
|
const result = await runner.execCLI([
|
|
'benchmark',
|
|
'--size', '10',
|
|
'--sparsity', '0.1',
|
|
'--methods', 'jacobi,cg',
|
|
'--iterations', '2'
|
|
]);
|
|
|
|
// Should fail without WASM but arguments should parse
|
|
assert.notEqual(result.code, 0);
|
|
});
|
|
|
|
runner.test('CLI benchmark command output file', async () => {
|
|
const outputFile = path.join(runner.tempDir, 'benchmark_results.json');
|
|
|
|
const result = await runner.execCLI([
|
|
'benchmark',
|
|
'--size', '5',
|
|
'--output', outputFile
|
|
]);
|
|
|
|
// Should fail without WASM implementation
|
|
assert.notEqual(result.code, 0);
|
|
});
|
|
|
|
// Serve Command Tests
|
|
runner.test('CLI serve command with default port', async () => {
|
|
// Start server in background and kill it quickly
|
|
const child = spawn('node', [runner.cliPath, 'serve'], {
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
});
|
|
|
|
// Give it a moment to start
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
// Kill the server
|
|
child.kill('SIGTERM');
|
|
|
|
// Wait for it to exit
|
|
const exitCode = await new Promise(resolve => {
|
|
child.on('close', resolve);
|
|
});
|
|
|
|
// The server might fail to start due to missing WASM, which is expected
|
|
assert.ok(exitCode !== undefined);
|
|
});
|
|
|
|
runner.test('CLI serve command with custom port', async () => {
|
|
const child = spawn('node', [runner.cliPath, 'serve', '--port', '3001'], {
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
});
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
child.kill('SIGTERM');
|
|
|
|
const exitCode = await new Promise(resolve => {
|
|
child.on('close', resolve);
|
|
});
|
|
|
|
assert.ok(exitCode !== undefined);
|
|
});
|
|
|
|
// Flow-Nexus Command Tests
|
|
runner.test('CLI flow-nexus command structure', async () => {
|
|
const result = await runner.execCLI(['flow-nexus', '--help']);
|
|
|
|
// Should show flow-nexus specific help or fail gracefully
|
|
assert.ok(result.code !== undefined);
|
|
});
|
|
|
|
// File Format Tests
|
|
runner.test('CLI handles JSON matrix format', async () => {
|
|
const matrixFile = path.join(runner.tempDir, 'matrix2x2.json');
|
|
|
|
// Verify the file exists and is readable by the CLI
|
|
const stats = await fs.stat(matrixFile);
|
|
assert.ok(stats.isFile());
|
|
|
|
const content = await fs.readFile(matrixFile, 'utf8');
|
|
const matrix = JSON.parse(content);
|
|
assert.equal(matrix.rows, 2);
|
|
assert.equal(matrix.cols, 2);
|
|
});
|
|
|
|
runner.test('CLI handles CSV matrix format', async () => {
|
|
const matrixFile = path.join(runner.tempDir, 'identity3x3.csv');
|
|
|
|
const stats = await fs.stat(matrixFile);
|
|
assert.ok(stats.isFile());
|
|
|
|
const content = await fs.readFile(matrixFile, 'utf8');
|
|
const lines = content.trim().split('\n');
|
|
assert.equal(lines.length, 3);
|
|
assert.equal(lines[0], '1,0,0');
|
|
});
|
|
|
|
runner.test('CLI handles Matrix Market format', async () => {
|
|
const matrixFile = path.join(runner.tempDir, 'identity3x3.mtx');
|
|
|
|
const stats = await fs.stat(matrixFile);
|
|
assert.ok(stats.isFile());
|
|
|
|
const content = await fs.readFile(matrixFile, 'utf8');
|
|
assert.ok(content.includes('%%MatrixMarket'));
|
|
assert.ok(content.includes('3 3 3'));
|
|
});
|
|
|
|
// Error Handling Tests
|
|
runner.test('CLI handles missing matrix file', async () => {
|
|
const result = await runner.execCLI([
|
|
'solve',
|
|
'-m', '/nonexistent/matrix.json'
|
|
]);
|
|
|
|
assert.notEqual(result.code, 0);
|
|
assert.ok(result.stderr.includes('Error') || result.stdout.includes('Error'));
|
|
});
|
|
|
|
runner.test('CLI handles invalid JSON matrix', async () => {
|
|
const invalidFile = path.join(runner.tempDir, 'invalid.json');
|
|
await fs.writeFile(invalidFile, '{ invalid json }');
|
|
|
|
const result = await runner.execCLI([
|
|
'solve',
|
|
'-m', invalidFile
|
|
]);
|
|
|
|
assert.notEqual(result.code, 0);
|
|
});
|
|
|
|
// Verbose and Debug Mode Tests
|
|
runner.test('CLI verbose mode', async () => {
|
|
const result = await runner.execCLI([
|
|
'--verbose',
|
|
'solve',
|
|
'-m', path.join(runner.tempDir, 'matrix2x2.json')
|
|
]);
|
|
|
|
// Should produce more output in verbose mode
|
|
assert.notEqual(result.code, 0); // Will fail without WASM
|
|
// In verbose mode, there might be more detailed error information
|
|
});
|
|
|
|
runner.test('CLI debug mode', async () => {
|
|
const result = await runner.execCLI([
|
|
'--debug',
|
|
'solve',
|
|
'-m', path.join(runner.tempDir, 'matrix2x2.json')
|
|
]);
|
|
|
|
assert.notEqual(result.code, 0); // Will fail without WASM
|
|
// Debug mode should provide stack traces
|
|
});
|
|
|
|
runner.test('CLI quiet mode', async () => {
|
|
const result = await runner.execCLI([
|
|
'--quiet',
|
|
'solve',
|
|
'-m', path.join(runner.tempDir, 'matrix2x2.json')
|
|
]);
|
|
|
|
assert.notEqual(result.code, 0); // Will fail without WASM
|
|
// Output should be minimal in quiet mode
|
|
});
|
|
|
|
// Signal Handling Tests
|
|
runner.test('CLI handles SIGTERM gracefully', async () => {
|
|
const child = spawn('node', [runner.cliPath, 'serve'], {
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
});
|
|
|
|
// Let it start
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
// Send SIGTERM
|
|
child.kill('SIGTERM');
|
|
|
|
// Wait for graceful shutdown
|
|
const exitCode = await new Promise(resolve => {
|
|
child.on('close', resolve);
|
|
setTimeout(() => {
|
|
child.kill('SIGKILL');
|
|
resolve(-1);
|
|
}, 5000);
|
|
});
|
|
|
|
// Should exit (might be 0 or error code depending on implementation)
|
|
assert.ok(exitCode !== undefined);
|
|
});
|
|
|
|
// Run all tests
|
|
if (require.main === module) {
|
|
runner.run().then(success => {
|
|
process.exit(success ? 0 : 1);
|
|
}).catch(error => {
|
|
console.error('Test runner failed:', error);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = { CLITestRunner, runner }; |