diff --git a/assets/monaco_ide.js b/assets/monaco_ide.js
index d8d53d3..92b0f17 100644
--- a/assets/monaco_ide.js
+++ b/assets/monaco_ide.js
@@ -162,15 +162,25 @@
logToTerminal(`🧪 Führe automatisierte Tests aus (Strg+Shift+Enter)...\n----------------------------------------\n`, "info");
- const result = await runner.runTests(userCode, currentChapterData.testCode, logToTerminal);
+ const result = await runner.runTests(userCode, currentChapterData.testCode, logToTerminal, currentChapterData);
- if (result.success) {
+ const isGenuineSuccess = Boolean(
+ result &&
+ result.success === true &&
+ result.total > 0 &&
+ (!result.failures || result.failures === 0) &&
+ (!result.errors || result.errors === 0)
+ );
+
+ if (isGenuineSuccess) {
logToTerminal(`\n🎉 HERVORRAGEND! Alle ${result.total || 'erforderlichen'} Tests erfolgreich bestanden! (+100 XP)\n`, "success");
triggerSuccessCelebration();
recordChapterSolved();
} else {
- logToTerminal(`\n⚠️ ${result.failures || 1} Tests fehlgeschlagen. Überarbeite deinen Code!\n`, "warning");
- if (result.rawOutput) {
+ const failCount = (result && (result.failures || result.errors)) ? (result.failures + (result.errors || 0)) : 1;
+ const errorMsg = (result && result.error) ? `\n❌ ${result.error}\n` : `\n⚠️ ${failCount} Test(s) fehlgeschlagen oder unvollständig. Überarbeite deinen Code!\n`;
+ logToTerminal(errorMsg, "warning");
+ if (result && result.rawOutput) {
suggestErrorInterpreter(result.rawOutput);
}
}
diff --git a/assets/runners/cpp_runner.js b/assets/runners/cpp_runner.js
index 0ef1e65..0ff212a 100644
--- a/assets/runners/cpp_runner.js
+++ b/assets/runners/cpp_runner.js
@@ -56,18 +56,154 @@
return { success: true };
},
- async runTests(userCode, testCode, logCallback) {
- logCallback("🧪 Führe GoogleTest (gtest) Suite aus...\n----------------------------------------\n", "info");
-
- const testLines = testCode.split("\n").filter(l => l.trim().startsWith("TEST(") || l.trim().startsWith("// TEST:"));
- let total = Math.max(testLines.length, 4);
-
- for (let i = 1; i <= total; i++) {
- logCallback(`[ RUN ] CppTestSuite.TestCase_${i}\n[ OK ] CppTestSuite.TestCase_${i} (0 ms)\n`, "stdout");
+ async runTests(userCode, testCode, logCallback, chapterContext) {
+ if (chapterContext && chapterContext.starterCode) {
+ const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim();
+ const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim();
+ if (cleanUser === cleanStarter) {
+ logCallback("❌ FEHLER: Der C/C++ Code wurde noch nicht bearbeitet!\nBitte implementiere die geforderte Logik und entferne die TODO-Hinweise.\n", "error");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" };
+ }
}
- logCallback(`\n[==========] ${total} tests from 1 test suite ran. (1 ms total)\n[ PASSED ] ${total} tests.\n🎉 Alle GoogleTest Assertions erfolgreich bestanden!\n`, "success");
- return { success: true, total, passed: total, failures: 0 };
+ logCallback("🧪 Führe GoogleTest (gtest) Suite aus...\n----------------------------------------\n", "info");
+
+ // Prüfe Klammer-Balance
+ let braceCount = 0;
+ for (const char of (userCode || "")) {
+ if (char === '{') braceCount++;
+ else if (char === '}') braceCount--;
+ }
+ if (braceCount !== 0) {
+ logCallback(`\n❌ C++ Kompilierfehler: Ungleichgewicht an geschweiften Klammern (Differenz: ${braceCount}).\n`, "error");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 1, rawOutput: "Syntax error: unmatched braces" };
+ }
+
+ const testCases = [];
+ const testRegex = /TEST\s*\(\s*([A-Za-z0-9_]+)\s*,\s*([A-Za-z0-9_]+)\s*\)\s*\{([\s\S]*?)\n\}/g;
+ let match;
+ while ((match = testRegex.exec(testCode)) !== null) {
+ testCases.push({
+ fullName: `${match[1]}.${match[2]}`,
+ target: match[2],
+ body: match[3]
+ });
+ }
+ if (testCases.length === 0) {
+ const commentRegex = /\/\/\s*TEST:\s*(?:Teilziel\s+\d+\s*-\s*)?([A-Za-z0-9_]+)/gi;
+ while ((match = commentRegex.exec(testCode)) !== null) {
+ testCases.push({
+ fullName: `CppTestSuite.${match[1]}`,
+ target: match[1],
+ body: ""
+ });
+ }
+ }
+ if (testCases.length === 0) {
+ const userSymbols = [...(userCode || "").matchAll(/(?:[A-Za-z0-9_]+[*&\s]+)+([A-Za-z0-9_]+)\s*\([^)]*\)\s*\{/g)].map(m => m[1]).filter(s => s !== "if" && s !== "for" && s !== "while" && s !== "switch" && s !== "main");
+ if (userSymbols.length > 0) {
+ userSymbols.forEach(s => testCases.push({
+ fullName: `CppTestSuite.${s}`,
+ target: s,
+ body: ""
+ }));
+ } else {
+ testCases.push({
+ fullName: "CppTestSuite.ExerciseLogic",
+ target: "ExerciseLogic",
+ body: ""
+ });
+ }
+ }
+
+ let passed = 0;
+ let failed = 0;
+
+ for (const test of testCases) {
+ const testName = test.fullName;
+ let testPassed = true;
+ let failReason = "";
+
+ // Finde das zu testende C/C++ Symbol
+ let searchSymbol = "";
+ if (test.target && userCode.includes(test.target)) {
+ searchSymbol = test.target;
+ } else if (test.body) {
+ // Extrahiere Funktionsaufrufe im Testkörper
+ const calls = [...test.body.matchAll(/\b([A-Za-z0-9_]{3,})\s*\(/g)].map(m => m[1]);
+ for (const c of calls) {
+ if (c !== "assert" && c !== "EXPECT_EQ" && c !== "ASSERT_TRUE" && c !== "sizeof" && c !== "memset" && userCode.includes(c)) {
+ searchSymbol = c;
+ break;
+ }
+ }
+ }
+ if (!searchSymbol) {
+ searchSymbol = test.target.replace(/^Test/, "");
+ }
+
+ if (searchSymbol !== "ExerciseLogic" && !userCode.includes(searchSymbol)) {
+ testPassed = false;
+ failReason = `Funktion, Klasse oder Methode '${searchSymbol}' nicht deklariert.`;
+ } else {
+ // Extrahiere exakten Funktionsrumpf innerhalb der geschweiften Klammern
+ let fnBody = "";
+ if (searchSymbol === "ExerciseLogic") {
+ fnBody = userCode;
+ } else {
+ const fnIdx = userCode.indexOf(searchSymbol);
+ if (fnIdx !== -1) {
+ const openBrace = userCode.indexOf("{", fnIdx);
+ if (openBrace !== -1) {
+ let depth = 1;
+ let endIdx = openBrace + 1;
+ while (endIdx < userCode.length && depth > 0) {
+ if (userCode[endIdx] === '{') depth++;
+ else if (userCode[endIdx] === '}') depth--;
+ endIdx++;
+ }
+ fnBody = userCode.slice(openBrace + 1, endIdx - 1);
+ }
+ }
+ }
+
+ const cleanBody = fnBody.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").trim();
+ const isStubOnly = (
+ cleanBody === "" ||
+ cleanBody === 'return "";' ||
+ cleanBody === 'return 0;' ||
+ cleanBody === 'return 0.0;' ||
+ cleanBody === 'return false;' ||
+ cleanBody === 'return nullptr;' ||
+ cleanBody === 'return NULL;' ||
+ cleanBody === 'return;'
+ );
+
+ if (/\/\/\s*TODO:/i.test(fnBody) || /\/\/\s*🎯\s*TEILZIEL/i.test(fnBody)) {
+ testPassed = false;
+ failReason = `Symbol '${searchSymbol}' enthält noch ungelöste TODOs/Teilziele.`;
+ } else if (isStubOnly) {
+ testPassed = false;
+ failReason = `Symbol '${searchSymbol}' gibt noch Standard-Platzhalterwerte zurück.`;
+ }
+ }
+
+ if (testPassed) {
+ logCallback(`[ RUN ] ${testName}\n[ OK ] ${testName} (0 ms)\n`, "stdout");
+ passed++;
+ } else {
+ logCallback(`[ RUN ] ${testName}\n[ FAILED ] ${testName} (0 ms)\n gtest error: ${failReason}\n`, "stderr");
+ failed++;
+ }
+ }
+
+ if (failed === 0 && passed > 0) {
+ logCallback(`\n[==========] ${testCases.length} tests from 1 test suite ran. (1 ms total)\n[ PASSED ] ${passed} tests.\n🎉 Alle GoogleTest Assertions erfolgreich bestanden!\n`, "success");
+ return { success: true, total: testCases.length, passed, failures: 0 };
+ } else {
+ logCallback(`\n[==========] ${testCases.length} tests from 1 test suite ran.\n[ FAILED ] ${failed} tests.\n⚠️ ${failed} von ${testCases.length} Tests fehlgeschlagen.\n`, "warning");
+ return { success: false, total: testCases.length, passed, failures: failed };
+ }
}
};
diff --git a/assets/runners/csharp_runner.js b/assets/runners/csharp_runner.js
index a6161de..f858efd 100644
--- a/assets/runners/csharp_runner.js
+++ b/assets/runners/csharp_runner.js
@@ -56,18 +56,132 @@
return { success: true };
},
- async runTests(userCode, testCode, logCallback) {
- logCallback("🧪 Führe `dotnet test` (xUnit) Testsuite aus...\n----------------------------------------\n", "info");
-
- const testLines = testCode.split("\n").filter(l => l.trim().startsWith("[Fact]") || l.trim().startsWith("[Theory]") || l.trim().startsWith("// TEST:"));
- let total = Math.max(testLines.length, 4);
-
- for (let i = 1; i <= total; i++) {
- logCallback(`[xUnit.net 00:00:00.01] Test_${i} [PASS]\n`, "stdout");
+ async runTests(userCode, testCode, logCallback, chapterContext) {
+ if (chapterContext && chapterContext.starterCode) {
+ const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim();
+ const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim();
+ if (cleanUser === cleanStarter) {
+ logCallback("❌ FEHLER: Der C#-Code wurde noch nicht bearbeitet!\nBitte implementiere die geforderte Logik und entferne die TODO-Hinweise.\n", "error");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" };
+ }
}
- logCallback("\nPassed! - Failed: 0, Passed: " + total + ", Skipped: 0, Total: " + total + "\n🎉 Alle xUnit Tests erfolgreich bestanden!\n", "success");
- return { success: true, total, passed: total, failures: 0 };
+ logCallback("🧪 Führe `dotnet test` (xUnit) Testsuite aus...\n----------------------------------------\n", "info");
+
+ // Prüfe Klammer-Balance
+ let braceCount = 0;
+ for (const char of (userCode || "")) {
+ if (char === '{') braceCount++;
+ else if (char === '}') braceCount--;
+ }
+ if (braceCount !== 0) {
+ logCallback(`\n❌ C# Kompilierfehler: Ungleichgewicht an geschweiften Klammern (Differenz: ${braceCount}).\n`, "error");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 1, rawOutput: "Syntax error: unmatched braces" };
+ }
+
+ const testCases = [];
+ const testRegex = /(?:void|public void|Task)\s+(Test[A-Za-z0-9_]+)\s*\(/g;
+ let match;
+ while ((match = testRegex.exec(testCode)) !== null) {
+ testCases.push(match[1]);
+ }
+ if (testCases.length === 0) {
+ const commentRegex = /\/\/\s*TEST:\s*(?:Testet\s+)?([A-Za-z0-9_]+)/gi;
+ while ((match = commentRegex.exec(testCode)) !== null) {
+ testCases.push(`Test_${match[1]}`);
+ }
+ }
+ if (testCases.length === 0) {
+ const userSymbols = [...(userCode || "").matchAll(/(?:public|private|static|\s)+\s+(?:class|[A-Za-z0-9_<>[\]]+)\s+([A-Za-z0-9_]+)\s*(?:\(|\{)/g)].map(m => m[1]).filter(s => s !== "class" && s !== "Aufgabe");
+ if (userSymbols.length > 0) {
+ userSymbols.forEach(s => testCases.push(`Test_${s}`));
+ } else {
+ testCases.push("TestExerciseLogic");
+ }
+ }
+
+ let passed = 0;
+ let failed = 0;
+
+ for (const testName of testCases) {
+ const targetMethod = testName.replace(/^Test_?/, "");
+ let testPassed = true;
+ let failReason = "";
+
+ if (targetMethod !== "ExerciseLogic" && !userCode.includes(targetMethod)) {
+ testPassed = false;
+ failReason = `Methode, Eigenschaft oder Klasse '${targetMethod}' nicht gefunden.`;
+ } else {
+ // Extrahiere exakten Methodenrumpf (unterstützt {} Blöcke und => Expressions)
+ let fnBody = "";
+ let searchSym = targetMethod;
+ if (searchSym === "ExerciseLogic") {
+ fnBody = userCode;
+ } else {
+ const fnIdx = userCode.indexOf(searchSym);
+ if (fnIdx !== -1) {
+ const openBrace = userCode.indexOf("{", fnIdx);
+ const arrowIdx = userCode.indexOf("=>", fnIdx);
+ if (arrowIdx !== -1 && (openBrace === -1 || arrowIdx < openBrace)) {
+ // Expression-bodied member e.g. => ...;
+ const semiIdx = userCode.indexOf(";", arrowIdx);
+ if (semiIdx !== -1) {
+ fnBody = userCode.slice(arrowIdx + 2, semiIdx);
+ }
+ } else if (openBrace !== -1) {
+ let depth = 1;
+ let endIdx = openBrace + 1;
+ while (endIdx < userCode.length && depth > 0) {
+ if (userCode[endIdx] === '{') depth++;
+ else if (userCode[endIdx] === '}') depth--;
+ endIdx++;
+ }
+ fnBody = userCode.slice(openBrace + 1, endIdx - 1);
+ }
+ }
+ }
+
+ const cleanBody = fnBody.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").trim();
+ const isStubOnly = (
+ cleanBody === "" ||
+ cleanBody === 'throw new NotImplementedException();' ||
+ cleanBody === 'throw new NotImplementedException()' ||
+ cleanBody === 'return "";' ||
+ cleanBody === 'return 0;' ||
+ cleanBody === 'return 0.0;' ||
+ cleanBody === 'return false;' ||
+ cleanBody === 'return null;' ||
+ cleanBody === 'return;'
+ );
+
+ if (/\/\/\s*TODO:/i.test(fnBody) || /\/\/\s*🎯\s*TEILZIEL/i.test(fnBody)) {
+ testPassed = false;
+ failReason = `Methode '${targetMethod}' enthält noch ungelöste TODOs/Teilziele.`;
+ } else if (/throw new NotImplementedException/i.test(fnBody)) {
+ testPassed = false;
+ failReason = `Methode '${targetMethod}' wirft noch 'NotImplementedException()'.`;
+ } else if (isStubOnly) {
+ testPassed = false;
+ failReason = `Methode '${targetMethod}' gibt noch Standard-Platzhalterwerte zurück.`;
+ }
+ }
+
+ if (testPassed) {
+ logCallback(`[xUnit.net 00:00:00.01] ${testName} [PASS]\n`, "stdout");
+ passed++;
+ } else {
+ logCallback(`[xUnit.net 00:00:00.01] ${testName} [FAIL]\n Assert.Equal() Failure: ${failReason}\n`, "stderr");
+ failed++;
+ }
+ }
+
+ if (failed === 0 && passed > 0) {
+ logCallback(`\nPassed! - Failed: 0, Passed: ${passed}, Skipped: 0, Total: ${testCases.length}\n🎉 Alle xUnit Tests erfolgreich bestanden!\n`, "success");
+ return { success: true, total: testCases.length, passed, failures: 0 };
+ } else {
+ logCallback(`\nFailed! - Failed: ${failed}, Passed: ${passed}, Skipped: 0, Total: ${testCases.length}\n⚠️ ${failed} von ${testCases.length} Tests fehlgeschlagen.\n`, "warning");
+ return { success: false, total: testCases.length, passed, failures: failed };
+ }
}
};
diff --git a/assets/runners/go_runner.js b/assets/runners/go_runner.js
index 53c9ffb..fbfc93c 100644
--- a/assets/runners/go_runner.js
+++ b/assets/runners/go_runner.js
@@ -56,18 +56,104 @@
return { success: true };
},
- async runTests(userCode, testCode, logCallback) {
- logCallback("🧪 Führe `go test -v ./...` Test-Suite aus...\n----------------------------------------\n", "info");
-
- const testLines = testCode.split("\n").filter(l => l.trim().startsWith("func Test") || l.trim().startsWith("// TEST:"));
- let total = Math.max(testLines.length, 4);
-
- for (let i = 1; i <= total; i++) {
- logCallback(`=== RUN TestFunction_${i}\n--- PASS: TestFunction_${i} (0.00s)\n`, "stdout");
+ async runTests(userCode, testCode, logCallback, chapterContext) {
+ if (chapterContext && chapterContext.starterCode) {
+ const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim();
+ const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim();
+ if (cleanUser === cleanStarter) {
+ logCallback("❌ FEHLER: Der Go-Code wurde noch nicht bearbeitet!\nBitte implementiere die geforderten Funktionen und entferne die TODO-Hinweise.\n", "error");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" };
+ }
}
- logCallback("\nPASS\nok exercise/package 0.004s\n🎉 Alle Go-Unittests erfolgreich bestanden!\n", "success");
- return { success: true, total, passed: total, failures: 0 };
+ logCallback("🧪 Führe `go test -v ./...` Test-Suite aus...\n----------------------------------------\n", "info");
+
+ // Prüfe Klammer-Balance
+ let braceCount = 0;
+ for (const char of (userCode || "")) {
+ if (char === '{') braceCount++;
+ else if (char === '}') braceCount--;
+ }
+ if (braceCount !== 0) {
+ logCallback(`\n❌ Go Kompilierfehler: Ungleichgewicht an geschweiften Klammern (Differenz: ${braceCount}).\n`, "error");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 1, rawOutput: "Syntax error: unmatched braces" };
+ }
+
+ const testCases = [];
+ const testRegex = /func\s+(Test[A-Za-z0-9_]+)\s*\(/g;
+ let match;
+ while ((match = testRegex.exec(testCode)) !== null) {
+ testCases.push(match[1]);
+ }
+ if (testCases.length === 0) testCases.push("TestExerciseLogic");
+
+ let passed = 0;
+ let failed = 0;
+
+ for (const testName of testCases) {
+ const targetFn = testName.replace(/^Test/, "");
+ const fnRegex = new RegExp(`func\\s+${targetFn}\\b`);
+
+ let testPassed = true;
+ let failReason = "";
+
+ if (targetFn !== "ExerciseLogic" && !fnRegex.test(userCode) && !userCode.includes(targetFn)) {
+ testPassed = false;
+ failReason = `Funktion '${targetFn}' nicht deklariert oder falsche Signatur.`;
+ } else {
+ // Extrahiere exakten Funktionsrumpf innerhalb der geschweiften Klammern
+ let fnBody = "";
+ const fnIdx = userCode.indexOf(targetFn);
+ if (fnIdx !== -1) {
+ const openBrace = userCode.indexOf("{", fnIdx);
+ if (openBrace !== -1) {
+ let depth = 1;
+ let endIdx = openBrace + 1;
+ while (endIdx < userCode.length && depth > 0) {
+ if (userCode[endIdx] === '{') depth++;
+ else if (userCode[endIdx] === '}') depth--;
+ endIdx++;
+ }
+ fnBody = userCode.slice(openBrace + 1, endIdx - 1);
+ }
+ }
+
+ const cleanBody = fnBody.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").trim();
+ const isStubOnly = (
+ cleanBody === "" ||
+ cleanBody === 'return "", 0, 0, false' ||
+ cleanBody === 'return 0.0' ||
+ cleanBody === 'return 0' ||
+ cleanBody === 'return ""' ||
+ cleanBody === 'return false' ||
+ cleanBody === 'return'
+ );
+
+ if (/\/\/\s*TODO:/i.test(fnBody)) {
+ testPassed = false;
+ failReason = `Funktion '${targetFn}' enthält noch ungelöste TODO-Anweisungen.`;
+ } else if (isStubOnly) {
+ testPassed = false;
+ failReason = `Funktion '${targetFn}' gibt noch Standard-Platzhalterwerte zurück.`;
+ }
+ }
+
+ if (testPassed) {
+ logCallback(`=== RUN ${testName}\n--- PASS: ${testName} (0.00s)\n`, "stdout");
+ passed++;
+ } else {
+ logCallback(`=== RUN ${testName}\n--- FAIL: ${testName} (0.00s)\n exercise_test.go: ${failReason}\n`, "stderr");
+ failed++;
+ }
+ }
+
+ if (failed === 0 && passed > 0) {
+ logCallback(`\nPASS\nok exercise/package 0.004s\n🎉 Alle ${passed} Go-Unittests erfolgreich bestanden!\n`, "success");
+ return { success: true, total: testCases.length, passed, failures: 0 };
+ } else {
+ logCallback(`\nFAIL\nexit status 1\nFAIL exercise/package 0.005s\n⚠️ ${failed} von ${testCases.length} Tests fehlgeschlagen.\n`, "warning");
+ return { success: false, total: testCases.length, passed, failures: failed };
+ }
}
};
diff --git a/assets/runners/html_css_runner.js b/assets/runners/html_css_runner.js
index 807a48a..67de2ec 100644
--- a/assets/runners/html_css_runner.js
+++ b/assets/runners/html_css_runner.js
@@ -54,7 +54,16 @@
return { success: true };
},
- async runTests(userCode, testCode, logCallback) {
+ async runTests(userCode, testCode, logCallback, chapterContext) {
+ if (chapterContext && chapterContext.starterCode) {
+ const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim();
+ const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim();
+ if (cleanUser === cleanStarter) {
+ logCallback("❌ FEHLER: Die HTML/CSS-Aufgabe wurde noch nicht bearbeitet!\nBitte implementiere die geforderten Tags und CSS-Regeln.\n", "error");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" };
+ }
+ }
+
logCallback("🧪 Führe automatisierte DOM- & CSS-Prüfungen aus...\n----------------------------------------\n", "info");
const parser = new DOMParser();
diff --git a/assets/runners/java_runner.js b/assets/runners/java_runner.js
index 40108ae..0e97667 100644
--- a/assets/runners/java_runner.js
+++ b/assets/runners/java_runner.js
@@ -56,18 +56,154 @@
return { success: true };
},
- async runTests(userCode, testCode, logCallback) {
- logCallback("🧪 Führe JUnit 5 Testsuite aus...\n----------------------------------------\n", "info");
-
- const testLines = testCode.split("\n").filter(l => l.trim().startsWith("@Test") || l.trim().startsWith("// TEST:"));
- let total = Math.max(testLines.length, 4);
-
- for (let i = 1; i <= total; i++) {
- logCallback(`[✔] JUnit 5 Test ${i}/${total} PASSED\n`, "stdout");
+ async runTests(userCode, testCode, logCallback, chapterContext) {
+ if (chapterContext && chapterContext.starterCode) {
+ const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim();
+ const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim();
+ if (cleanUser === cleanStarter) {
+ logCallback("❌ FEHLER: Der Java-Code wurde noch nicht bearbeitet!\nBitte implementiere die geforderte Logik und entferne die TODO-Hinweise.\n", "error");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" };
+ }
}
- logCallback("\n🎉 JUnit 5: Alle Tests erfolgreich bestanden!\n", "success");
- return { success: true, total, passed: total, failures: 0 };
+ logCallback("🧪 Führe JUnit 5 Testsuite aus...\n----------------------------------------\n", "info");
+
+ // Prüfe Klammer-Balance
+ let braceCount = 0;
+ for (const char of (userCode || "")) {
+ if (char === '{') braceCount++;
+ else if (char === '}') braceCount--;
+ }
+ if (braceCount !== 0) {
+ logCallback(`\n❌ Java Kompilierfehler: Ungleichgewicht an geschweiften Klammern (Differenz: ${braceCount}).\n`, "error");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 1, rawOutput: "Syntax error: unmatched braces" };
+ }
+
+ const testCases = [];
+ const testRegex = /(?:void|public void|public static void)\s+(test[A-Za-z0-9_]+)\s*\([^)]*\)\s*\{([\s\S]*?)\n\s*\}/g;
+ let match;
+ while ((match = testRegex.exec(testCode)) !== null) {
+ testCases.push({
+ name: match[1],
+ body: match[2]
+ });
+ }
+ if (testCases.length === 0) {
+ const commentRegex = /\/\/\s*TEST:\s*(?:Testet\s+)?([A-Za-z0-9_]+)/gi;
+ while ((match = commentRegex.exec(testCode)) !== null) {
+ testCases.push({
+ name: `test${match[1]}`,
+ body: ""
+ });
+ }
+ }
+ if (testCases.length === 0) {
+ const userSymbols = [...(userCode || "").matchAll(/(?:public|private|protected|static|\s)+\s+(?:class|record|[A-Za-z0-9_<>[\]]+)\s+([A-Za-z0-9_]+)\s*(?:\(|\{)/g)].map(m => m[1]).filter(s => s !== "class" && s !== "record" && s !== "Aufgabe" && s !== "Musterloesung");
+ if (userSymbols.length > 0) {
+ userSymbols.forEach(s => testCases.push({ name: `test${s}`, body: "" }));
+ } else {
+ testCases.push({ name: "testAufgabeLogic", body: "" });
+ }
+ }
+
+ let passed = 0;
+ let failed = 0;
+
+ for (const test of testCases) {
+ const testName = test.name;
+ let testPassed = true;
+ let failReason = "";
+
+ // Ermittle das Zielsymbol (Klasse oder Methode)
+ let searchSymbol = "";
+ const targetMethod = testName.replace(/^test_?/, "");
+
+ if (userCode.includes(targetMethod)) {
+ searchSymbol = targetMethod;
+ } else if (test.body) {
+ // Extrahiere Aufrufe wie Aufgabe.XYZ oder Klassen/Methodennamen im Test-Body
+ const aufgabeMatches = [...test.body.matchAll(/Aufgabe\.([A-Za-z0-9_]+)/g)].map(m => m[1]);
+ for (const a of aufgabeMatches) {
+ if (userCode.includes(a)) {
+ searchSymbol = a;
+ break;
+ }
+ }
+ if (!searchSymbol) {
+ const calls = [...test.body.matchAll(/\b([A-Za-z0-9_]{3,})\b/g)].map(m => m[1]);
+ for (const c of calls) {
+ if (c !== "assertEquals" && c !== "assertTrue" && c !== "assertFalse" && c !== "assert" && c !== "Aufgabe" && userCode.includes(c)) {
+ searchSymbol = c;
+ break;
+ }
+ }
+ }
+ }
+ if (!searchSymbol) {
+ searchSymbol = targetMethod;
+ }
+
+ if (searchSymbol !== "AufgabeLogic" && !userCode.includes(searchSymbol)) {
+ testPassed = false;
+ failReason = `Symbol, Methode oder Klasse '${searchSymbol}' nicht im Java-Code gefunden.`;
+ } else {
+ // Extrahiere exakten Methodenrumpf innerhalb der geschweiften Klammern
+ let fnBody = "";
+ if (searchSymbol === "AufgabeLogic") {
+ fnBody = userCode;
+ } else {
+ const fnIdx = userCode.indexOf(searchSymbol);
+ if (fnIdx !== -1) {
+ const openBrace = userCode.indexOf("{", fnIdx);
+ if (openBrace !== -1) {
+ let depth = 1;
+ let endIdx = openBrace + 1;
+ while (endIdx < userCode.length && depth > 0) {
+ if (userCode[endIdx] === '{') depth++;
+ else if (userCode[endIdx] === '}') depth--;
+ endIdx++;
+ }
+ fnBody = userCode.slice(openBrace + 1, endIdx - 1);
+ }
+ }
+ }
+
+ const cleanBody = fnBody.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").trim();
+ const isStubOnly = (
+ cleanBody === "" ||
+ cleanBody === 'return "";' ||
+ cleanBody === 'return 0;' ||
+ cleanBody === 'return 0.0;' ||
+ cleanBody === 'return false;' ||
+ cleanBody === 'return null;' ||
+ cleanBody === 'return;'
+ );
+
+ if (/\/\/\s*TODO:/i.test(fnBody) || /\/\/\s*🎯\s*TEILZIEL/i.test(fnBody)) {
+ testPassed = false;
+ failReason = `Bereich '${searchSymbol}' enthält noch ungelöste TODOs/Teilziele.`;
+ } else if (isStubOnly) {
+ testPassed = false;
+ failReason = `Bereich '${searchSymbol}' gibt noch Standard-Platzhalterwerte zurück.`;
+ }
+ }
+
+ if (testPassed) {
+ logCallback(`[✔] JUnit 5: ${testName}() PASSED\n`, "stdout");
+ passed++;
+ } else {
+ logCallback(`[✘] JUnit 5: ${testName}() FAILED\n AssertionError: ${failReason}\n`, "stderr");
+ failed++;
+ }
+ }
+
+ if (failed === 0 && passed > 0) {
+ logCallback(`\n🎉 JUnit 5: Alle ${passed} Tests erfolgreich bestanden!\n`, "success");
+ return { success: true, total: testCases.length, passed, failures: 0 };
+ } else {
+ logCallback(`\n⚠️ JUnit 5: ${failed} von ${testCases.length} Tests fehlgeschlagen.\n`, "warning");
+ return { success: false, total: testCases.length, passed, failures: failed };
+ }
}
};
diff --git a/assets/runners/js_runner.js b/assets/runners/js_runner.js
index 1b1afaa..3a48061 100644
--- a/assets/runners/js_runner.js
+++ b/assets/runners/js_runner.js
@@ -126,13 +126,22 @@
/**
* Führt JS Unittests aus
*/
- async runTests(userCode, testCode, logCallback) {
+ async runTests(userCode, testCode, logCallback, chapterContext) {
const sec = this.validateSecurity(userCode);
if (!sec.safe) {
logCallback(`\n${sec.error}\n\n`, "error");
return { success: false, error: sec.error };
}
+ if (chapterContext && chapterContext.starterCode) {
+ const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim();
+ const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim();
+ if (cleanUser === cleanStarter) {
+ logCallback("❌ FEHLER: Die JavaScript-Aufgabe wurde noch nicht bearbeitet!\nBitte implementiere die geforderte Logik in der Datei.\n", "error");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" };
+ }
+ }
+
return new Promise((resolve) => {
const fullScript = `
let total = 0;
diff --git a/assets/runners/powershell_runner.js b/assets/runners/powershell_runner.js
index 2f855d8..1a6e351 100644
--- a/assets/runners/powershell_runner.js
+++ b/assets/runners/powershell_runner.js
@@ -58,26 +58,76 @@
return { success: true };
},
- async runTests(userCode, testCode, logCallback) {
- logCallback("🧪 Führe automatisierte Pester v5 Test-Suite aus...\n----------------------------------------\n", "info");
-
- const testLines = testCode.split("\n").filter(l => l.trim().startsWith("It ") || l.trim().startsWith("# TEST:"));
- let total = Math.max(testLines.length, 4);
-
- for (let i = 1; i <= total; i++) {
- logCallback(`[+] Pester Test ${i}/${total} bestanden [Passed]\n`, "stdout");
+ async runTests(userCode, testCode, logCallback, chapterContext) {
+ if (chapterContext && chapterContext.starterCode) {
+ const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim();
+ const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim();
+ if (cleanUser === cleanStarter) {
+ logCallback("❌ FEHLER: Das PowerShell-Skript wurde noch nicht bearbeitet!\nBitte implementiere die geforderte Logik in der Datei, bevor du die Tests ausführst.\n", "error");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" };
+ }
}
- logCallback("\n🎉 Pester v5 Suite: Alle Tests erfolgreich bestanden!\n", "success");
- return { success: true, total, passed: total, failures: 0 };
+ logCallback("🧪 Starte automatisierte Pester Testsuite in isolierter PowerShell 7+ Sandbox...\n----------------------------------------\n", "info");
+
+ try {
+ const token = window.AUTH ? window.AUTH.getToken() : localStorage.getItem("auth_token");
+ const isAD = chapterContext && (chapterContext.courseId === "active_directory" || chapterContext.courseId === "ad");
+ const payload = {
+ language: isAD ? "active_directory" : "powershell",
+ base_path: (chapterContext && chapterContext.basePath) ? chapterContext.basePath : "",
+ user_code: userCode,
+ task_file: (chapterContext && chapterContext.taskFile) ? chapterContext.taskFile : "aufgabe.ps1",
+ test_file: (chapterContext && chapterContext.testFile) ? chapterContext.testFile : "test_aufgabe.ps1"
+ };
+
+ const res = await fetch("/api/runners/test", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...(token ? { "Authorization": `Bearer ${token}` } : {})
+ },
+ body: JSON.stringify(payload)
+ });
+
+ const data = await res.json();
+
+ if (data.stdout) {
+ logCallback(data.stdout, "stdout");
+ }
+ if (data.stderr && data.stderr.trim()) {
+ logCallback(data.stderr, "stderr");
+ }
+
+ if (data.error && !data.stdout) {
+ logCallback(`\n❌ Fehler: ${data.error}\n`, "error");
+ }
+
+ return {
+ success: Boolean(data.success),
+ total: data.total || 1,
+ passed: data.passed || 0,
+ failures: data.failures || (data.success ? 0 : 1),
+ rawOutput: (data.stdout || "") + "\n" + (data.stderr || "")
+ };
+ } catch (err) {
+ logCallback(`\n❌ Verbindungsfehler zum Backend-Runner: ${err.message}\n`, "error");
+ return { success: false, total: 1, passed: 0, failures: 1, rawOutput: err.message };
+ }
}
};
if (window.RunnerRegistry) {
window.RunnerRegistry.register(PowerShellAdapter);
+ window.RunnerRegistry.register({ ...PowerShellAdapter, id: "active_directory" });
+ window.RunnerRegistry.register({ ...PowerShellAdapter, id: "pwsh" });
} else {
document.addEventListener("DOMContentLoaded", () => {
- if (window.RunnerRegistry) window.RunnerRegistry.register(PowerShellAdapter);
+ if (window.RunnerRegistry) {
+ window.RunnerRegistry.register(PowerShellAdapter);
+ window.RunnerRegistry.register({ ...PowerShellAdapter, id: "active_directory" });
+ window.RunnerRegistry.register({ ...PowerShellAdapter, id: "pwsh" });
+ }
});
}
})();
diff --git a/assets/runners/python_runner.js b/assets/runners/python_runner.js
index 6aee077..11ca44f 100644
--- a/assets/runners/python_runner.js
+++ b/assets/runners/python_runner.js
@@ -47,7 +47,9 @@
});
}
- pyodideInstance = await window.loadPyodide();
+ pyodideInstance = await window.loadPyodide({
+ indexURL: "https://cdn.jsdelivr.net/pyodide/v0.25.0/full/"
+ });
// Sandbox-Härtung im Pyodide-Interpreter aktivieren
if (window.CODE_GUARD && window.CODE_GUARD.getSandboxBootstrap) {
@@ -113,41 +115,58 @@
/**
* Führt automatisierte Unittests aus (🧪 Tests starten)
*/
- async runTests(userCode, testCode, logCallback) {
+ async runTests(userCode, testCode, logCallback, chapterContext) {
const securityCheck = this.validateSecurity(userCode);
if (!securityCheck.safe) {
logCallback(`\n${securityCheck.error}\n\n`, "error");
return { success: false, error: securityCheck.error };
}
+ // Guard: Prüfe ob Startercode noch völlig unberührt ist
+ if (chapterContext && chapterContext.starterCode) {
+ const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim();
+ const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim();
+ if (cleanUser === cleanStarter) {
+ logCallback("❌ FEHLER: Aufgabe noch nicht bearbeitet!\nBitte implementiere die geforderte Logik in der Datei, bevor du die Tests ausführst.\n", "error");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" };
+ }
+ }
+
const py = await this.loadEngine(logCallback);
if (!py) return { success: false, error: "Pyodide nicht verfügbar" };
try {
py.FS.writeFile("aufgabe.py", userCode);
+ py.FS.writeFile("test_aufgabe.py", testCode);
const testRunnerScript = `
-import unittest, io, sys
+import importlib, io, sys, unittest
from unittest import TextTestRunner
+if '.' not in sys.path:
+ sys.path.insert(0, '.')
+
for mod in list(sys.modules.keys()):
if mod in ('aufgabe', 'test_aufgabe'):
del sys.modules[mod]
import aufgabe
+import test_aufgabe
+importlib.reload(aufgabe)
+importlib.reload(test_aufgabe)
-${testCode}
-
-suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
+loader = unittest.TestLoader()
+suite = loader.loadTestsFromModule(test_aufgabe)
stream = io.StringIO()
runner = TextTestRunner(stream=stream, verbosity=2)
result = runner.run(suite)
output_text = stream.getvalue()
-success = result.wasSuccessful()
total_runs = result.testsRun
failures = len(result.failures)
errors = len(result.errors)
+success = (total_runs > 0) and (failures == 0) and (errors == 0) and result.wasSuccessful()
+
(success, total_runs, failures, errors, output_text)
`;
@@ -166,8 +185,10 @@ errors = len(result.errors)
logCallback(outputText + "\n", "stdout");
+ const hasPassed = Boolean(isSuccess) && totalRuns > 0 && fails === 0 && errs === 0;
+
return {
- success: Boolean(isSuccess),
+ success: hasPassed,
total: totalRuns,
passed: totalRuns - (fails + errs),
failures: fails,
diff --git a/assets/runners/rust_runner.js b/assets/runners/rust_runner.js
index d11305a..507be17 100644
--- a/assets/runners/rust_runner.js
+++ b/assets/runners/rust_runner.js
@@ -56,18 +56,107 @@
return { success: true };
},
- async runTests(userCode, testCode, logCallback) {
- logCallback("🧪 Führe `cargo test` Suite aus...\n----------------------------------------\n", "info");
-
- const testLines = testCode.split("\n").filter(l => l.trim().startsWith("#[test]") || l.trim().startsWith("// TEST:"));
- let total = Math.max(testLines.length, 4);
-
- for (let i = 1; i <= total; i++) {
- logCallback(`test test_case_${i} ... ok\n`, "stdout");
+ async runTests(userCode, testCode, logCallback, chapterContext) {
+ if (chapterContext && chapterContext.starterCode) {
+ const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim();
+ const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim();
+ if (cleanUser === cleanStarter) {
+ logCallback("❌ FEHLER: Der Rust-Code wurde noch nicht bearbeitet!\nBitte implementiere die geforderte Logik und entferne die TODO-Hinweise.\n", "error");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" };
+ }
}
- logCallback("\ntest result: ok. " + total + " passed; 0 failed; 0 ignored\n🎉 Alle Cargo Tests erfolgreich bestanden!\n", "success");
- return { success: true, total, passed: total, failures: 0 };
+ logCallback("🧪 Führe `cargo test` Suite aus...\n----------------------------------------\n", "info");
+
+ // Prüfe Klammer-Balance
+ let braceCount = 0;
+ for (const char of (userCode || "")) {
+ if (char === '{') braceCount++;
+ else if (char === '}') braceCount--;
+ }
+ if (braceCount !== 0) {
+ logCallback(`\n❌ Rust Kompilierfehler: Ungleichgewicht an geschweiften Klammern (Differenz: ${braceCount}).\n`, "error");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 1, rawOutput: "Syntax error: unmatched braces" };
+ }
+
+ const testCases = [];
+ const testRegex = /fn\s+(test_[A-Za-z0-9_]+)\s*\(/g;
+ let match;
+ while ((match = testRegex.exec(testCode)) !== null) {
+ testCases.push(match[1]);
+ }
+ if (testCases.length === 0) testCases.push("test_exercise_logic");
+
+ let passed = 0;
+ let failed = 0;
+
+ for (const testName of testCases) {
+ const targetFn = testName.replace(/^test_/, "");
+ let testPassed = true;
+ let failReason = "";
+
+ if (targetFn !== "exercise_logic" && !userCode.includes(targetFn)) {
+ testPassed = false;
+ failReason = `Funktion oder Typ '${targetFn}' nicht im Rust-Code gefunden.`;
+ } else {
+ let fnBody = "";
+ const fnIdx = userCode.indexOf(targetFn);
+ if (fnIdx !== -1) {
+ const openBrace = userCode.indexOf("{", fnIdx);
+ if (openBrace !== -1) {
+ let depth = 1;
+ let endIdx = openBrace + 1;
+ while (endIdx < userCode.length && depth > 0) {
+ if (userCode[endIdx] === '{') depth++;
+ else if (userCode[endIdx] === '}') depth--;
+ endIdx++;
+ }
+ fnBody = userCode.slice(openBrace + 1, endIdx - 1);
+ }
+ }
+
+ const cleanBody = fnBody.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").trim();
+ const isStubOnly = (
+ cleanBody === "" ||
+ cleanBody === 'todo!()' ||
+ cleanBody === 'todo!();' ||
+ cleanBody === 'unimplemented!()' ||
+ cleanBody === 'unimplemented!();' ||
+ cleanBody === '0' ||
+ cleanBody === '0.0' ||
+ cleanBody === '()' ||
+ cleanBody === 'false' ||
+ cleanBody === 'true'
+ );
+
+ if (/todo!\(\)/.test(cleanBody) || /unimplemented!\(\)/.test(cleanBody)) {
+ testPassed = false;
+ failReason = `Enthält noch ungelöstes 'todo!()' oder 'unimplemented!()' Makro.`;
+ } else if (/\/\/\s*TODO:/i.test(fnBody)) {
+ testPassed = false;
+ failReason = `Enthält noch ungelöste TODO-Anweisungen.`;
+ } else if (isStubOnly) {
+ testPassed = false;
+ failReason = `Funktion '${targetFn}' gibt noch Standard-Platzhalterwerte zurück.`;
+ }
+ }
+
+ if (testPassed) {
+ logCallback(`test ${testName} ... ok\n`, "stdout");
+ passed++;
+ } else {
+ logCallback(`test ${testName} ... FAILED\n src/lib.rs: ${failReason}\n`, "stderr");
+ failed++;
+ }
+ }
+
+ if (failed === 0 && passed > 0) {
+ logCallback(`\ntest result: ok. ${passed} passed; 0 failed; 0 ignored\n🎉 Alle Cargo Tests erfolgreich bestanden!\n`, "success");
+ return { success: true, total: testCases.length, passed, failures: 0 };
+ } else {
+ logCallback(`\ntest result: FAILED. ${passed} passed; ${failed} failed; 0 ignored\n⚠️ ${failed} von ${testCases.length} Tests fehlgeschlagen.\n`, "warning");
+ return { success: false, total: testCases.length, passed, failures: failed };
+ }
}
};
diff --git a/assets/runners/shell_runner.js b/assets/runners/shell_runner.js
index 2897dc1..2063664 100644
--- a/assets/runners/shell_runner.js
+++ b/assets/runners/shell_runner.js
@@ -63,29 +63,77 @@
return { success: true };
},
- async runTests(userCode, testCode, logCallback) {
- logCallback("🧪 Führe automatisierte Shell-Unittests (BATS-Stil) aus...\n----------------------------------------\n", "info");
-
- // Simuliere Test-Ausführung
- const testLines = testCode.split("\n").filter(l => l.trim().startsWith("# TEST:") || l.trim().startsWith("assert"));
- let passed = 0;
- let total = Math.max(testLines.length, 4);
-
- for (let i = 1; i <= total; i++) {
- logCallback(`✓ Test ${i}/${total} erfolgreich bestanden.\n`, "stdout");
- passed++;
+ async runTests(userCode, testCode, logCallback, chapterContext) {
+ if (chapterContext && chapterContext.starterCode) {
+ const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim();
+ const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim();
+ if (cleanUser === cleanStarter) {
+ logCallback("❌ FEHLER: Das Shell-Skript wurde noch nicht bearbeitet!\nBitte implementiere die geforderte Logik in der Datei, bevor du die Tests ausführst.\n", "error");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" };
+ }
}
- logCallback("\n🎉 100% aller Shell-Tests erfolgreich bestanden!\n", "success");
- return { success: true, total, passed, failures: 0 };
+ logCallback("🧪 Starte automatisierte Testsuite in isolierter Linux-Sandbox...\n----------------------------------------\n", "info");
+
+ try {
+ const token = window.AUTH ? window.AUTH.getToken() : localStorage.getItem("auth_token");
+ const payload = {
+ language: (chapterContext && (chapterContext.courseId === "git" ? "git" : (chapterContext.courseId === "dns_records" ? "dns" : "bash"))) || "bash",
+ base_path: (chapterContext && chapterContext.basePath) ? chapterContext.basePath : "",
+ user_code: userCode,
+ task_file: (chapterContext && chapterContext.taskFile) ? chapterContext.taskFile : "aufgabe.sh",
+ test_file: (chapterContext && chapterContext.testFile) ? chapterContext.testFile : "test_aufgabe.sh"
+ };
+
+ const res = await fetch("/api/runners/test", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...(token ? { "Authorization": `Bearer ${token}` } : {})
+ },
+ body: JSON.stringify(payload)
+ });
+
+ const data = await res.json();
+
+ if (data.stdout) {
+ logCallback(data.stdout, "stdout");
+ }
+ if (data.stderr && data.stderr.trim()) {
+ logCallback(data.stderr, "stderr");
+ }
+
+ if (data.error && !data.stdout) {
+ logCallback(`\n❌ Fehler: ${data.error}\n`, "error");
+ }
+
+ return {
+ success: Boolean(data.success),
+ total: data.total || 1,
+ passed: data.passed || 0,
+ failures: data.failures || (data.success ? 0 : 1),
+ rawOutput: (data.stdout || "") + "\n" + (data.stderr || "")
+ };
+ } catch (err) {
+ logCallback(`\n❌ Verbindungsfehler zum Backend-Runner: ${err.message}\n`, "error");
+ return { success: false, total: 1, passed: 0, failures: 1, rawOutput: err.message };
+ }
}
};
if (window.RunnerRegistry) {
window.RunnerRegistry.register(ShellAdapter);
+ window.RunnerRegistry.register({ ...ShellAdapter, id: "git" });
+ window.RunnerRegistry.register({ ...ShellAdapter, id: "dns_records" });
+ window.RunnerRegistry.register({ ...ShellAdapter, id: "dns" });
} else {
document.addEventListener("DOMContentLoaded", () => {
- if (window.RunnerRegistry) window.RunnerRegistry.register(ShellAdapter);
+ if (window.RunnerRegistry) {
+ window.RunnerRegistry.register(ShellAdapter);
+ window.RunnerRegistry.register({ ...ShellAdapter, id: "git" });
+ window.RunnerRegistry.register({ ...ShellAdapter, id: "dns_records" });
+ window.RunnerRegistry.register({ ...ShellAdapter, id: "dns" });
+ }
});
}
})();
diff --git a/assets/runners/sql_runner.js b/assets/runners/sql_runner.js
index ac4369d..06d514b 100644
--- a/assets/runners/sql_runner.js
+++ b/assets/runners/sql_runner.js
@@ -126,7 +126,16 @@
}
},
- async runTests(userCode, testCode, logCallback) {
+ async runTests(userCode, testCode, logCallback, chapterContext) {
+ if (chapterContext && chapterContext.starterCode) {
+ const cleanUser = (userCode || "").replace(/\r\n/g, "\n").trim();
+ const cleanStarter = (chapterContext.starterCode || "").replace(/\r\n/g, "\n").trim();
+ if (cleanUser === cleanStarter) {
+ logCallback("❌ FEHLER: Die SQL-Aufgabe wurde noch nicht bearbeitet!\nBitte implementiere die geforderten Tabellen und Abfragen.\n", "error");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Aufgabe noch nicht bearbeitet" };
+ }
+ }
+
const SQL = await this.loadEngine(logCallback);
if (!SQL) return { success: false, error: "SQL-Engine nicht geladen" };
@@ -141,10 +150,29 @@
logCallback(formatted, "stdout");
db.close();
- return { success: true, total: 3, passed: 3, failures: 0 };
+
+ if (!testResults || testResults.length === 0) {
+ logCallback("⚠️ Die Validierungsabfrage ergab kein Tabellenergebnis.\n", "warning");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "Keine Ergebnisdaten" };
+ }
+
+ let totalRows = 0;
+ for (const res of testResults) {
+ if (res.values && res.values.length > 0) {
+ totalRows += res.values.length;
+ }
+ }
+
+ if (totalRows === 0) {
+ logCallback("⚠️ Die Testabfrage lieferte 0 Ergebniszeilen zurück. Überprüfe WHERE-Bedingungen und Datensätze!\n", "warning");
+ return { success: false, total: 1, passed: 0, failures: 1, errors: 0, rawOutput: "0 Ergebniszeilen" };
+ }
+
+ const totalTests = Math.max(testResults.length, 3);
+ return { success: true, total: totalTests, passed: totalTests, failures: 0 };
} catch (err) {
logCallback(`\n❌ Test-Fehler im SQL:\n${err.message}\n`, "error");
- return { success: false, error: err.message, failures: 1 };
+ return { success: false, error: err.message, total: 1, passed: 0, failures: 1, rawOutput: err.message };
}
}
};
diff --git a/courses/active_directory/01_ad_architektur_und_wald_baum_domaene/aufgabe.ps1 b/courses/active_directory/01_ad_architektur_und_wald_baum_domaene/aufgabe.ps1
index 7bcc28a..aafbe42 100644
--- a/courses/active_directory/01_ad_architektur_und_wald_baum_domaene/aufgabe.ps1
+++ b/courses/active_directory/01_ad_architektur_und_wald_baum_domaene/aufgabe.ps1
@@ -21,7 +21,7 @@ function Get-ADDomainArchitecture {
# TODO: Implementiere die geforderten Active Directory Schritte
$result = [PSCustomObject]@{
- Success = $true
+ Success = $false
Domain = $Domain
Status = "Ready"
Module = "01_ad_architektur_und_wald_baum_domaene"
diff --git a/courses/active_directory/02_domaenencontroller_und_fsmo_rollen/aufgabe.ps1 b/courses/active_directory/02_domaenencontroller_und_fsmo_rollen/aufgabe.ps1
index cfe2b0a..308a607 100644
--- a/courses/active_directory/02_domaenencontroller_und_fsmo_rollen/aufgabe.ps1
+++ b/courses/active_directory/02_domaenencontroller_und_fsmo_rollen/aufgabe.ps1
@@ -21,7 +21,7 @@ function Get-FSMORoleOwner {
# TODO: Implementiere die geforderten Active Directory Schritte
$result = [PSCustomObject]@{
- Success = $true
+ Success = $false
Domain = $Domain
Status = "Ready"
Module = "02_domaenencontroller_und_fsmo_rollen"
diff --git a/courses/active_directory/03_organisationseinheiten_und_struktur/aufgabe.ps1 b/courses/active_directory/03_organisationseinheiten_und_struktur/aufgabe.ps1
index af1e6b5..5d37fa1 100644
--- a/courses/active_directory/03_organisationseinheiten_und_struktur/aufgabe.ps1
+++ b/courses/active_directory/03_organisationseinheiten_und_struktur/aufgabe.ps1
@@ -21,7 +21,7 @@ function New-ADStructureOU {
# TODO: Implementiere die geforderten Active Directory Schritte
$result = [PSCustomObject]@{
- Success = $true
+ Success = $false
Domain = $Domain
Status = "Ready"
Module = "03_organisationseinheiten_und_struktur"
diff --git a/courses/active_directory/04_standorte_subnetze_und_replikation/aufgabe.ps1 b/courses/active_directory/04_standorte_subnetze_und_replikation/aufgabe.ps1
index 22986da..d2c84ea 100644
--- a/courses/active_directory/04_standorte_subnetze_und_replikation/aufgabe.ps1
+++ b/courses/active_directory/04_standorte_subnetze_und_replikation/aufgabe.ps1
@@ -21,7 +21,7 @@ function Sync-ADReplicationSite {
# TODO: Implementiere die geforderten Active Directory Schritte
$result = [PSCustomObject]@{
- Success = $true
+ Success = $false
Domain = $Domain
Status = "Ready"
Module = "04_standorte_subnetze_und_replikation"
diff --git a/courses/active_directory/05_benutzerkonten_und_attribute/aufgabe.ps1 b/courses/active_directory/05_benutzerkonten_und_attribute/aufgabe.ps1
index 92ed89f..4c56c75 100644
--- a/courses/active_directory/05_benutzerkonten_und_attribute/aufgabe.ps1
+++ b/courses/active_directory/05_benutzerkonten_und_attribute/aufgabe.ps1
@@ -21,7 +21,7 @@ function New-EnterpriseADUser {
# TODO: Implementiere die geforderten Active Directory Schritte
$result = [PSCustomObject]@{
- Success = $true
+ Success = $false
Domain = $Domain
Status = "Ready"
Module = "05_benutzerkonten_und_attribute"
diff --git a/courses/active_directory/06_computerkonten_und_domaenenbeitritt/aufgabe.ps1 b/courses/active_directory/06_computerkonten_und_domaenenbeitritt/aufgabe.ps1
index b8d230d..3470249 100644
--- a/courses/active_directory/06_computerkonten_und_domaenenbeitritt/aufgabe.ps1
+++ b/courses/active_directory/06_computerkonten_und_domaenenbeitritt/aufgabe.ps1
@@ -21,7 +21,7 @@ function Add-DomainComputerAccount {
# TODO: Implementiere die geforderten Active Directory Schritte
$result = [PSCustomObject]@{
- Success = $true
+ Success = $false
Domain = $Domain
Status = "Ready"
Module = "06_computerkonten_und_domaenenbeitritt"
diff --git a/courses/active_directory/07_sicherheitsgruppen_und_bereichstypen/aufgabe.ps1 b/courses/active_directory/07_sicherheitsgruppen_und_bereichstypen/aufgabe.ps1
index d8a8628..4693a9f 100644
--- a/courses/active_directory/07_sicherheitsgruppen_und_bereichstypen/aufgabe.ps1
+++ b/courses/active_directory/07_sicherheitsgruppen_und_bereichstypen/aufgabe.ps1
@@ -21,7 +21,7 @@ function New-SecurityGroupHierarchy {
# TODO: Implementiere die geforderten Active Directory Schritte
$result = [PSCustomObject]@{
- Success = $true
+ Success = $false
Domain = $Domain
Status = "Ready"
Module = "07_sicherheitsgruppen_und_bereichstypen"
diff --git a/courses/active_directory/08_das_agdlp_prinzip_in_der_praxis/aufgabe.ps1 b/courses/active_directory/08_das_agdlp_prinzip_in_der_praxis/aufgabe.ps1
index 2562c85..cbc6ae1 100644
--- a/courses/active_directory/08_das_agdlp_prinzip_in_der_praxis/aufgabe.ps1
+++ b/courses/active_directory/08_das_agdlp_prinzip_in_der_praxis/aufgabe.ps1
@@ -21,7 +21,7 @@ function Deploy-AGDLPFramework {
# TODO: Implementiere die geforderten Active Directory Schritte
$result = [PSCustomObject]@{
- Success = $true
+ Success = $false
Domain = $Domain
Status = "Ready"
Module = "08_das_agdlp_prinzip_in_der_praxis"
diff --git a/courses/active_directory/09_gpo_grundlagen_und_lsdous/aufgabe.ps1 b/courses/active_directory/09_gpo_grundlagen_und_lsdous/aufgabe.ps1
index b53265a..081c5c9 100644
--- a/courses/active_directory/09_gpo_grundlagen_und_lsdous/aufgabe.ps1
+++ b/courses/active_directory/09_gpo_grundlagen_und_lsdous/aufgabe.ps1
@@ -21,7 +21,7 @@ function New-TargetedGPO {
# TODO: Implementiere die geforderten Active Directory Schritte
$result = [PSCustomObject]@{
- Success = $true
+ Success = $false
Domain = $Domain
Status = "Ready"
Module = "09_gpo_grundlagen_und_lsdous"
diff --git a/courses/active_directory/10_computerkonfiguration_vs_benutzerkonfiguration/aufgabe.ps1 b/courses/active_directory/10_computerkonfiguration_vs_benutzerkonfiguration/aufgabe.ps1
index 681b611..b1858d1 100644
--- a/courses/active_directory/10_computerkonfiguration_vs_benutzerkonfiguration/aufgabe.ps1
+++ b/courses/active_directory/10_computerkonfiguration_vs_benutzerkonfiguration/aufgabe.ps1
@@ -21,7 +21,7 @@ function Set-GPOLoopbackPolicy {
# TODO: Implementiere die geforderten Active Directory Schritte
$result = [PSCustomObject]@{
- Success = $true
+ Success = $false
Domain = $Domain
Status = "Ready"
Module = "10_computerkonfiguration_vs_benutzerkonfiguration"
diff --git a/courses/active_directory/11_sicherheitsfilterung_und_wmi_filter/aufgabe.ps1 b/courses/active_directory/11_sicherheitsfilterung_und_wmi_filter/aufgabe.ps1
index ad7e79b..349b3ce 100644
--- a/courses/active_directory/11_sicherheitsfilterung_und_wmi_filter/aufgabe.ps1
+++ b/courses/active_directory/11_sicherheitsfilterung_und_wmi_filter/aufgabe.ps1
@@ -21,7 +21,7 @@ function New-WmiGpoFilter {
# TODO: Implementiere die geforderten Active Directory Schritte
$result = [PSCustomObject]@{
- Success = $true
+ Success = $false
Domain = $Domain
Status = "Ready"
Module = "11_sicherheitsfilterung_und_wmi_filter"
diff --git a/courses/active_directory/12_gpo_troubleshooting_gpupdate_und_gpresult/aufgabe.ps1 b/courses/active_directory/12_gpo_troubleshooting_gpupdate_und_gpresult/aufgabe.ps1
index 0b8f9d1..2af4fa2 100644
--- a/courses/active_directory/12_gpo_troubleshooting_gpupdate_und_gpresult/aufgabe.ps1
+++ b/courses/active_directory/12_gpo_troubleshooting_gpupdate_und_gpresult/aufgabe.ps1
@@ -21,7 +21,7 @@ function Invoke-GPODiagnostics {
# TODO: Implementiere die geforderten Active Directory Schritte
$result = [PSCustomObject]@{
- Success = $true
+ Success = $false
Domain = $Domain
Status = "Ready"
Module = "12_gpo_troubleshooting_gpupdate_und_gpresult"
diff --git a/courses/active_directory/13_powershell_activedirectory_modul_basics/aufgabe.ps1 b/courses/active_directory/13_powershell_activedirectory_modul_basics/aufgabe.ps1
index 09fbd56..9de832e 100644
--- a/courses/active_directory/13_powershell_activedirectory_modul_basics/aufgabe.ps1
+++ b/courses/active_directory/13_powershell_activedirectory_modul_basics/aufgabe.ps1
@@ -21,7 +21,7 @@ function Get-ADUserReport {
# TODO: Implementiere die geforderten Active Directory Schritte
$result = [PSCustomObject]@{
- Success = $true
+ Success = $false
Domain = $Domain
Status = "Ready"
Module = "13_powershell_activedirectory_modul_basics"
diff --git a/courses/active_directory/14_csv_bulk_import_und_provisioning/aufgabe.ps1 b/courses/active_directory/14_csv_bulk_import_und_provisioning/aufgabe.ps1
index 6dbeb5f..5b3e1f0 100644
--- a/courses/active_directory/14_csv_bulk_import_und_provisioning/aufgabe.ps1
+++ b/courses/active_directory/14_csv_bulk_import_und_provisioning/aufgabe.ps1
@@ -21,7 +21,7 @@ function Import-ADUsersFromCSV {
# TODO: Implementiere die geforderten Active Directory Schritte
$result = [PSCustomObject]@{
- Success = $true
+ Success = $false
Domain = $Domain
Status = "Ready"
Module = "14_csv_bulk_import_und_provisioning"
diff --git a/courses/active_directory/15_ntfs_und_share_berechtigungssteuerung/aufgabe.ps1 b/courses/active_directory/15_ntfs_und_share_berechtigungssteuerung/aufgabe.ps1
index 2384550..b3a16cb 100644
--- a/courses/active_directory/15_ntfs_und_share_berechtigungssteuerung/aufgabe.ps1
+++ b/courses/active_directory/15_ntfs_und_share_berechtigungssteuerung/aufgabe.ps1
@@ -21,7 +21,7 @@ function Set-ShareAndNtfsSecurity {
# TODO: Implementiere die geforderten Active Directory Schritte
$result = [PSCustomObject]@{
- Success = $true
+ Success = $false
Domain = $Domain
Status = "Ready"
Module = "15_ntfs_und_share_berechtigungssteuerung"
diff --git a/courses/active_directory/16_master_abschlussprojekt_ad_automation_suite/aufgabe.ps1 b/courses/active_directory/16_master_abschlussprojekt_ad_automation_suite/aufgabe.ps1
index 722a574..f08bf42 100644
--- a/courses/active_directory/16_master_abschlussprojekt_ad_automation_suite/aufgabe.ps1
+++ b/courses/active_directory/16_master_abschlussprojekt_ad_automation_suite/aufgabe.ps1
@@ -21,7 +21,7 @@ function Invoke-ADEnterpriseAutomationSuite {
# TODO: Implementiere die geforderten Active Directory Schritte
$result = [PSCustomObject]@{
- Success = $true
+ Success = $false
Domain = $Domain
Status = "Ready"
Module = "16_master_abschlussprojekt_ad_automation_suite"
diff --git a/courses/dns_records/01_dns_hierarchie_und_root_server/aufgabe.sh b/courses/dns_records/01_dns_hierarchie_und_root_server/aufgabe.sh
index b395edd..33ccdbb 100755
--- a/courses/dns_records/01_dns_hierarchie_und_root_server/aufgabe.sh
+++ b/courses/dns_records/01_dns_hierarchie_und_root_server/aufgabe.sh
@@ -13,7 +13,7 @@ analyze_dns_hierarchy() {
echo "▶ Analysiere DNS-Konfiguration für $domain..."
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
- return 0
+ return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
diff --git a/courses/dns_records/02_rekursive_vs_iterative_abfragen/aufgabe.sh b/courses/dns_records/02_rekursive_vs_iterative_abfragen/aufgabe.sh
index 698374a..65e5fe1 100755
--- a/courses/dns_records/02_rekursive_vs_iterative_abfragen/aufgabe.sh
+++ b/courses/dns_records/02_rekursive_vs_iterative_abfragen/aufgabe.sh
@@ -13,7 +13,7 @@ trace_dns_resolution() {
echo "▶ Analysiere DNS-Konfiguration für $domain..."
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
- return 0
+ return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
diff --git a/courses/dns_records/03_ports_udp_tcp_und_dns_paketaufbau/aufgabe.sh b/courses/dns_records/03_ports_udp_tcp_und_dns_paketaufbau/aufgabe.sh
index b99e789..b32eb90 100755
--- a/courses/dns_records/03_ports_udp_tcp_und_dns_paketaufbau/aufgabe.sh
+++ b/courses/dns_records/03_ports_udp_tcp_und_dns_paketaufbau/aufgabe.sh
@@ -13,7 +13,7 @@ inspect_dns_transport() {
echo "▶ Analysiere DNS-Konfiguration für $domain..."
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
- return 0
+ return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
diff --git a/courses/dns_records/04_ttl_und_dns_propagation/aufgabe.sh b/courses/dns_records/04_ttl_und_dns_propagation/aufgabe.sh
index f90223a..e44f88f 100755
--- a/courses/dns_records/04_ttl_und_dns_propagation/aufgabe.sh
+++ b/courses/dns_records/04_ttl_und_dns_propagation/aufgabe.sh
@@ -13,7 +13,7 @@ calculate_ttl_migration() {
echo "▶ Analysiere DNS-Konfiguration für $domain..."
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
- return 0
+ return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
diff --git a/courses/dns_records/05_a_und_aaaa_records/aufgabe.sh b/courses/dns_records/05_a_und_aaaa_records/aufgabe.sh
index 2825b45..8983ee2 100755
--- a/courses/dns_records/05_a_und_aaaa_records/aufgabe.sh
+++ b/courses/dns_records/05_a_und_aaaa_records/aufgabe.sh
@@ -13,7 +13,7 @@ configure_a_aaaa_records() {
echo "▶ Analysiere DNS-Konfiguration für $domain..."
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
- return 0
+ return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
diff --git a/courses/dns_records/06_cname_canonical_name_und_alias_regeln/aufgabe.sh b/courses/dns_records/06_cname_canonical_name_und_alias_regeln/aufgabe.sh
index 5e0be8c..6e16296 100755
--- a/courses/dns_records/06_cname_canonical_name_und_alias_regeln/aufgabe.sh
+++ b/courses/dns_records/06_cname_canonical_name_und_alias_regeln/aufgabe.sh
@@ -13,7 +13,7 @@ validate_cname_rules() {
echo "▶ Analysiere DNS-Konfiguration für $domain..."
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
- return 0
+ return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
diff --git a/courses/dns_records/07_soa_start_of_authority_und_serial/aufgabe.sh b/courses/dns_records/07_soa_start_of_authority_und_serial/aufgabe.sh
index cd46091..c6e081b 100755
--- a/courses/dns_records/07_soa_start_of_authority_und_serial/aufgabe.sh
+++ b/courses/dns_records/07_soa_start_of_authority_und_serial/aufgabe.sh
@@ -13,7 +13,7 @@ generate_soa_header() {
echo "▶ Analysiere DNS-Konfiguration für $domain..."
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
- return 0
+ return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
diff --git a/courses/dns_records/08_ns_und_ptr_reverse_dns_lookups/aufgabe.sh b/courses/dns_records/08_ns_und_ptr_reverse_dns_lookups/aufgabe.sh
index d66cb5f..78a4709 100755
--- a/courses/dns_records/08_ns_und_ptr_reverse_dns_lookups/aufgabe.sh
+++ b/courses/dns_records/08_ns_und_ptr_reverse_dns_lookups/aufgabe.sh
@@ -13,7 +13,7 @@ configure_ns_and_ptr() {
echo "▶ Analysiere DNS-Konfiguration für $domain..."
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
- return 0
+ return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
diff --git a/courses/dns_records/09_mx_mail_exchange_und_prioritaeten/aufgabe.sh b/courses/dns_records/09_mx_mail_exchange_und_prioritaeten/aufgabe.sh
index 872cde6..09bbf66 100755
--- a/courses/dns_records/09_mx_mail_exchange_und_prioritaeten/aufgabe.sh
+++ b/courses/dns_records/09_mx_mail_exchange_und_prioritaeten/aufgabe.sh
@@ -13,7 +13,7 @@ setup_mx_records() {
echo "▶ Analysiere DNS-Konfiguration für $domain..."
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
- return 0
+ return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
diff --git a/courses/dns_records/10_txt_records_und_spf_sender_policy_framework/aufgabe.sh b/courses/dns_records/10_txt_records_und_spf_sender_policy_framework/aufgabe.sh
index 3756739..192a7ea 100755
--- a/courses/dns_records/10_txt_records_und_spf_sender_policy_framework/aufgabe.sh
+++ b/courses/dns_records/10_txt_records_und_spf_sender_policy_framework/aufgabe.sh
@@ -13,7 +13,7 @@ create_spf_record() {
echo "▶ Analysiere DNS-Konfiguration für $domain..."
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
- return 0
+ return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
diff --git a/courses/dns_records/11_dkim_domainkeys_identified_mail/aufgabe.sh b/courses/dns_records/11_dkim_domainkeys_identified_mail/aufgabe.sh
index 8e35064..60d2328 100755
--- a/courses/dns_records/11_dkim_domainkeys_identified_mail/aufgabe.sh
+++ b/courses/dns_records/11_dkim_domainkeys_identified_mail/aufgabe.sh
@@ -13,7 +13,7 @@ publish_dkim_key() {
echo "▶ Analysiere DNS-Konfiguration für $domain..."
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
- return 0
+ return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
diff --git a/courses/dns_records/12_dmarc_und_email_security_suite/aufgabe.sh b/courses/dns_records/12_dmarc_und_email_security_suite/aufgabe.sh
index 711bbb9..38c1ebe 100755
--- a/courses/dns_records/12_dmarc_und_email_security_suite/aufgabe.sh
+++ b/courses/dns_records/12_dmarc_und_email_security_suite/aufgabe.sh
@@ -13,7 +13,7 @@ deploy_dmarc_policy() {
echo "▶ Analysiere DNS-Konfiguration für $domain..."
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
- return 0
+ return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
diff --git a/courses/dns_records/13_srv_service_records_fuer_ad_und_sip/aufgabe.sh b/courses/dns_records/13_srv_service_records_fuer_ad_und_sip/aufgabe.sh
index bc2ed14..4552cc2 100755
--- a/courses/dns_records/13_srv_service_records_fuer_ad_und_sip/aufgabe.sh
+++ b/courses/dns_records/13_srv_service_records_fuer_ad_und_sip/aufgabe.sh
@@ -13,7 +13,7 @@ configure_ad_srv_records() {
echo "▶ Analysiere DNS-Konfiguration für $domain..."
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
- return 0
+ return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
diff --git a/courses/dns_records/14_caa_und_dnssec_sicherheit/aufgabe.sh b/courses/dns_records/14_caa_und_dnssec_sicherheit/aufgabe.sh
index 3f21ead..eb12ff5 100755
--- a/courses/dns_records/14_caa_und_dnssec_sicherheit/aufgabe.sh
+++ b/courses/dns_records/14_caa_und_dnssec_sicherheit/aufgabe.sh
@@ -13,7 +13,7 @@ setup_caa_and_dnssec() {
echo "▶ Analysiere DNS-Konfiguration für $domain..."
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
- return 0
+ return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
diff --git a/courses/dns_records/15_dns_troubleshooting_mit_dig_und_nslookup/aufgabe.sh b/courses/dns_records/15_dns_troubleshooting_mit_dig_und_nslookup/aufgabe.sh
index 834dae8..ed54669 100755
--- a/courses/dns_records/15_dns_troubleshooting_mit_dig_und_nslookup/aufgabe.sh
+++ b/courses/dns_records/15_dns_troubleshooting_mit_dig_und_nslookup/aufgabe.sh
@@ -13,7 +13,7 @@ diagnose_dns_health() {
echo "▶ Analysiere DNS-Konfiguration für $domain..."
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
- return 0
+ return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
diff --git a/courses/dns_records/16_master_abschlussprojekt_dns_zone_auditor/aufgabe.sh b/courses/dns_records/16_master_abschlussprojekt_dns_zone_auditor/aufgabe.sh
index cd387b2..7ed1f3f 100755
--- a/courses/dns_records/16_master_abschlussprojekt_dns_zone_auditor/aufgabe.sh
+++ b/courses/dns_records/16_master_abschlussprojekt_dns_zone_auditor/aufgabe.sh
@@ -13,7 +13,7 @@ audit_dns_zone() {
echo "▶ Analysiere DNS-Konfiguration für $domain..."
# TODO: Implementiere die geforderten DNS-Prüfungen oder Record-Erstellungen
- return 0
+ return 1
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
diff --git a/courses/git/01_git_init_und_repo_basics/aufgabe.sh b/courses/git/01_git_init_und_repo_basics/aufgabe.sh
index 46cae10..8616efe 100755
--- a/courses/git/01_git_init_und_repo_basics/aufgabe.sh
+++ b/courses/git/01_git_init_und_repo_basics/aufgabe.sh
@@ -14,7 +14,7 @@ init_repository() {
# TODO: Implementiere die geforderten Git-Befehle
echo "Führe init_repository in $repo_dir aus..."
- return 0
+ return 1
}
# Direkter Aufruf bei Skript-Ausführung
diff --git a/courses/git/02_staging_commits_und_historie/aufgabe.sh b/courses/git/02_staging_commits_und_historie/aufgabe.sh
index a015ea4..a04b1e8 100755
--- a/courses/git/02_staging_commits_und_historie/aufgabe.sh
+++ b/courses/git/02_staging_commits_und_historie/aufgabe.sh
@@ -14,7 +14,7 @@ manage_commits() {
# TODO: Implementiere die geforderten Git-Befehle
echo "Führe manage_commits in $repo_dir aus..."
- return 0
+ return 1
}
# Direkter Aufruf bei Skript-Ausführung
diff --git a/courses/git/03_aenderungen_vergleichen_mit_git_diff/aufgabe.sh b/courses/git/03_aenderungen_vergleichen_mit_git_diff/aufgabe.sh
index 6a70684..1ba2b6c 100755
--- a/courses/git/03_aenderungen_vergleichen_mit_git_diff/aufgabe.sh
+++ b/courses/git/03_aenderungen_vergleichen_mit_git_diff/aufgabe.sh
@@ -14,7 +14,7 @@ diff_inspection() {
# TODO: Implementiere die geforderten Git-Befehle
echo "Führe diff_inspection in $repo_dir aus..."
- return 0
+ return 1
}
# Direkter Aufruf bei Skript-Ausführung
diff --git a/courses/git/04_dateien_ignorieren_und_gitignore/aufgabe.sh b/courses/git/04_dateien_ignorieren_und_gitignore/aufgabe.sh
index 5f96d33..9b31399 100755
--- a/courses/git/04_dateien_ignorieren_und_gitignore/aufgabe.sh
+++ b/courses/git/04_dateien_ignorieren_und_gitignore/aufgabe.sh
@@ -14,7 +14,7 @@ setup_gitignore() {
# TODO: Implementiere die geforderten Git-Befehle
echo "Führe setup_gitignore in $repo_dir aus..."
- return 0
+ return 1
}
# Direkter Aufruf bei Skript-Ausführung
diff --git a/courses/git/05_branching_und_switch/aufgabe.sh b/courses/git/05_branching_und_switch/aufgabe.sh
index db0ab82..9df43ee 100755
--- a/courses/git/05_branching_und_switch/aufgabe.sh
+++ b/courses/git/05_branching_und_switch/aufgabe.sh
@@ -14,7 +14,7 @@ manage_branches() {
# TODO: Implementiere die geforderten Git-Befehle
echo "Führe manage_branches in $repo_dir aus..."
- return 0
+ return 1
}
# Direkter Aufruf bei Skript-Ausführung
diff --git a/courses/git/06_fast_forward_und_3way_merge/aufgabe.sh b/courses/git/06_fast_forward_und_3way_merge/aufgabe.sh
index 48ff5b1..5e593df 100755
--- a/courses/git/06_fast_forward_und_3way_merge/aufgabe.sh
+++ b/courses/git/06_fast_forward_und_3way_merge/aufgabe.sh
@@ -14,7 +14,7 @@ execute_merges() {
# TODO: Implementiere die geforderten Git-Befehle
echo "Führe execute_merges in $repo_dir aus..."
- return 0
+ return 1
}
# Direkter Aufruf bei Skript-Ausführung
diff --git a/courses/git/07_merge_konflikte_verstehen_und_loesen/aufgabe.sh b/courses/git/07_merge_konflikte_verstehen_und_loesen/aufgabe.sh
index 9a79a86..9777c0a 100755
--- a/courses/git/07_merge_konflikte_verstehen_und_loesen/aufgabe.sh
+++ b/courses/git/07_merge_konflikte_verstehen_und_loesen/aufgabe.sh
@@ -14,7 +14,7 @@ resolve_conflicts() {
# TODO: Implementiere die geforderten Git-Befehle
echo "Führe resolve_conflicts in $repo_dir aus..."
- return 0
+ return 1
}
# Direkter Aufruf bei Skript-Ausführung
diff --git a/courses/git/08_temporaere_aenderungen_mit_git_stash/aufgabe.sh b/courses/git/08_temporaere_aenderungen_mit_git_stash/aufgabe.sh
index 7bcd10c..777dcab 100755
--- a/courses/git/08_temporaere_aenderungen_mit_git_stash/aufgabe.sh
+++ b/courses/git/08_temporaere_aenderungen_mit_git_stash/aufgabe.sh
@@ -14,7 +14,7 @@ use_stash() {
# TODO: Implementiere die geforderten Git-Befehle
echo "Führe use_stash in $repo_dir aus..."
- return 0
+ return 1
}
# Direkter Aufruf bei Skript-Ausführung
diff --git a/courses/git/09_remotes_verbinden_und_git_push/aufgabe.sh b/courses/git/09_remotes_verbinden_und_git_push/aufgabe.sh
index 5842df3..6b7574f 100755
--- a/courses/git/09_remotes_verbinden_und_git_push/aufgabe.sh
+++ b/courses/git/09_remotes_verbinden_und_git_push/aufgabe.sh
@@ -14,7 +14,7 @@ setup_remote() {
# TODO: Implementiere die geforderten Git-Befehle
echo "Führe setup_remote in $repo_dir aus..."
- return 0
+ return 1
}
# Direkter Aufruf bei Skript-Ausführung
diff --git a/courses/git/10_git_fetch_vs_git_pull/aufgabe.sh b/courses/git/10_git_fetch_vs_git_pull/aufgabe.sh
index 8984f20..f22fb71 100755
--- a/courses/git/10_git_fetch_vs_git_pull/aufgabe.sh
+++ b/courses/git/10_git_fetch_vs_git_pull/aufgabe.sh
@@ -14,7 +14,7 @@ sync_upstream() {
# TODO: Implementiere die geforderten Git-Befehle
echo "Führe sync_upstream in $repo_dir aus..."
- return 0
+ return 1
}
# Direkter Aufruf bei Skript-Ausführung
diff --git a/courses/git/11_git_tags_und_releases/aufgabe.sh b/courses/git/11_git_tags_und_releases/aufgabe.sh
index 03ddaee..e0ad830 100755
--- a/courses/git/11_git_tags_und_releases/aufgabe.sh
+++ b/courses/git/11_git_tags_und_releases/aufgabe.sh
@@ -14,7 +14,7 @@ manage_tags() {
# TODO: Implementiere die geforderten Git-Befehle
echo "Führe manage_tags in $repo_dir aus..."
- return 0
+ return 1
}
# Direkter Aufruf bei Skript-Ausführung
diff --git a/courses/git/12_cherry_picking_und_selektive_commits/aufgabe.sh b/courses/git/12_cherry_picking_und_selektive_commits/aufgabe.sh
index 2827362..25e563b 100755
--- a/courses/git/12_cherry_picking_und_selektive_commits/aufgabe.sh
+++ b/courses/git/12_cherry_picking_und_selektive_commits/aufgabe.sh
@@ -14,7 +14,7 @@ cherry_pick_commit() {
# TODO: Implementiere die geforderten Git-Befehle
echo "Führe cherry_pick_commit in $repo_dir aus..."
- return 0
+ return 1
}
# Direkter Aufruf bei Skript-Ausführung
diff --git a/courses/git/13_rebase_fuer_saubere_historie/aufgabe.sh b/courses/git/13_rebase_fuer_saubere_historie/aufgabe.sh
index 2c65169..f05d2cf 100755
--- a/courses/git/13_rebase_fuer_saubere_historie/aufgabe.sh
+++ b/courses/git/13_rebase_fuer_saubere_historie/aufgabe.sh
@@ -14,7 +14,7 @@ rebase_feature() {
# TODO: Implementiere die geforderten Git-Befehle
echo "Führe rebase_feature in $repo_dir aus..."
- return 0
+ return 1
}
# Direkter Aufruf bei Skript-Ausführung
diff --git a/courses/git/14_interaktives_rebase_squash_und_fixup/aufgabe.sh b/courses/git/14_interaktives_rebase_squash_und_fixup/aufgabe.sh
index 38e7a6a..c4b25bd 100755
--- a/courses/git/14_interaktives_rebase_squash_und_fixup/aufgabe.sh
+++ b/courses/git/14_interaktives_rebase_squash_und_fixup/aufgabe.sh
@@ -14,7 +14,7 @@ interactive_rebase() {
# TODO: Implementiere die geforderten Git-Befehle
echo "Führe interactive_rebase in $repo_dir aus..."
- return 0
+ return 1
}
# Direkter Aufruf bei Skript-Ausführung
diff --git a/courses/git/15_git_hooks_und_automatisierte_checks/aufgabe.sh b/courses/git/15_git_hooks_und_automatisierte_checks/aufgabe.sh
index 9704855..d31c984 100755
--- a/courses/git/15_git_hooks_und_automatisierte_checks/aufgabe.sh
+++ b/courses/git/15_git_hooks_und_automatisierte_checks/aufgabe.sh
@@ -14,7 +14,7 @@ setup_pre_commit_hook() {
# TODO: Implementiere die geforderten Git-Befehle
echo "Führe setup_pre_commit_hook in $repo_dir aus..."
- return 0
+ return 1
}
# Direkter Aufruf bei Skript-Ausführung
diff --git a/courses/git/16_master_abschlussprojekt_git_flow_und_release/aufgabe.sh b/courses/git/16_master_abschlussprojekt_git_flow_und_release/aufgabe.sh
index f570e8c..b30f9d8 100755
--- a/courses/git/16_master_abschlussprojekt_git_flow_und_release/aufgabe.sh
+++ b/courses/git/16_master_abschlussprojekt_git_flow_und_release/aufgabe.sh
@@ -14,7 +14,7 @@ master_git_flow() {
# TODO: Implementiere die geforderten Git-Befehle
echo "Führe master_git_flow in $repo_dir aus..."
- return 0
+ return 1
}
# Direkter Aufruf bei Skript-Ausführung
diff --git a/server.py b/server.py
index 15d078e..9e7c516 100644
--- a/server.py
+++ b/server.py
@@ -25,6 +25,9 @@ import re
import uuid
import urllib.parse
import ipaddress
+import subprocess
+import tempfile
+import shutil
from pathlib import Path
from collections import defaultdict
@@ -314,18 +317,33 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler):
def is_path_blocked(self, path: str) -> bool:
clean_path = urllib.parse.unquote(path).split("?")[0].lower()
clean_path = os.path.normpath(clean_path).replace("\\", "/")
- blocked_extensions = (
- ".db", ".sqlite", ".sqlite3", ".secret_key", ".py", ".sh", ".php",
- ".jsonl", ".env", ".bak", ".sql", ".log", ".conf", ".ini", ".yaml", ".yml"
- )
- if any(clean_path.endswith(ext) for ext in blocked_extensions):
+
+ # 1. System-, Konfigurations- und geheime Dateien immer strikt sperren
+ if any(seg in clean_path for seg in ["/.git", "/.system_generated", "/.gemini", "platform_data.db", ".secret_key", ".env", ".jsonl"]):
return True
if "/." in clean_path or clean_path.startswith("."):
return True
if clean_path.startswith("/exams/") or clean_path == "/exams":
return True
- if any(seg in clean_path for seg in ["/.git", "/.system_generated", "/.gemini", "platform_data.db", ".secret_key"]):
+ if clean_path in ("/server.py", "server.py"):
return True
+
+ # 2. Gefährliche Endungen für Server-Root und PHP/DB
+ if any(clean_path.endswith(ext) for ext in [".db", ".sqlite", ".sqlite3", ".secret_key", ".php", ".env", ".bak", ".conf"]):
+ return True
+
+ # 3. Kursdateien in den Lehrpfaden und Kursordnern explizit erlauben
+ is_course_file = (
+ clean_path.startswith("/lehrpfad_") or
+ clean_path.startswith("lehrpfad_") or
+ clean_path.startswith("/courses/") or
+ clean_path.startswith("courses/")
+ )
+ if not is_course_file:
+ blocked_outside = (".py", ".sh", ".sql", ".log", ".ini", ".yaml", ".yml")
+ if any(clean_path.endswith(ext) for ext in blocked_outside):
+ return True
+
return False
def do_GET(self):
@@ -1056,6 +1074,250 @@ class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler):
conn.close()
return self.send_json({"success": True, "message": "Einstellungen gespeichert"})
+ # 14. AUTOMATISIERTER RUNNER TESTSUITE ENDPUNKT (BASH, GIT, DNS, POWERSHELL, AD)
+ elif path == "/api/runners/test":
+ user_token = self.get_auth_user()
+ if not user_token:
+ return self.send_json({"error": "Nicht authentifiziert. Bitte melde dich an, um Tests auszuführen."}, 401)
+
+ language = str(body.get("language", "")).strip().lower()
+ base_path = str(body.get("base_path", "")).strip()
+ user_code = str(body.get("user_code", ""))[:100_000]
+ task_file = str(body.get("task_file", "")).strip()
+ test_file = str(body.get("test_file", "")).strip()
+
+ if not base_path or ".." in base_path or base_path.startswith("/") or "\\" in base_path:
+ return self.send_json({"error": "Ungültiger Pfad"}, 400)
+
+ chapter_dir = os.path.normpath(os.path.join(str(BASE_DIR), base_path))
+ if not os.path.isdir(chapter_dir) or not chapter_dir.startswith(str(BASE_DIR)):
+ return self.send_json({"error": "Kapitelverzeichnis nicht gefunden"}, 404)
+
+ # Security: Gefährliche Shell/PowerShell-Befehle blockieren
+ danger_patterns = [
+ r"\brm\s+-[rf]*\s+/(?:\s|$)",
+ r"\bmkfs\b",
+ r"\bdd\s+if=",
+ r"\bshutdown\b",
+ r"\breboot\b",
+ r":\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;",
+ r"\bcurl\b.*\|\s*(?:bash|sh)\b",
+ r"Remove-Item\s+-Recurse\s+[\'\"]?/(?:[\'\"]|\s|$)",
+ r"Format-Volume"
+ ]
+ for pat in danger_patterns:
+ if re.search(pat, user_code, re.IGNORECASE):
+ return self.send_json({
+ "success": False,
+ "error": "Sicherheits-Sperre: Unzulässiger Systembefehl im Code erkannt.",
+ "stdout": "❌ Ausführung durch Sicherheitsfilter blockiert.\n",
+ "failures": 1,
+ "total": 1
+ }, 400)
+
+ sandbox = tempfile.mkdtemp(prefix="test_sandbox_")
+ try:
+ # 1. BASH / GIT / DNS EXECUTION
+ if language in ("bash", "shell", "git", "dns_records", "dns"):
+ actual_task_file = task_file or "aufgabe.sh"
+ actual_test_file = test_file or "test_aufgabe.sh"
+ src_test = os.path.join(chapter_dir, actual_test_file)
+ if not os.path.isfile(src_test):
+ return self.send_json({"error": f"Testdatei '{actual_test_file}' nicht gefunden"}, 404)
+
+ shutil.copy(src_test, os.path.join(sandbox, actual_test_file))
+ user_script_path = os.path.join(sandbox, actual_task_file)
+ with open(user_script_path, "w", encoding="utf-8") as f:
+ f.write(user_code)
+ os.chmod(user_script_path, 0o755)
+ os.chmod(os.path.join(sandbox, actual_test_file), 0o755)
+
+ env = os.environ.copy()
+ env["HOME"] = sandbox
+ env["TERM"] = "xterm-256color"
+
+ proc = subprocess.run(
+ ["bash", actual_test_file, actual_task_file],
+ cwd=sandbox,
+ capture_output=True,
+ text=True,
+ timeout=12,
+ env=env
+ )
+
+ stdout = proc.stdout or ""
+ stderr = proc.stderr or ""
+ exit_code = proc.returncode
+
+ passed_match = re.search(r"(\d+)\s+von\s+(\d+)\s+Tests\s+bestanden", stdout)
+ fail_match = re.search(r"(\d+)\s+von\s+(\d+)\s+Tests\s+fehlgeschlagen", stdout)
+
+ if exit_code == 0:
+ failures = 0
+ passed = int(passed_match.group(1)) if passed_match else 4
+ total = int(passed_match.group(2)) if passed_match else passed
+ else:
+ if fail_match:
+ failures = int(fail_match.group(1))
+ total = int(fail_match.group(2))
+ passed = max(0, total - failures)
+ else:
+ failures = 1
+ passed = 0
+ total = 1
+
+ return self.send_json({
+ "success": exit_code == 0 and failures == 0 and total > 0,
+ "exit_code": exit_code,
+ "stdout": stdout,
+ "stderr": stderr,
+ "total": total,
+ "passed": passed,
+ "failures": failures
+ })
+
+ # 2. POWERSHELL / ACTIVE DIRECTORY EXECUTION
+ elif language in ("powershell", "pwsh", "active_directory", "ad"):
+ actual_task_file = task_file or "aufgabe.ps1"
+ actual_test_file = test_file or "test_aufgabe.ps1"
+ src_test = os.path.join(chapter_dir, actual_test_file)
+ if not os.path.isfile(src_test):
+ return self.send_json({"error": f"Testdatei '{actual_test_file}' nicht gefunden"}, 404)
+
+ shutil.copy(src_test, os.path.join(sandbox, actual_test_file))
+ user_script_path = os.path.join(sandbox, actual_task_file)
+ with open(user_script_path, "w", encoding="utf-8") as f:
+ f.write(user_code)
+
+ pester_shim = f"""
+function Describe ($name, [ScriptBlock]$block) {{
+ Write-Host "`n🧪 Testsuite: $name" -ForegroundColor Cyan
+ & $block
+}}
+function Context ($name, [ScriptBlock]$block) {{
+ Write-Host " 📂 $name" -ForegroundColor DarkCyan
+ & $block
+}}
+$script:PesterTotal = 0
+$script:PesterPassed = 0
+$script:PesterFailed = 0
+
+function It ($name, [ScriptBlock]$block) {{
+ $script:PesterTotal++
+ try {{
+ & $block
+ Write-Host " [+] $name [Passed]" -ForegroundColor Green
+ $script:PesterPassed++
+ }} catch {{
+ Write-Host " [-] $name [Failed]" -ForegroundColor Red
+ Write-Host " $($_.Exception.Message)" -ForegroundColor DarkGray
+ $script:PesterFailed++
+ }}
+}}
+
+function Should {{
+ [CmdletBinding()]
+ param(
+ [Parameter(ValueFromPipeline = $true)]
+ $Actual,
+ [switch]$Be,
+ [switch]$Not,
+ [switch]$BeGreaterThan,
+ [switch]$BeLessThan,
+ [switch]$Match,
+ [Parameter(Position = 0)]
+ $Expected
+ )
+ process {{
+ if ($Be) {{
+ if ("$Actual" -ne "$Expected") {{
+ throw "Expected: $Expected, but got: $Actual"
+ }}
+ }} elseif ($Not) {{
+ if ("$Actual" -eq "$Expected") {{
+ throw "Expected not: $Expected, but got equal value"
+ }}
+ }} elseif ($Match) {{
+ if ("$Actual" -notmatch "$Expected") {{
+ throw "Expected $Actual to match $Expected"
+ }}
+ }} else {{
+ if ("$Actual" -ne "$Expected") {{
+ throw "Expected: $Expected, but got: $Actual"
+ }}
+ }}
+ }}
+}}
+
+. ./{actual_task_file}
+. ./{actual_test_file}
+
+Write-Host "`n📊 Gesamtergebnis: $script:PesterPassed von $script:PesterTotal Tests bestanden."
+if ($script:PesterFailed -gt 0 -or $script:PesterTotal -eq 0) {{
+ Write-Host "❌ $script:PesterFailed Test(s) fehlgeschlagen!" -ForegroundColor Red
+ exit 1
+}} else {{
+ Write-Host "🎉 Alle $script:PesterTotal Tests erfolgreich bestanden!" -ForegroundColor Green
+ exit 0
+}}
+"""
+ runner_path = os.path.join(sandbox, "run_pester_test.ps1")
+ with open(runner_path, "w", encoding="utf-8") as f:
+ f.write(pester_shim)
+
+ proc = subprocess.run(
+ ["pwsh", "-NoProfile", "-NonInteractive", "-File", "run_pester_test.ps1"],
+ cwd=sandbox,
+ capture_output=True,
+ text=True,
+ timeout=12
+ )
+
+ stdout = proc.stdout or ""
+ stderr = proc.stderr or ""
+ exit_code = proc.returncode
+
+ passed_match = re.search(r"(\d+)\s+von\s+(\d+)\s+Tests\s+bestanden", stdout)
+ if passed_match:
+ passed = int(passed_match.group(1))
+ total = int(passed_match.group(2))
+ failures = total - passed
+ else:
+ if exit_code == 0:
+ passed = 4
+ total = 4
+ failures = 0
+ else:
+ passed = 0
+ total = 1
+ failures = 1
+
+ return self.send_json({
+ "success": exit_code == 0 and failures == 0 and total > 0,
+ "exit_code": exit_code,
+ "stdout": stdout,
+ "stderr": stderr,
+ "total": total,
+ "passed": passed,
+ "failures": failures
+ })
+
+ else:
+ return self.send_json({"error": f"Kein Server-Runner für Sprache '{language}' konfiguriert"}, 400)
+
+ except subprocess.TimeoutExpired:
+ return self.send_json({
+ "success": False,
+ "error": "Zeitüberschreitung: Testausführung dauerte länger als 12 Sekunden.",
+ "stdout": "⏱️ Zeitüberschreitung bei der Testausführung.\n",
+ "failures": 1,
+ "total": 1
+ }, 408)
+ except Exception as e:
+ return self.send_json({"error": f"Serverfehler bei Testausführung: {str(e)}"}, 500)
+ finally:
+ shutil.rmtree(sandbox, ignore_errors=True)
+
return self.send_json({"error": "Endpunkt nicht gefunden"}, 404)
def run_server():
diff --git a/workspace.html b/workspace.html
index eb2bfae..550da85 100644
--- a/workspace.html
+++ b/workspace.html
@@ -357,20 +357,20 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+