Commit 5b36aae9 authored by Jakob Moser's avatar Jakob Moser
Browse files

Merge branch '43-feat-add-practice-exercises' into 'master'

Feat: Add practice exercises

Closes #43

See merge request !13
parents cbaac325 bfcbd5ad
Loading
Loading
Loading
Loading
+52 −14
Original line number Diff line number Diff line
@@ -49,14 +49,6 @@ export class Exercise {
        this.#context._confirmExerciseCompletion()
    }

    /**
     * Returns true if the user should be able to click a button
     * to manually confirm they completed this exercise
     */
    get awaitsManualConfirmation() {
        return this.#context._confirmExerciseCompletion !== null
    }

    /**
     * Set the handler that is called every time the exercise executor
     * sets the description of the exercise.
@@ -140,6 +132,9 @@ class ExerciseExecutionContext {
     * By default, after running the provided command, "clear" is
     * executed to clear the terminal. If you do not want to clear the
     * terminal, set `keepVisible` to true.
     * 
     * If no command is provided (i.e. command === null), no command is executed.
     * This is helpful if you just want to clear the terminal at the start of an exercise.
     * @param {string} command
     * @param {boolean} keepVisible
     */
@@ -148,7 +143,9 @@ class ExerciseExecutionContext {

            function prepareAndResolve() {
                // Actually prepare and resolve the promise (we get the resolve() function via the closure)
                if(command !== null) {
                    runCommand(command)
                }
                if (!keepVisible) {
                    runCommand("clear")
                }
@@ -171,6 +168,13 @@ class ExerciseExecutionContext {
        })
    }

    /**
     * Clear the terminal. Shorthand for prepareWith(null, false)
     */
    async clear() {
        await this.prepareWith(null, false)
    }

    /**
     * Indicates that manual confirmation is needed. Used like this:
     * 
@@ -190,9 +194,9 @@ class ExerciseExecutionContext {
    }

    /**
     * Verify the completion of the exercise by executing a command and checking
     * if the output is as expected. If yes, the exercise is treated as completed,
     * if no, the exercise is treated as failed.
     * Verify the completion of the exercise by optionally executing a command and checking
     * if the output is as expected. If yes, the exercise is treated as solved, if no,
     * the user has to try again.
     * 
     * Used like this:
     * 
@@ -200,6 +204,9 @@ class ExerciseExecutionContext {
     * this.verify("cat botschaft").hasOutput("Der Junge sieht den Mann mit dem Fernglas.")
     * ```
     * 
     * verify() can also be called without a command, in which case no command is executed – instead
     * the last command entered by the user (if any) is used.
     * 
     * When the parameter passed to `hasOutput` is a string, the verification succeeds iff
     * this string is the first and only line of output of the command.
     * When the parameter passed to `hasOutput` is a list, the verification succeeds iff
@@ -209,7 +216,9 @@ class ExerciseExecutionContext {
     * @returns An object that has the method "hasOutput"
     */
    verify(command) {
        if(command) {
            runCommand(command)
        }
        const verifyHandler = this._verifyHandler

        return {
@@ -230,7 +239,7 @@ class ExerciseExecutionContext {
                    const terminalContents = getTerminalContents()
                    const latestEntry = terminalContents[terminalContents.length - 1]

                    if (latestEntry.input !== command) {
                    if (command && latestEntry.input !== command) {
                        throw new Error("Could not find expected input in terminal")
                    }

@@ -244,6 +253,35 @@ class ExerciseExecutionContext {
                    // !! converts any value to a boolean (not a special operator, just two ! (boolean not) operators in series)
                    const result = !!predicate(latestEntry.output)

                    verifyHandler(result)
                }, 200)
            },

            /**
             * Verify the command matches a certain condition.
             * 
             * - If param is a string, check that the command is exactly equal to param
             * - If param is a function, the function is called with the actual command as parameter, and we check it returns
             *   a truthy value.
             * 
             * @param {Function|String} param String to exactly match, or predicate function
             */
            commandWas(param) {
                // Wait for 200ms to ensure that the VM has processed the input and written some
                // output to the terminal
                setTimeout(() => {
                    const terminalContents = getTerminalContents()
                    const latestEntry = terminalContents[terminalContents.length - 1]


                    // Poor man's switch-case: depending on the "typeof param", a different verification predicate is chosen
                    const predicate = {
                        "function": param,
                        "string": (actualInput) => actualInput === param,
                    }[typeof param]

                    const result = !!predicate(latestEntry.input)

                    verifyHandler(result)
                }, 200)
            }
+7 −15
Original line number Diff line number Diff line
@@ -36,27 +36,21 @@ export function displayAsNonCurrent(cardEl) {
}

/**
 * Create an (optional) confirm and a reset button that can be appended to the actions section of
 * Create a confirm and a reset button that can be appended to the actions section of
 * an exercise card.
 * @param {Function} manuallyConfirm Function to call to manually confirm this exercise. If falsy, no confirm button is created.
 * @param {Function} manuallyConfirm Function to call to manually confirm this exercise
 * @returns List of buttons
 */
function createExerciseCardActionButtons(manuallyConfirm) {
    const ret = []

    if(manuallyConfirm) {
    const confirmButton = createElementWithClass("button", "primary")
    confirmButton.textContent = "Ich denke, ich bin fertig."
    confirmButton.onclick = () => manuallyConfirm()
        ret.push(confirmButton)
    }

    const resetButton = document.createElement("button")
    resetButton.textContent = "Aufgabe neu beginnen"
    resetButton.onclick = () => location.reload()
    ret.push(resetButton)

    return ret
    return [confirmButton, resetButton]
}

/**
@@ -77,9 +71,7 @@ export function createExerciseCard(exercise, onTitleClick) {
    const descriptionEl = createElementWithClass("p", "description")
    const actionsEl = createElementWithClass("div", "actions")
    actionsEl.append(...createExerciseCardActionButtons(
        exercise.awaitsManualConfirmation ?
        exercise.manuallyConfirm.bind(exercise) :
        null
        exercise.manuallyConfirm.bind(exercise)
    ))
    cardEl.querySelector(".content").append(descriptionEl, actionsEl)

+150 −2
Original line number Diff line number Diff line
@@ -5,13 +5,161 @@ export const practice = test("Linux-Übungsmodus", function() {
    this.welcome(`
        <h1>Willkommen zum Linux-Übungsmodus</h1>
        <p>Hier kannst du vollkommen frei mit dem Linux-Terminal auf der linken Seite herumexperimentieren. Das Terminal läuft isoliert in diesem Browsertab, <strong>du kannst also absolut nichts kaputt machen</strong> (weder auf deinem Rechner, noch bei uns) – im schlimmsten Fall lädst du die Seite neu und alles ist vergessen ;)</p>
        <p>Aufgaben gibt es im Übungsmodus nicht (du kannst den leeren Abschnitt „Aufgaben also ignorieren).</p>
        <p>Es gibt ein paar Aufgaben im Übungsmodus, diese sind allerdings komplett freiwillig. Falls du sie bearbeitest, solltest du sie allerdings in der angegebenen Reihenfolge lösen, da die Aufgaben aufeinander aufbauen.</p>
        <p>Wir verwenden dieses Semester zum ersten Mal YALIKEJAZZ, also zögere nicht, dich <strong>bei Fragen und Problemen jederzeit an uns zu wenden</strong> (<code>technik@cl.uni-heidelberg.de</code>) – wir schauen uns das an und finden gemeinsam eine Lösung.</p>
    `)

    this.disableHandIn()

    // TODO Add practice exercises
    this.exercise("Verzeichnisinhalt anzeigen", async function () {
        this.describe(`Lass dir anzeigen, ob und wenn ja welche Dateien sich in diesem Verzeichnis befinden.`)
        
        await this.prepareWith("cd /root; if [[ -d tux ]]; then rm -r tux; fi")

        await this.manualConfirmation()

        this.verify().commandWas((cmd)=>cmd.match(/ls.*/))
    })

    this.exercise("Auch versteckte Dateien anzeigen", async function () {
        this.describe(`Lass dir anzeigen, welche Dateien sich in diesem Verzeichnis befinden (inklusive versteckter Dateien).`)

        await this.prepareWith("cd /root; if [[ -d tux ]]; then rm -r tux; fi")

        await this.manualConfirmation()

        this.verify().hasOutput((outputLines) => {
            for(const line of outputLines) {
                if(line.match(/.*\.ash_history.*/)) {
                    return true
                }
            }
            
            return false
        })
    })

    this.exercise("Verzeichnis anlegen", async function () {
        this.describe(`Erstelle einen Ordner (= ein Verzeichnis) mit dem Namen "tux" (ohne Anführungszeichen).`)

        await this.prepareWith("cd /root; if [[ -d tux ]]; then rm -r tux; fi")

        await this.manualConfirmation()

        this.verify("if [[ -d tux ]]; then echo yes; else echo no; fi").hasOutput("yes")
    })

    this.exercise("Aktuelles Verzeichnis anzeigen", async function () {
        this.describe("Lass dir anzeigen, in welchem Verzeichnis du gerade arbeitest.")

        await this.prepareWith("cd /root")

        await this.manualConfirmation()

        this.verify().hasOutput("/root")
    })

    this.exercise("Verzeichnis wechseln", async function() {
        this.describe(`Wechsele in das von dir erstelle Verzeichnis "tux".`)

        await this.prepareWith("cd /root; if [[ ! -d tux ]]; then mkdir tux; fi")

        await this.manualConfirmation()

        this.verify("pwd").hasOutput("/root/tux")
    })

    this.exercise("Datei anlegen", async function () {
        this.describe(`Erstelle eine Textdatei mit dem Namen "satz" und dem Inhalt "Colorless green ideas sleep furiously." 
                      (jeweils ohne Anführungszeichen).`)

        await this.prepareWith("if [[ ! -d /root/tux ]]; then mkdir /root/tux; fi; cd /root/tux; if [[ -f satz ]]; then rm satz; fi")

        await this.manualConfirmation()

        this.verify("cat satz").hasOutput("Colorless green ideas sleep furiously.")
    })

    this.exercise("Datei umbenennen", async function () {
        this.describe(`Ändere den Namen der Datei "satz" in "saetze".`)

        await this.prepareWith("if [[ ! -d /root/tux ]]; then mkdir /root/tux; fi; cd /root/tux; if [[ -f saetze ]]; then rm saetze; fi; if [[ ! -f satz ]]; then echo Colorless green ideas sleep furiously. > satz; fi")

        await this.manualConfirmation()

        this.verify("if [[ -f saetze && ! -f satz ]]; then echo yes; else echo no; fi").hasOutput("yes")
    })

    this.exercise("Datei bearbeiten", async function () {
        this.describe(`Füge die Zeile "The cat is on the mat." ans Ende der Datei "saetze" an.`)

        await this.prepareWith("if [[ ! -d /root/tux ]]; then mkdir /root/tux; fi; cd /root/tux; echo Colorless green ideas sleep furiously. > saetze")

        await this.manualConfirmation()

        this.verify("cat saetze").hasOutput(["Colorless green ideas sleep furiously.", "The cat is on the mat."])
    })

    this.exercise("Inhalt anzeigen", async function () {
        this.describe(`Zeige den Inhalt der Datei "saetze" auf der Konsole an.`)

        await this.prepareWith("if [[ ! -d /root/tux ]]; then mkdir /root/tux; fi; cd /root/tux; echo Colorless green ideas sleep furiously. > saetze; echo The cat is on the mat. >> saetze")

        await this.manualConfirmation()

        this.verify().hasOutput(["Colorless green ideas sleep furiously.", "The cat is on the mat."])
    })

    this.exercise("Berechtigungen ändern", async function () {
        this.describe(`Ändere die Dateirechte von "saetze" so, dass der Besitzer die Datei nur lesen kann, die besitzende Gruppe die Datei lesen und schreiben kann und alle anderen gar nichts dürfen.`)

        await this.prepareWith("if [[ ! -d /root/tux ]]; then mkdir /root/tux; fi; cd /root/tux; echo Colorless green ideas sleep furiously. > saetze; echo The cat is on the mat. >> saetze; chmod 644 saetze")

        await this.manualConfirmation()

        this.verify("ls -ld saetze").hasOutput((outputLines) => {
            return outputLines.length === 1 && outputLines[0].substring(0, 10).match(/-r--rw----/)
        })
    })

    this.exercise("Berechtigungen anzeigen", async function () {
        this.describe(`Lass dir die aktuellen Berechtigungen für die Datei "saetze" anzeigen.`)

        await this.prepareWith("if [[ ! -d /root/tux ]]; then mkdir /root/tux; fi; cd /root/tux; if [[ ! -f saetze ]]; then echo Colorless green ideas sleep furiously. > saetze; echo The cat is on the mat. >> saetze; fi; chmod 460 saetze")

        await this.manualConfirmation()

        this.verify().hasOutput((outputLines) => {
            for(const line of outputLines) {
                if(line.match(/.*-r--rw----.*/)) {
                    return true
                }
            }
            
            return false
        })
    })

    this.exercise("Datei löschen", async function () {
        this.describe(`Lösche die Datei "saetze".`)

        await this.prepareWith("if [[ ! -d /root/tux ]]; then mkdir /root/tux; fi; cd /root/tux; if [[ ! -f saetze ]]; then echo Colorless green ideas sleep furiously. > saetze; echo The cat is on the mat. >> saetze; fi; chmod 460 saetze")

        await this.manualConfirmation()

        this.verify("if [[ ! -e saetze ]]; then echo yes; else echo no; fi").hasOutput("yes")
    })

    this.exercise("Freies Spiel", async function () {
        // TODO Replace with something better
        this.describe(`Wir haben leider keine weiteren Übungsaufgaben mehr vorbereitet, aber fühle dich frei, noch weiter herumzuspielen (zum Beispiel mit Dateirechten).`)

        await this.clear()

        await this.manualConfirmation()

        alert("Prima :) Viel Erfolg dann beim Pooltest, sobald du ihn schreibst.")
    })
})

export const pooltest = test("Pooltest Sommersemester 2022", async function () {