359 lines
10 KiB
JavaScript
359 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Unit tests for Matrix class and related functionality
|
|
* Run with: node tests/unit/matrix.test.js
|
|
*/
|
|
|
|
const { strict: assert } = require('assert');
|
|
const { Matrix, SolverConfig, MemoryManager } = require('../../js/solver.js');
|
|
|
|
class TestRunner {
|
|
constructor() {
|
|
this.tests = [];
|
|
this.passed = 0;
|
|
this.failed = 0;
|
|
this.verbose = process.argv.includes('--verbose');
|
|
}
|
|
|
|
test(name, fn) {
|
|
this.tests.push({ name, fn });
|
|
}
|
|
|
|
async run() {
|
|
console.log('🧪 Running Matrix Unit Tests');
|
|
console.log('============================\n');
|
|
|
|
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`);
|
|
}
|
|
}
|
|
}
|
|
|
|
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)}%`);
|
|
}
|
|
}
|
|
|
|
const runner = new TestRunner();
|
|
|
|
// Matrix Constructor Tests
|
|
runner.test('Matrix constructor with Float64Array', () => {
|
|
const data = new Float64Array([1, 2, 3, 4]);
|
|
const matrix = new Matrix(data, 2, 2);
|
|
|
|
assert.equal(matrix.rows, 2);
|
|
assert.equal(matrix.cols, 2);
|
|
assert.equal(matrix.data.length, 4);
|
|
assert.equal(matrix.data[0], 1);
|
|
assert.equal(matrix.data[3], 4);
|
|
});
|
|
|
|
runner.test('Matrix constructor with Array', () => {
|
|
const data = [1, 2, 3, 4];
|
|
const matrix = new Matrix(data, 2, 2);
|
|
|
|
assert.equal(matrix.rows, 2);
|
|
assert.equal(matrix.cols, 2);
|
|
assert.ok(matrix.data instanceof Float64Array);
|
|
assert.equal(matrix.data[0], 1);
|
|
});
|
|
|
|
runner.test('Matrix constructor dimension validation', () => {
|
|
assert.throws(() => {
|
|
new Matrix([1, 2, 3], 2, 2);
|
|
}, /Data length must match matrix dimensions/);
|
|
});
|
|
|
|
runner.test('Matrix constructor invalid data type', () => {
|
|
assert.throws(() => {
|
|
new Matrix("invalid", 2, 2);
|
|
}, /Matrix data must be Float64Array or Array/);
|
|
});
|
|
|
|
// Matrix Static Methods
|
|
runner.test('Matrix.zeros creates zero matrix', () => {
|
|
const matrix = Matrix.zeros(3, 2);
|
|
|
|
assert.equal(matrix.rows, 3);
|
|
assert.equal(matrix.cols, 2);
|
|
assert.equal(matrix.data.length, 6);
|
|
|
|
for (let i = 0; i < matrix.data.length; i++) {
|
|
assert.equal(matrix.data[i], 0);
|
|
}
|
|
});
|
|
|
|
runner.test('Matrix.identity creates identity matrix', () => {
|
|
const matrix = Matrix.identity(3);
|
|
|
|
assert.equal(matrix.rows, 3);
|
|
assert.equal(matrix.cols, 3);
|
|
|
|
// Check diagonal elements
|
|
assert.equal(matrix.get(0, 0), 1);
|
|
assert.equal(matrix.get(1, 1), 1);
|
|
assert.equal(matrix.get(2, 2), 1);
|
|
|
|
// Check off-diagonal elements
|
|
assert.equal(matrix.get(0, 1), 0);
|
|
assert.equal(matrix.get(1, 0), 0);
|
|
assert.equal(matrix.get(1, 2), 0);
|
|
});
|
|
|
|
runner.test('Matrix.random creates random matrix', () => {
|
|
const matrix = Matrix.random(2, 3);
|
|
|
|
assert.equal(matrix.rows, 2);
|
|
assert.equal(matrix.cols, 3);
|
|
assert.equal(matrix.data.length, 6);
|
|
|
|
// Check that values are in [0, 1) range
|
|
for (let i = 0; i < matrix.data.length; i++) {
|
|
assert.ok(matrix.data[i] >= 0 && matrix.data[i] < 1);
|
|
}
|
|
});
|
|
|
|
// Matrix Access Methods
|
|
runner.test('Matrix get/set operations', () => {
|
|
const matrix = Matrix.zeros(2, 2);
|
|
|
|
matrix.set(0, 1, 5.5);
|
|
matrix.set(1, 0, -2.3);
|
|
|
|
assert.equal(matrix.get(0, 1), 5.5);
|
|
assert.equal(matrix.get(1, 0), -2.3);
|
|
assert.equal(matrix.get(0, 0), 0);
|
|
assert.equal(matrix.get(1, 1), 0);
|
|
});
|
|
|
|
runner.test('Matrix bounds checking for get', () => {
|
|
const matrix = new Matrix([1, 2, 3, 4], 2, 2);
|
|
|
|
// Valid access
|
|
assert.equal(matrix.get(1, 1), 4);
|
|
|
|
// Should not throw for out of bounds (JavaScript behavior)
|
|
// But should return undefined or unexpected values
|
|
const result = matrix.get(2, 2);
|
|
assert.ok(result === undefined || typeof result === 'number');
|
|
});
|
|
|
|
// SolverConfig Tests
|
|
runner.test('SolverConfig default values', () => {
|
|
const config = new SolverConfig();
|
|
|
|
assert.equal(config.maxIterations, 1000);
|
|
assert.equal(config.tolerance, 1e-10);
|
|
assert.equal(config.simdEnabled, true);
|
|
assert.equal(config.streamChunkSize, 100);
|
|
});
|
|
|
|
runner.test('SolverConfig custom values', () => {
|
|
const config = new SolverConfig({
|
|
maxIterations: 500,
|
|
tolerance: 1e-6,
|
|
simdEnabled: false,
|
|
streamChunkSize: 50
|
|
});
|
|
|
|
assert.equal(config.maxIterations, 500);
|
|
assert.equal(config.tolerance, 1e-6);
|
|
assert.equal(config.simdEnabled, false);
|
|
assert.equal(config.streamChunkSize, 50);
|
|
});
|
|
|
|
runner.test('SolverConfig partial custom values', () => {
|
|
const config = new SolverConfig({
|
|
maxIterations: 2000,
|
|
tolerance: 1e-8
|
|
});
|
|
|
|
assert.equal(config.maxIterations, 2000);
|
|
assert.equal(config.tolerance, 1e-8);
|
|
assert.equal(config.simdEnabled, true); // Default
|
|
assert.equal(config.streamChunkSize, 100); // Default
|
|
});
|
|
|
|
// MemoryManager Tests
|
|
runner.test('MemoryManager allocation and deallocation', () => {
|
|
const manager = new MemoryManager();
|
|
|
|
const allocation = manager.allocateFloat64Array(100);
|
|
|
|
assert.ok(allocation.id);
|
|
assert.ok(allocation.buffer instanceof Float64Array);
|
|
assert.equal(allocation.buffer.length, 100);
|
|
|
|
const usage = manager.getUsage();
|
|
assert.equal(usage.allocations, 1);
|
|
assert.equal(usage.totalBytes, 100 * 8); // 8 bytes per Float64
|
|
|
|
manager.deallocate(allocation.id);
|
|
|
|
const usageAfter = manager.getUsage();
|
|
assert.equal(usageAfter.allocations, 0);
|
|
assert.equal(usageAfter.totalBytes, 0);
|
|
});
|
|
|
|
runner.test('MemoryManager multiple allocations', () => {
|
|
const manager = new MemoryManager();
|
|
|
|
const alloc1 = manager.allocateFloat64Array(50);
|
|
const alloc2 = manager.allocateFloat64Array(100);
|
|
const alloc3 = manager.allocateFloat64Array(25);
|
|
|
|
const usage = manager.getUsage();
|
|
assert.equal(usage.allocations, 3);
|
|
assert.equal(usage.totalBytes, (50 + 100 + 25) * 8);
|
|
|
|
manager.deallocate(alloc2.id);
|
|
|
|
const usageAfter = manager.getUsage();
|
|
assert.equal(usageAfter.allocations, 2);
|
|
assert.equal(usageAfter.totalBytes, (50 + 25) * 8);
|
|
});
|
|
|
|
runner.test('MemoryManager clear all allocations', () => {
|
|
const manager = new MemoryManager();
|
|
|
|
manager.allocateFloat64Array(10);
|
|
manager.allocateFloat64Array(20);
|
|
manager.allocateFloat64Array(30);
|
|
|
|
assert.equal(manager.getUsage().allocations, 3);
|
|
|
|
manager.clear();
|
|
|
|
const usage = manager.getUsage();
|
|
assert.equal(usage.allocations, 0);
|
|
assert.equal(usage.totalBytes, 0);
|
|
});
|
|
|
|
// Mathematical Property Tests
|
|
runner.test('Matrix mathematical properties - transpose concept', () => {
|
|
const matrix = new Matrix([1, 2, 3, 4, 5, 6], 2, 3);
|
|
|
|
// Original: [[1, 2, 3], [4, 5, 6]]
|
|
assert.equal(matrix.get(0, 0), 1);
|
|
assert.equal(matrix.get(0, 1), 2);
|
|
assert.equal(matrix.get(0, 2), 3);
|
|
assert.equal(matrix.get(1, 0), 4);
|
|
assert.equal(matrix.get(1, 1), 5);
|
|
assert.equal(matrix.get(1, 2), 6);
|
|
});
|
|
|
|
runner.test('Matrix identity properties', () => {
|
|
const identity = Matrix.identity(4);
|
|
|
|
// Check all diagonal elements are 1
|
|
for (let i = 0; i < 4; i++) {
|
|
assert.equal(identity.get(i, i), 1);
|
|
}
|
|
|
|
// Check all off-diagonal elements are 0
|
|
for (let i = 0; i < 4; i++) {
|
|
for (let j = 0; j < 4; j++) {
|
|
if (i !== j) {
|
|
assert.equal(identity.get(i, j), 0);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
runner.test('Matrix zero properties', () => {
|
|
const zeros = Matrix.zeros(3, 4);
|
|
|
|
// Check all elements are 0
|
|
for (let i = 0; i < 3; i++) {
|
|
for (let j = 0; j < 4; j++) {
|
|
assert.equal(zeros.get(i, j), 0);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Edge Cases
|
|
runner.test('Matrix with single element', () => {
|
|
const matrix = new Matrix([42], 1, 1);
|
|
|
|
assert.equal(matrix.rows, 1);
|
|
assert.equal(matrix.cols, 1);
|
|
assert.equal(matrix.get(0, 0), 42);
|
|
});
|
|
|
|
runner.test('Matrix with large dimensions', () => {
|
|
const size = 1000;
|
|
const matrix = Matrix.zeros(size, size);
|
|
|
|
assert.equal(matrix.rows, size);
|
|
assert.equal(matrix.cols, size);
|
|
assert.equal(matrix.data.length, size * size);
|
|
|
|
// Test corner elements
|
|
assert.equal(matrix.get(0, 0), 0);
|
|
assert.equal(matrix.get(size - 1, size - 1), 0);
|
|
});
|
|
|
|
runner.test('Matrix memory efficiency check', () => {
|
|
const size = 100;
|
|
const matrix = Matrix.random(size, size);
|
|
|
|
// Check that data is stored efficiently as Float64Array
|
|
assert.ok(matrix.data instanceof Float64Array);
|
|
assert.equal(matrix.data.length, size * size);
|
|
assert.equal(matrix.data.byteLength, size * size * 8);
|
|
});
|
|
|
|
// Performance Tests
|
|
runner.test('Matrix creation performance benchmark', () => {
|
|
const sizes = [10, 100, 500];
|
|
|
|
for (const size of sizes) {
|
|
const start = Date.now();
|
|
const matrix = Matrix.zeros(size, size);
|
|
const end = Date.now();
|
|
|
|
const duration = end - start;
|
|
|
|
// Should create matrices quickly (under 100ms for reasonable sizes)
|
|
if (size <= 500) {
|
|
assert.ok(duration < 1000, `Matrix creation too slow: ${duration}ms for ${size}x${size}`);
|
|
}
|
|
|
|
// Verify matrix was created correctly
|
|
assert.equal(matrix.rows, size);
|
|
assert.equal(matrix.cols, size);
|
|
}
|
|
});
|
|
|
|
// 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 = { TestRunner, runner }; |