Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use papyros to debug testcases #5336

Merged
merged 20 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
189 changes: 96 additions & 93 deletions app/assets/javascripts/coding_scratchpad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
const PANEL_PARENT_ID = "scratchpad-panel-wrapper";
const CODE_OUTPUT_PARENT_ID = "scratchpad-output-wrapper";
const CODE_INPUT_PARENT_ID = "scratchpad-input-wrapper";
const OFFCANVAS_ID = "scratchpad-offcanvas";
export const OFFCANVAS_ID = "scratchpad-offcanvas";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to add export here and not use the export at the bottom as we do in our other files?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a personal preference for this style. It is sometimes useful to know whether something is exported when looking at the definition. I have never had a use case where I needed a quick overview of all exported definitions from a file.

I have almost always used this style over the past years, except when explicitly modifying older files.
This has caused for a divergence in styles across files. I am open to fixing this in another pr.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked the idea of having a clear overview in the "old" style, but have no strong opinion about this. I just noticed a mix of two styles. According to "The Internet" multiple exports from the same file is a code smell. We can discuss this in person over lunch or coffee ;)

const SHOW_OFFCANVAS_BUTTON_ID = "scratchpad-offcanvas-show-btn";
const CODE_COPY_BUTTON_ID = "scratchpad-code-copy-btn";
const CLOSE_BUTTON_ID = "scratchpad-offcanvas-close-btn";
Expand All @@ -21,103 +21,80 @@
const TRACE_TAB_ID = "scratchpad-trace-tab";
const DESCRIPTION_TAB_ID = "scratchpad-description-tab";

function initCodingScratchpad(programmingLanguage: ProgrammingLanguage): void {
if (Papyros.supportsProgrammingLanguage(programmingLanguage)) {
let papyros: Papyros | undefined = undefined;
let editor: EditorView | undefined = undefined;
const closeButton = document.getElementById(CLOSE_BUTTON_ID);
// To prevent horizontal scrollbar issues, we delay rendering the button
// until after the page is loaded
const showButton = document.getElementById(SHOW_OFFCANVAS_BUTTON_ID);
showButton.classList.add("offcanvas-show-btn");
showButton.classList.remove("hidden");
showButton.addEventListener("click", async function () {
if (!papyros) { // Only create Papyros once per session, but only when required
// Papyros registers a service worker on a specific path
// We used to do this on a different path
// So we need to unregister old serviceworkers manually as these won't get overwritten
navigator.serviceWorker.getRegistrations().then(function (registrations) {
for (const registration of registrations) {
if (registration.scope !== document.location.origin + "/") {
registration.unregister();
}
}
});

papyros = new Papyros(
{
programmingLanguage: Papyros.toProgrammingLanguage(programmingLanguage),
standAlone: false,
locale: i18n.locale(),
inputMode: InputMode.Interactive,
channelOptions: {
serviceWorkerName: "inputServiceWorker.js"
}
});

// Render once new button is added
papyros.render({
codeEditorOptions: {
parentElementId: CODE_EDITOR_PARENT_ID
},
statusPanelOptions: {
parentElementId: PANEL_PARENT_ID
},
outputOptions: {
parentElementId: CODE_OUTPUT_PARENT_ID
},
inputOptions: {
parentElementId: CODE_INPUT_PARENT_ID,
inputStyling: {
// Allows 4 lines of input
maxHeight: "10vh"
}
},
traceOptions: {
parentElementId: CODE_TRACE_PARENT_ID
},
darkMode: themeState.theme === "dark"
});
editor ||= window.dodona.editor;
if (editor) {
// Shortcut to copy code to editor
papyros.addButton(
{
id: CODE_COPY_BUTTON_ID,
buttonText: i18n.t("js.coding_scratchpad.copy_to_submit"),
classNames: "btn-secondary",
icon: "<i class=\"mdi mdi-clipboard-arrow-left-outline\"></i>"
},
() => {
setCode(editor, papyros.getCode());
closeButton.click();
// Open submit panel if possible
document.getElementById(SUBMIT_TAB_ID)?.click();
}
);
let papyros: Papyros | undefined = undefined;
let editor: EditorView | undefined = undefined;
jorg-vr marked this conversation as resolved.
Show resolved Hide resolved
export async function initPapyros(programmingLanguage: ProgrammingLanguage): Promise<Papyros> {
if (!papyros) { // Only create Papyros once per session, but only when required
// Papyros registers a service worker on a specific path
// We used to do this on a different path
// So we need to unregister old serviceworkers manually as these won't get overwritten
navigator.serviceWorker.getRegistrations().then(function (registrations) {
for (const registration of registrations) {
if (registration.scope !== document.location.origin + "/") {
registration.unregister();

Check warning on line 34 in app/assets/javascripts/coding_scratchpad.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/coding_scratchpad.ts#L34

Added line #L34 was not covered by tests
jorg-vr marked this conversation as resolved.
Show resolved Hide resolved
}
await papyros.launch();

papyros.codeRunner.editor.reconfigure([CodeEditor.STYLE, syntaxHighlighting(rougeStyle, {
fallback: true
})]);
}
});
// Ask user to choose after offcanvas is shown
document.getElementById(OFFCANVAS_ID).addEventListener("shown.bs.offcanvas", () => {
editor ||= window.dodona.editor;
if (editor) { // Start with code from the editor, if there is any
const editorCode = editor.state.doc.toString();
const currentCode = papyros.getCode();
if (!currentCode || // Papyros empty
// Neither code areas are empty, but they differ
(editorCode && currentCode !== editorCode &&
// and user chooses to overwrite current code with editor value
confirm(i18n.t("js.coding_scratchpad.overwrite_code")))) {
papyros.setCode(editorCode);

papyros = new Papyros(
{
programmingLanguage: Papyros.toProgrammingLanguage(programmingLanguage),
standAlone: false,
locale: i18n.locale(),
inputMode: InputMode.Interactive,
channelOptions: {
root: "/",
serviceWorkerName: "inputServiceWorker.js"
}
}
});

// Render once new button is added
papyros.render({
codeEditorOptions: {
parentElementId: CODE_EDITOR_PARENT_ID
},
statusPanelOptions: {
parentElementId: PANEL_PARENT_ID
},
outputOptions: {
parentElementId: CODE_OUTPUT_PARENT_ID
},
inputOptions: {
parentElementId: CODE_INPUT_PARENT_ID,
inputStyling: {
// Allows 4 lines of input
maxHeight: "10vh"
}
},
traceOptions: {
parentElementId: CODE_TRACE_PARENT_ID
},
darkMode: themeState.theme === "dark"
});
editor ||= window.dodona.editor;
if (editor) {
// Shortcut to copy code to editor
papyros.addButton(
{
id: CODE_COPY_BUTTON_ID,
buttonText: i18n.t("js.coding_scratchpad.copy_to_submit"),
classNames: "btn-secondary",
icon: "<i class=\"mdi mdi-clipboard-arrow-left-outline\"></i>"
},
() => {
setCode(editor, papyros.getCode());
const closeButton = document.getElementById(CLOSE_BUTTON_ID);
closeButton.click();

Check warning on line 87 in app/assets/javascripts/coding_scratchpad.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/coding_scratchpad.ts#L84-L87

Added lines #L84 - L87 were not covered by tests
// Open submit panel if possible
document.getElementById(SUBMIT_TAB_ID)?.click();
}
);
}
await papyros.launch();

papyros.codeRunner.editor.reconfigure([CodeEditor.STYLE, syntaxHighlighting(rougeStyle, {
fallback: true
})]);

// Hide Trace tab when a new run is started
BackendManager.subscribe(BackendEventType.Start, () => {
Expand All @@ -142,6 +119,32 @@
}
});
}
return papyros;
}

function initCodingScratchpad(programmingLanguage: ProgrammingLanguage): void {
if (Papyros.supportsProgrammingLanguage(programmingLanguage)) {
// To prevent horizontal scrollbar issues, we delay rendering the button
// until after the page is loaded
const showButton = document.getElementById(SHOW_OFFCANVAS_BUTTON_ID);
showButton.classList.add("offcanvas-show-btn");
showButton.classList.remove("hidden");
showButton.addEventListener("click", () => {
initPapyros(programmingLanguage);
editor ||= window.dodona.editor;
if (editor) { // Start with code from the editor, if there is any
const editorCode = editor.state.doc.toString();
const currentCode = papyros.getCode();
jorg-vr marked this conversation as resolved.
Show resolved Hide resolved
if (!currentCode || // Papyros empty
// Neither code areas are empty, but they differ
(editorCode && currentCode !== editorCode &&

Check warning on line 140 in app/assets/javascripts/coding_scratchpad.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/coding_scratchpad.ts#L140

Added line #L140 was not covered by tests
// and user chooses to overwrite current code with editor value
confirm(i18n.t("js.coding_scratchpad.overwrite_code")))) {

Check warning on line 142 in app/assets/javascripts/coding_scratchpad.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/coding_scratchpad.ts#L142

Added line #L142 was not covered by tests
papyros.setCode(editorCode);
}
}
});
}
}

export { initCodingScratchpad };
2 changes: 0 additions & 2 deletions app/assets/javascripts/i18n/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,6 @@
"timeseries_desc": "This graph shows on which moments the students submitted the most solutions.",
"timeseries_title": "Submissions over time",
"total": "total",
"tutor-failed": "Something went wrong while loading the online python tutor",
"unfavorite-course-do": "Unfavorite",
"unfavorite-course-failed": "Unfavoriting course failed",
"unfavorite-course-succeeded": "Unfavorited course",
Expand Down Expand Up @@ -953,7 +952,6 @@
"timeseries_desc": "Deze grafiek toont op welke momenten het meest aan een bepaalde oefening werd gewerkt.",
"timeseries_title": "Ingediende oplossingen over tijd",
"total": "totaal aantal",
"tutor-failed": "Er ging iets fout bij het laden van de online python tutor",
"unfavorite-course-do": "Verwijder uit favorieten",
"unfavorite-course-failed": "Cursus uit favorieten verwijderen mislukt",
"unfavorite-course-succeeded": "Cursus verwijderd uit favorieten",
Expand Down
10 changes: 1 addition & 9 deletions app/assets/javascripts/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,7 @@
import { render, TemplateResult } from "lit";


function showInfoModal(title: TemplateResult | string, content: TemplateResult | string, options?: {allowFullscreen: boolean}): void {
const button = document.querySelector("#info-modal .modal-header #fullscreen-button") as HTMLElement;

if (options?.allowFullscreen) {
button.style.display = "inline";
} else {
button.style.display = "none";
}

function showInfoModal(title: TemplateResult | string, content: TemplateResult | string): void {

Check warning on line 5 in app/assets/javascripts/modal.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/modal.ts#L5

Added line #L5 was not covered by tests
render(title, document.querySelector("#info-modal .modal-title") as HTMLElement);
render(content, document.querySelector("#info-modal .modal-body") as HTMLElement);

Expand Down
118 changes: 14 additions & 104 deletions app/assets/javascripts/tutor.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
import fscreen from "fscreen";
import { showInfoModal } from "modal";
import { html } from "lit";
import { TraceGenerator } from "@dodona/pyodide-trace-library";
import { initPapyros, OFFCANVAS_ID } from "coding_scratchpad";
import { InputMode, ProgrammingLanguage } from "@dodona/papyros";

Check warning on line 2 in app/assets/javascripts/tutor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/tutor.ts#L1-L2

Added lines #L1 - L2 were not covered by tests
import { BatchInputHandler } from "@dodona/papyros/dist/input/BatchInputHandler";
import { Offcanvas } from "bootstrap";

Check warning on line 4 in app/assets/javascripts/tutor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/tutor.ts#L4

Added line #L4 was not covered by tests
const DEBUG_BUTTON_ID = "__papyros-debug-code-btn";

export function initTutor(submissionCode: string): void {
let traceGenerator: TraceGenerator;
let traceGeneratorReady: Promise<void>;
function init(): void {
initTutorLinks();
if (document.querySelectorAll(".tutormodal").length == 1) {
initFullScreen();
} else {
const tutorModal = Array.from(document.querySelectorAll(".tutormodal")).pop();
if (tutorModal) {
tutorModal.remove();
}
}
}

function initTutorLinks(): void {
Expand All @@ -32,7 +23,7 @@
const exerciseId = (document.querySelector(".feedback-table") as HTMLElement).dataset.exercise_id;
const tutorLink = e.currentTarget as HTMLLinkElement;
const group = tutorLink.closest(".group");
const stdin = tutorLink.dataset.stdin.slice(0, -1);
const stdin = tutorLink.dataset.stdin;

Check warning on line 26 in app/assets/javascripts/tutor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/tutor.ts#L26

Added line #L26 was not covered by tests
const statements = tutorLink.dataset.statements;
const files = { inline: {}, href: {} };

Expand All @@ -55,105 +46,24 @@
}));
}

function initFullScreen(): void {
fscreen.addEventListener("fullscreenchange", resizeFullScreen);

document.querySelector("#tutor #fullscreen-button").addEventListener("click", () => {
const tutor = document.querySelector("#tutor");
if (fscreen.fullscreenElement) {
document.querySelector("#tutor .modal-dialog").classList.remove("modal-fullscreen");
fscreen.exitFullscreen();
} else {
document.querySelector("#tutor .modal-dialog").classList.add("modal-fullscreen");
fscreen.requestFullscreen(tutor);
}
});
}

function resizeFullScreen(): void {
const tutor = document.querySelector("#tutor");
const tutorviz = document.querySelector("#tutorviz") as HTMLElement;
if (!fscreen.fullscreenElement) {
tutor.classList.remove("fullscreen");
tutorviz.style.height = tutorviz.dataset.standardheight;
} else {
tutorviz.dataset.standardheight = `${tutorviz.clientHeight}px`;
tutor.classList.add("fullscreen");
tutorviz.style.height = "100%";
}
}

async function loadTutor(exerciseId: string, studentCode: string, statements: string, stdin: string, inlineFiles: Record<string, string>, hrefFiles: Record<string, string>): Promise<void> {
if (!traceGenerator) {
// only setup the traceGenerator upon first use, as it is a heavy operation
traceGenerator = new TraceGenerator();
traceGeneratorReady = traceGenerator.setup();
}
const papyros = await initPapyros(ProgrammingLanguage.Python);

Check warning on line 50 in app/assets/javascripts/tutor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/tutor.ts#L50

Added line #L50 was not covered by tests

const lines = studentCode.split("\n");
// find and remove main
let i = 0;
let remove = false;
const sourceArray = [];
while (i < lines.length) {
if (remove && !lines[i].match(/^\s+.*/g)) {
remove = false;
}
if (lines[i].match(/if\s+__name__\s*==\s*(['"])__main__\s*\1:\s*/g)) {
remove = true;
}
if (!remove) {
sourceArray.push(lines[i]);
}
i += 1;
}
sourceArray.push(statements);
const sourceCode = sourceArray.join("\n");
papyros.setCode(studentCode);
papyros.codeRunner.inputManager.setInputMode(InputMode.Batch);
(papyros.codeRunner.inputManager.inputHandler as BatchInputHandler).batchEditor.setText(stdin);
papyros.codeRunner.editor.testCode = statements;

Check warning on line 55 in app/assets/javascripts/tutor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/tutor.ts#L52-L55

Added lines #L52 - L55 were not covered by tests

// make full url from path
const hrefFilesFull = Object.keys(hrefFiles).reduce((result, key) => {
result[key] = `${location.protocol}//${location.hostname}${location.port ? `:${location.port}` : ""}/nl/exercises/${exerciseId}/${hrefFiles[key]}`;
return result;
}, {});

showInfoModal("Python Tutor", html`<div id="tutorcontent"></div>`, { allowFullscreen: true });
const modalShown = new Promise<void>(resolve => {
const modal = document.querySelector("#tutor #info-modal");
modal.addEventListener("shown.bs.modal", () => resolve());
});

const content = document.querySelector("#tutorcontent");
if (content) {
content.innerHTML = `<div class="dodona-progress dodona-progress-indeterminate" style="visibility: visible">
<div class="progressbar bar bar1" style="width: 0%;"></div>
<div class="bufferbar bar bar2" style="width: 100%;"></div>
<div class="auxbar bar bar3" style="width: 0%;"></div>
</div>`;
}

await traceGeneratorReady;
const result = await traceGenerator.generateTrace(sourceCode, stdin, inlineFiles, hrefFilesFull);
await modalShown;
createTutor(result);
}

function createTutor(codeTrace: string): void {
const modal = document.querySelector("#tutor #info-modal");

const content = document.querySelector("#tutorcontent");
if (content) {
content.innerHTML = `<iframe id="tutorviz" width="100%" frameBorder="0" src="${window.dodona.sandboxUrl}/tutorviz/tutorviz.html"></iframe>`;
document.querySelector("#tutorviz").addEventListener("load", () => {
window.iFrameResize({ checkOrigin: false, onInit: frame => frame.iFrameResizer.sendMessage(JSON.parse(codeTrace)), scrolling: "omit" }, "#tutorviz");
});
}
new Offcanvas(document.getElementById(OFFCANVAS_ID)).show();
await papyros.codeRunner.provideFiles(inlineFiles, hrefFilesFull);

Check warning on line 64 in app/assets/javascripts/tutor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/tutor.ts#L63-L64

Added lines #L63 - L64 were not covered by tests

modal.addEventListener("hidden.bs.modal", () => {
if (fscreen.fullscreenElement) {
document.querySelector("#tutor .modal-dialog").classList.remove("modal-fullscreen");
fscreen.exitFullscreen();
}
});
document.getElementById(DEBUG_BUTTON_ID).click();

Check warning on line 66 in app/assets/javascripts/tutor.ts

View check run for this annotation

Codecov / codecov/patch

app/assets/javascripts/tutor.ts#L66

Added line #L66 was not covered by tests
jorg-vr marked this conversation as resolved.
Show resolved Hide resolved
}

init();
Expand Down
Loading
Loading