409 lines
12 KiB
JavaScript
409 lines
12 KiB
JavaScript
/**
|
|
* 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);
|
|
}
|