sorting/test_algorithms.js

409 lines
12 KiB
JavaScript
Raw Normal View History

/**
* Test-Skript für Sortieralgorithmen
* Extrahiert buildSteps aus sorting_visualization.html und testet alle Algorithmen
*
* Usage:
* node test_algorithms.js # Kurzbericht
* node test_algorithms.js --verbose # Detaillierte Ausgabe
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import vm from 'node:vm';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const verbose = process.argv.includes('--verbose');
// ── HTML parsen und JS extrahieren ──
const html = fs.readFileSync(path.join(__dirname, 'sorting_visualization.html'), 'utf-8');
// Finde den letzten <script>-Block (der unseren Code enthält), ignoriere CDN-Scripts
const scriptBlocks = [];
let idx = 0;
while (true) {
const openIdx = html.indexOf('<script', idx);
if (openIdx === -1) break;
const closeIdx = html.indexOf('>', openIdx);
if (closeIdx === -1) break;
const tag = html.substring(openIdx, closeIdx + 1);
const endIdx = html.indexOf('</script>', closeIdx);
if (endIdx === -1) break;
const content = html.substring(closeIdx + 1, endIdx);
scriptBlocks.push({ tag, content, start: closeIdx + 1 });
idx = endIdx + 9;
}
// Nimm den Block der 'function buildSteps' enthält
const codeBlock = scriptBlocks.find(b => b.content.includes('function buildSteps'));
if (!codeBlock) {
console.error('Fehler: Kein <script>-Block mit buildSteps gefunden');
process.exit(1);
}
const rawJs = codeBlock.content;
// ── Mocks ──
const mockEl = (overrides = {}) => ({
classList: { add: () => {}, remove: () => {}, contains: () => false },
style: {},
textContent: '',
innerHTML: '',
value: '50',
min: '5',
max: '200',
clientWidth: 800,
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 400 }),
addEventListener: () => {},
setPointerCapture: () => {},
dispatchEvent: () => {},
...overrides,
});
const mockCtx = {
clearRect: () => {}, fillRect: () => {}, strokeRect: () => {},
beginPath: () => {}, closePath: () => {}, moveTo: () => {},
lineTo: () => {}, quadraticCurveTo: () => {}, arc: () => {},
fill: () => {}, stroke: () => {}, save: () => {}, restore: () => {},
setTransform: () => {},
};
const mockCanvasEl = {
width: 800,
height: 400,
style: {},
parentElement: { clientWidth: 800 },
getContext: () => mockCtx,
};
// ── VM-Kontext erstellen ──
const context = vm.createContext({
console, Math, Array, Object, String, Number,
parseInt, parseFloat, Date, Set, Map, JSON,
Infinity, NaN, undefined,
navigator: { hardwareConcurrency: 4 },
window: {
devicePixelRatio: 1,
setInterval: () => 0,
clearInterval: () => {},
setTimeout: () => 0,
clearTimeout: () => {},
matchMedia: () => ({ matches: false }),
localStorage: { getItem: () => null, setItem: () => {} },
history: { replaceState: () => {} },
location: { pathname: '/', search: '' },
innerWidth: 800,
addEventListener: () => {},
},
document: {
getElementById: (id) => {
if (id === 'sortCanvas' || id === 'memCanvas') return mockCanvasEl;
return mockEl();
},
querySelector: () => mockEl(),
querySelectorAll: () => [],
createElement: () => mockEl(),
documentElement: { classList: { contains: () => true } },
},
canvas: mockCanvasEl,
ctx: mockCtx,
memCanvas: mockCanvasEl,
memCtx: mockCtx,
lucide: { createIcons: () => {} },
$algoSelect: mockEl({ value: 'bubble' }),
$sizeSlider: mockEl(),
$speedSlider: mockEl(),
$threadSlider: mockEl(),
$presetSelect: mockEl({ value: 'random' }),
$customArray: mockEl({ value: '' }),
$btnPlayPause: mockEl(),
$btnStep: mockEl(),
$btnStepBack: mockEl(),
$btnReset: mockEl(),
$btnTheme: mockEl(),
$btnClearCustom: mockEl(),
$statComp: mockEl(),
$statSwap: mockEl(),
$statMove: mockEl(),
$statStep: mockEl(),
$statTime: mockEl(),
$speedVal: mockEl(),
$sizeVal: mockEl(),
$threadVal: mockEl(),
$memContainer: mockEl(),
$memLabel: mockEl(),
$progressFill: mockEl(),
$stepCounter: mockEl(),
$phaseLabel: mockEl(),
$stepExplanation: mockEl(),
// State-Variablen die von 'let' zu 'var'-artigen werden
array: [],
steps: [],
stepIndex: 0,
animTimer: null,
isRunning: false,
isPaused: false,
arrayFresh: false,
startTime: 0,
elapsedMs: 0,
});
// ── JS vorbereiten und ausführen ──
// Finde die buildSteps-Funktion
const buildStepsStart = rawJs.indexOf('function buildSteps(algoName)');
if (buildStepsStart === -1) {
console.error('Fehler: buildSteps-Funktion nicht gefunden');
process.exit(1);
}
// Finde das Ende von buildSteps durch Brace-Counting
let braceCount = 0;
let funcEnd = rawJs.length;
for (let i = buildStepsStart; i < rawJs.length; i++) {
if (rawJs[i] === '{') braceCount++;
if (rawJs[i] === '}') {
braceCount--;
if (braceCount === 0) {
funcEnd = i + 1;
break;
}
}
}
// Nimm alles vom Anfang bis zum Ende von buildSteps
// Finde den Anfang: const COLORS
const colorsStart = rawJs.indexOf('const COLORS');
if (colorsStart === -1) {
console.error('Fehler: COLORS-Konstanten nicht gefunden');
process.exit(1);
}
const codeToRun = rawJs.substring(colorsStart, funcEnd);
// Bereinige: Entferne Code der nicht benötigt wird oder fehlschlägt
const cleanedCode = codeToRun
.replace(/makeTouchSlider\(\$.*?\);/g, '')
.replace(/resizeCanvas\(\);/g, '')
.replace(/const prefersReducedMotion[\s\S]*?el\.classList\.add\('anim-stat'\);\s*\}\s*\)\s*;?/g, '')
.replace(/lucide\.createIcons\(\);/g, '')
.replace(/\(function initTheme\(\)[\s\S]*?\}\)\(\);/g, '')
.replace(/refreshColors\(\);/g, '')
.replace(/updateSpeedLabel\(\);/g, '')
.replace(/updateSizeLabel\(\);/g, '')
.replace(/updateThreadSlider\(\);/g, '')
.replace(/updateButtonStates\(\);/g, '')
.replace(/updateProgress\(0, 0\);/g, '')
.replace(/renderCurrent\(\);/g, '')
.replace(/generateArray\(\);/g, '')
.replace(/arrayFresh = true;/g, '')
// Ersetze 'let array = []' durch Zuweisung an context.array
.replace(/let array\s*=\s*\[\];/, 'array = [];')
// Entferne andere let-Deklarationen die schon im Kontext existieren
.replace(/let steps\s*=\s*\[\];/, 'steps = [];')
.replace(/let stepIndex\s*=\s*0;/, 'stepIndex = 0;')
.replace(/let animTimer\s*=\s*null;/, 'animTimer = null;')
.replace(/let isRunning\s*=\s*false;/, 'isRunning = false;')
.replace(/let isPaused\s*=\s*false;/, 'isPaused = false;')
.replace(/let arrayFresh\s*=\s*false;[\s\S]*?let startTime\s*=\s*0;[\s\S]*?let elapsedMs\s*=\s*0;/,
'arrayFresh = false;\nlet startTime = 0;\nlet elapsedMs = 0;');
try {
vm.runInContext(cleanedCode, context, { filename: 'sorting_visualization.html' });
} catch (e) {
console.error('Fehler beim Ausführen des JavaScript-Codes:', e.message);
console.error(e.stack.split('\n').slice(0, 5).join('\n'));
process.exit(1);
}
const buildSteps = context.buildSteps;
if (!buildSteps) {
console.error('Fehler: buildSteps wurde nicht exportiert');
process.exit(1);
}
// ── Test-Framework ──
const PRESETS = ['random', 'sorted', 'reversed', 'nearly', 'duplicates'];
const ALGORITHMS = [
'bubble', 'selection', 'insertion', 'cocktail',
'merge', 'heap', 'quick', 'quick3way', 'dualpivot', 'introsort',
'shell', 'tree', 'timsort',
'counting', 'radix', 'bucket',
'pancake', 'cycle', 'bogo',
];
const Bogo_MAX_SIZE = 6;
let totalTests = 0;
let passed = 0;
let failed = 0;
const failures = [];
function isSorted(arr) {
for (let i = 0; i < arr.length - 1; i++) {
if (arr[i] > arr[i + 1]) return false;
}
return true;
}
function arraysEqual(a, b) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
function generatePresetArray(preset, size) {
let arr = [];
switch (preset) {
case 'random':
for (let i = 0; i < size; i++) arr.push(Math.floor(Math.random() * 100) + 1);
break;
case 'sorted':
for (let i = 1; i <= size; i++) arr.push(i);
break;
case 'reversed':
for (let i = size; i >= 1; i--) arr.push(i);
break;
case 'nearly': {
for (let i = 1; i <= size; i++) arr.push(i);
const swaps = Math.max(1, Math.floor(size * 0.05));
for (let s = 0; s < swaps; s++) {
const i = Math.floor(Math.random() * size);
const j = Math.floor(Math.random() * size);
[arr[i], arr[j]] = [arr[j], arr[i]];
}
break;
}
case 'duplicates': {
const values = [5, 15, 25, 35, 45];
for (let i = 0; i < size; i++) arr.push(values[Math.floor(Math.random() * values.length)]);
break;
}
}
return arr;
}
function testAlgorithm(algo, preset, size) {
totalTests++;
const arr = generatePresetArray(preset, size);
context.array = arr.slice();
try {
const steps = buildSteps(algo);
const lastStep = steps[steps.length - 1];
if (lastStep.type !== 'done') {
throw new Error(`Letzter Step type '${lastStep.type}', erwartet 'done'`);
}
if (!isSorted(lastStep.array)) {
throw new Error(`Array ist nicht sortiert`);
}
// Prüfe Array-Länge (einige Algos wie Counting/Radix runden Werte)
if (lastStep.array.length !== arr.length) {
throw new Error(`Array-Länge geändert: ${lastStep.array.length} statt ${arr.length}`);
}
passed++;
if (verbose) {
console.log(`${algo} + ${preset} (${size} Elemente, ${steps.length} Steps)`);
}
} catch (e) {
failed++;
failures.push({ algo, preset, size, error: e.message });
console.log(`${algo} + ${preset} (${size} Elemente): ${e.message}`);
}
}
// ── Tests ausführen ──
console.log('\n🧪 Sortieralgorithmen-Tests\n');
console.log('═'.repeat(50));
const NORMAL_SIZES = [10, 50];
for (const algo of ALGORITHMS) {
if (verbose) console.log(`\n📦 ${algo}:`);
for (const preset of PRESETS) {
for (const size of NORMAL_SIZES) {
if (algo === 'bogo' && size > Bogo_MAX_SIZE) continue;
testAlgorithm(algo, preset, size);
}
}
}
// Edge Cases
if (verbose) console.log('\n📦 Edge Cases:');
context.array = [];
try {
const steps = buildSteps('bubble');
if (steps.length > 0 && steps[steps.length - 1].type === 'done') {
passed++; totalTests++;
if (verbose) console.log(' ✅ bubble + empty array');
} else {
throw new Error('Kein done-Step');
}
} catch (e) {
failed++; totalTests++;
console.log(` ❌ bubble + empty array: ${e.message}`);
}
context.array = [42];
try {
const steps = buildSteps('bubble');
const last = steps[steps.length - 1];
if (last.type === 'done' && isSorted(last.array)) {
passed++; totalTests++;
if (verbose) console.log(' ✅ bubble + single element');
} else {
throw new Error('Nicht sortiert');
}
} catch (e) {
failed++; totalTests++;
console.log(` ❌ bubble + single element: ${e.message}`);
}
// Performance-Test (100 Elemente)
if (verbose) console.log('\n📦 Performance-Test (100 Elemente):');
for (const algo of ['bubble', 'merge', 'quick', 'heap', 'shell', 'timsort']) {
const arr = [];
for (let i = 0; i < 100; i++) arr.push(Math.floor(Math.random() * 1000) + 1);
context.array = arr.slice();
const start = Date.now();
const steps = buildSteps(algo);
const elapsed = Date.now() - start;
totalTests++;
if (isSorted(steps[steps.length - 1].array)) {
passed++;
if (verbose) console.log(`${algo}: ${steps.length} Steps in ${elapsed}ms`);
} else {
failed++;
console.log(`${algo}: nicht sortiert (${elapsed}ms)`);
}
}
// ── Ergebnis ──
console.log('\n' + '═'.repeat(50));
console.log(`\nErgebnis: ${passed}/${totalTests} bestanden${failed > 0 ? `, ${failed} fehlgeschlagen` : ''}`);
if (failures.length > 0) {
console.log('\nFehler:');
for (const f of failures) {
console.log(` - ${f.algo} + ${f.preset} (${f.size}): ${f.error}`);
}
process.exit(1);
} else {
console.log('\n✅ Alle Tests bestanden!');
process.exit(0);
}