Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 268 additions & 0 deletions EventHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
const loadAddTwoNumbersButton = document.getElementById("btn-add-two-numbers");
const loadReverseStringButton = document.getElementById("btn-reverse-string");
const clearPageButton = document.getElementById("btn-clear-page");
const addTwoNumbersContent = document.getElementById("add-two-numbers");
const reverseStringContent = document.getElementById("reverse-string");

function clearPage() {
addTwoNumbersContent.innerHTML = "";
reverseStringContent.innerHTML = "";
}

// When Add Two Numbers Clicked => try submissions/add-two-numbers/correct.py, then wrong-answer.py, then broken.py
loadAddTwoNumbersButton.addEventListener("click",function() {
clearPage();
fetch("questions/add-two-numbers/index.md")
.then(res => res.text())
.then(text => {

// Set HTML
const submitHTML = `<button id="btn-submit" type="button">Submit</button>`;
const revealSolutionHTML = `<button id="btn-reveal-solution" type="button">Reveal Solution</button>`;
const testSubmissionHTML = `<button id="btn-test-submission" type="button">Test Submission</button>`;
const brokenSubmissionHTML = `<button id="btn-add-two-numbers-broken" type="button">Broken Input</button>`;
const correctSubmissionHTML = `<button id="btn-add-two-numbers-correct" type="button">Correct Input</button>`;
const wrongSubmissionHTML = `<button id="btn-add-two-numbers-wrong" type="button">Wrong Input</button>`;
const inputBoxHTML = `<textarea id="output" style="width: 25%;" rows="6" enabled></textarea> <br>`;
const resultsHTML = `<div id="test-results"></div>`;

addTwoNumbersContent.innerHTML = marked.parse(text) + inputBoxHTML + brokenSubmissionHTML + correctSubmissionHTML + wrongSubmissionHTML + testSubmissionHTML + submitHTML + resultsHTML;

// Get the Buttons
const brokenButton = document.getElementById("btn-add-two-numbers-broken");
const correctButton = document.getElementById("btn-add-two-numbers-correct");
const wrongButton = document.getElementById("btn-add-two-numbers-wrong");
const submitButton = document.getElementById("btn-submit");
const testSubmissionButton = document.getElementById("btn-test-submission");

// Load starter
fetch("questions/add-two-numbers/starter.py")
.then(res => res.text())
.then(text => {
document.getElementById("output").value = text;
})

brokenButton.addEventListener("click", async function() {
fetch("submissions/add-two-numbers/broken.py")
.then(res => res.text())
.then(text => {
document.getElementById("output").value = text;
});
});

correctButton.addEventListener("click", function() {
fetch("submissions/add-two-numbers/correct.py")
.then(res => res.text())
.then(text => {
document.getElementById("output").value = text;
});
});

wrongButton.addEventListener("click", function() {
fetch("submissions/add-two-numbers/wrong-answer.py")
.then(res => res.text())
.then(text => {
document.getElementById("output").value = text;
});
});

submitButton.addEventListener("click", async function() {
let pyodide = await pyodideReadyPromise;

let inputBoxData = document.getElementById("output").value;
pyodide.FS.writeFile("solution.py", inputBoxData, { encoding: "utf8"});

fetch("questions/add-two-numbers/tests.py")
.then(res => res.text())
.then(async testCode => {
const resultsDiv = document.getElementById("test-results");
try {
pyodide.runPython(`
import sys, io
sys.stdout = io.StringIO()
if 'solution' in sys.modules:
del sys.modules['solution']
`);
pyodide.runPython(testCode);
} catch(e) {
if (!e.message.includes("SystemExit")) {
resultsDiv.innerHTML = `<p>[ERROR, TRY AGAIN] ${e}</p>`
console.error(e);
}
}
const result = pyodide.runPython("sys.stdout.getvalue()");
const parsed = JSON.parse(result);

if (parsed.failed === 0) {
resultsDiv.innerHTML = `<p>Correct.</p>`;
} else {
resultsDiv.innerHTML = `<p>Incorrect, review submission and try again.</p>`;
}
});
});


testSubmissionButton.addEventListener("click", async function() {
let pyodide = await pyodideReadyPromise;
let inputBoxData = document.getElementById("output").value;
pyodide.FS.writeFile("solution.py", inputBoxData, { encoding: "utf8"});

fetch("questions/add-two-numbers/tests.py")
.then(res => res.text())
.then(testCode => {
const resultsDiv = document.getElementById("test-results");
try {
pyodide.runPython(`
import sys, io
sys.stdout = io.StringIO()
if 'solution' in sys.modules:
del sys.modules['solution']
`);
pyodide.runPython(testCode);
} catch(e) {
if (!e.message.includes("SystemExit")) {
resultsDiv.innerHTML = `<p>[ERROR, TRY AGAIN] ${e}</p>`
console.error(e);
}
}
const result = pyodide.runPython("sys.stdout.getvalue()");
const parsed = JSON.parse(result);
if (parsed.failed === 0) {
resultsDiv.innerHTML = `All ${parsed.passed} tests passed.`;
} else {
resultsDiv.innerHTML = `${parsed.failed} test(s) failed, ${parsed.passed} passed.`;
}
});
});
});
});

loadReverseStringButton.addEventListener("click",function() {
// Fetch .md file and display
clearPage();
fetch("questions/reverse-string/index.md")
.then(res => res.text())
.then(text => {

// Set HTML
const submitHTML = `<button id="btn-submit" type="button">Submit</button>`;
const revealSolutionHTML = `<button id="btn-reveal-solution" type="button">Reveal Solution</button>`;
const testSubmissionHTML = `<button id="btn-test-submission" type="button">Test Submission</button>`;
const brokenSubmissionHTML = `<button id="btn-reverse-string-broken" type="button">Broken Input</button>`;
const correctSubmissionHTML = `<button id="btn-reverse-string-correct" type="button">Correct Input</button>`;
const wrongSubmissionHTML = `<button id="btn-reverse-string-wrong" type="button">Wrong Input</button>`;
const inputBoxHTML = `<textarea id="output" style="width: 25%;" rows="6" enabled></textarea> <br>`;
const resultsHTML = `<div id="test-results"></div>`;
reverseStringContent.innerHTML = marked.parse(text) + inputBoxHTML + brokenSubmissionHTML + correctSubmissionHTML + wrongSubmissionHTML + testSubmissionHTML + submitHTML + resultsHTML;

// Get the Buttons
const brokenButton = document.getElementById("btn-reverse-string-broken");
const correctButton = document.getElementById("btn-reverse-string-correct");
const wrongButton = document.getElementById("btn-reverse-string-wrong");
const submitButton = document.getElementById("btn-submit");
const testSubmissionButton = document.getElementById("btn-test-submission");

fetch("questions/reverse-string/starter.py")
.then(res => res.text())
.then(text => {
document.getElementById("output").value = text;
})

brokenButton.addEventListener("click", function() {
fetch("submissions/reverse-string/broken.py")
.then(res => res.text())
.then(text => {
document.getElementById("output").value = text;
});
});

correctButton.addEventListener("click", function() {
fetch("submissions/reverse-string/correct.py")
.then(res => res.text())
.then(text => {
document.getElementById("output").value = text;
});
});

wrongButton.addEventListener("click", function() {
fetch("submissions/reverse-string/wrong-answer.py")
.then(res => res.text())
.then(text => {
document.getElementById("output").value = text;
});
});

submitButton.addEventListener("click", async function() {
let pyodide = await pyodideReadyPromise;

let inputBoxData = document.getElementById("output").value;
pyodide.FS.writeFile("solution.py", inputBoxData, { encoding: "utf8"});

fetch("questions/reverse-string/tests.py")
.then(res => res.text())
.then(async testCode => {
const resultsDiv = document.getElementById("test-results");
try {
pyodide.runPython(`
import sys, io
sys.stdout = io.StringIO()
if 'solution' in sys.modules:
del sys.modules['solution']
`);
pyodide.runPython(testCode);
} catch(e) {
if (!e.message.includes("SystemExit")) {
resultsDiv.innerHTML = `<p>[ERROR, TRY AGAIN] ${e}</p>`
console.error(e);
}
}
const result = pyodide.runPython("sys.stdout.getvalue()");
const parsed = JSON.parse(result);

if (parsed.failed === 0) {
resultsDiv.innerHTML = `<p>Correct.</p>`;
} else {
resultsDiv.innerHTML = `<p>Incorrect, review submission and try again.</p>`;
}
});
});
testSubmissionButton.addEventListener("click", async function() {
let pyodide = await pyodideReadyPromise;
let inputBoxData = document.getElementById("output").value;
pyodide.FS.writeFile("solution.py", inputBoxData, { encoding: "utf8"});

fetch("questions/reverse-string/tests.py")
.then(res => res.text())
.then(async testCode => {
const resultsDiv = document.getElementById("test-results");
try {
pyodide.runPython(`
import sys, io
sys.stdout = io.StringIO()
if 'solution' in sys.modules:
del sys.modules['solution']
`);
pyodide.runPython(testCode);
} catch(e) {
if (!e.message.includes("SystemExit")) {
resultsDiv.innerHTML = `<p>[ERROR, TRY AGAIN] ${e}</p>`
console.error(e);
}
}
const result = pyodide.runPython("sys.stdout.getvalue()");
const parsed = JSON.parse(result);
if (parsed.failed === 0) {
resultsDiv.innerHTML = `All ${parsed.passed} tests passed.`;
} else {
resultsDiv.innerHTML = `${parsed.failed} test(s) failed, ${parsed.passed} passed.`;
}
});
});
});
});

// When Clear Page Clicked => try clear page
clearPageButton.addEventListener("click",function() {
addTwoNumbersContent.innerHTML = "";
reverseStringContent.innerHTML = "";
});
32 changes: 32 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!-- INCLUDE DEPENDENCY INSTALLER -->
<!doctype html>
<html>
<head>
<!-- Import Pyodide, Pyodide Setup, and Optional Libraries-->
<script src="https://cdn.jsdelivr.net/pyodide/v0.29.4/full/pyodide.js"></script> <!-- Required -->
<script src="pyodide-setup.js"></script> <!-- Required for global use -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <!-- Not needed, I used this to format the index.md files. -->
</head>

<body>
<p>
Choose the Problem:
<!-- Functionality Buttons -->
<button id="btn-add-two-numbers" type="button">Add Two Numbers</button>
<button id="btn-reverse-string" type="button">Reverse String</button>
<button id="btn-clear-page" type="button">Clear Page</button>
</p>

<!-- Page Divs -->
<div id="add-two-numbers"></div>
<div id="reverse-string"></div>
<div id="clear-page"></div>

<!-- Import EventHandler.js into index.html -->
<script src="EventHandler.js"></script>
</body>
</html>




13 changes: 13 additions & 0 deletions pyodide-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
*
*
*
*
*/
async function main() {
console.time("Pyodide Loading Time");
let pyodide = await loadPyodide();
console.timeEnd("Pyodide Loading Time");
return pyodide;
};
let pyodideReadyPromise = main();
80 changes: 80 additions & 0 deletions report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Pyodide Findings
This report contains my findings while using Pyodide to display content onto WebAssembly. This report is not solidified and could be subject to change.

## Test & Submission Workflow
The test and submission workflow follows the current routine in summary:
1. Create the test file
2. Student submits code
3. Results are shown

The test file runs against the submission inside Pyodide by writing to the solution.py file (i.e., the answer file) and tests.py imports solution.py to run its tests. To receive whether the answer was correct or incorrect, we fetch the tests.py file. Since the user has the ability to run broken code, we surround the code execution in a try-catch state, where catch catches the expected broken code. Moving on, we parse the result from test.py to count the amount of test passes/fails to determine whether the answer they submitted was correct, or whether the test was successful depending on what button the user pressed.

## Startup Time
Cold Startup Time: ~3500ms on average.

Warm Subsequent Execution Time(s): ~800ms on average.

## Missing stdlib Modules

In version 0.29.4, only the Python stdlib is available after importing Pyodide by default.

The following libraries are included:
- math
- random
- collections
- dataclasses
- typing
- sys
- os
- io

The following libraries have limited functionality:
- hashlib
- decimal
- pydoc
- webbrowser
- zoneinfo
- urllib3
- requests

The following modules are unavailable:
- curses
- dbm
- ensurepip
- fcntl
- grp
- idlelib
- lib2to3
- msvcrt
- pwd
- resource
- syslog
- termios
- tkinter
- turtle.py
- turtledemo
- venv
- winreg
- winsound

The following modules are included, but do not work:

- multiprocessing
- threading
- sockets
- pty
- tty

Source: https://pyodide.org/en/stable/usage/wasm-constraints.html

## Multi-File Questions
N/A

## Timeout & Infinite Loops
Testing out a counting infinite while loop resulted in the website slowing down severely and essentially crashing. The only way to counteract this is through a Web Worker, where Pyodide runs on a seperate thread. In NodeJS you can use worker_threads which is similar to Web Workers, as well as vm.runInNewContext() with a timeout option. In sum, it is doable in both NodeJS and through a browser Web Worker, but NodeJS offers slightly more control.

## Output Capture
Output is captured by using the Python sys and io modules. The output is stored by sys.stdout = io.StringIO() when the test is executed, and then is retrieved and stored by sys.stdout.getvalue(), and then for formatting is parsed through JSON.

## Browser Deployment Setup
N/A