Dynamischer Python Checker (JACK3)

Aus JACK Wiki
Zur Navigation springen Zur Suche springen

Der Aufgabentyp Python lässt sich automatisiert bewerten, indem Testfälle ausgeführt werden. Diese können z. B. einen erwarteter Wert mit dem tatsächlichen Wert vergleichen. Der Python-Checker von JACK3 kann nicht nur Punktzahlen und Feedback zurückgeben, sondern auch Traces aufzeichnen (welche Zeilen ausgeführt wurden und wie die Variablenbelegung beim Testfall war). Es lassen sich beliebig komplexe Testfälle und auch zufallsbasierte Tests schreiben sowie passgenaues Feedback, programmiert in Python, erzeugen.

Hinweis: In diesem Artikel werden grundlegende Python-Kenntnisse vorausgesetzt.

Der Checker arbeitet auf Python 3.11. Aktuell ist es nicht möglich, eigene Module zum Check hinzuzufügen oder Pakete aus dem Python Package Index (PyPI) (über pip) zu laden. Das Paket pandas ist vorinstalliert und kann durch den Testtreiber geladen werden, ansonsten sind ausschließlich Pakete der Python-Standardbibliothek nutzbar.

Unterstützte Aufgabenstellungen

Der dynamische Python-Checker ist darauf ausgelegt, Funktionsaufrufe aufzuzeichnen und ihre Rückgabewerte zu prüfen. Dies funktioniert am besten mit folgenden Aufgabenstellungen:

  • Schreiben Sie eine parameterlose Funktion, die ... zurückgibt.
  • Schreiben Sie eine Funktion, die ... erwartet und ... zurückgibt.
  • Schreiben Sie eine Funktion mit x Parametern, die mit dem ersten Parameter ... macht. (Um die Funktion zu prüfen, muss der Typ des ersten Parameters mutierbar sein, z. B. eine Liste.)

Beispiel: Definieren Sie eine Funktion add. Die Funktion soll zwei Parameter entgegennehmen und das Ergebnis der Addition zurückgeben. Die erwartete Musterlösung für diesen Fall wäre:

def add(a, b):
  return a + b

Testfälle (s.u.) können diese Funktion dann mit verschiedenen Werten aufrufen und das Ergebnis prüfen.

Aufbau eines Testtreibers

Grundsätzlich ist ein Testtreiber ein einzelnes Modul mit einer oder mehreren parameterlosen Funktionen (Testfälle), die nacheinander ausgeführt werden. In der Regel importiert der Testtreiber dabei das Modul mit dem eingereichten (zu testenden) Code. Dessen Name wird im UI als "Modul-Pfad für studentischen Code" festgelegt.

Damit die Traces der Testfälle korrekt aufgezeichnet werden, wird jeder Testfall mit dem Decorator decorator aus dem vorgegebenen Communicator-Modul gekennzeichnet. Der folgende Code zeigt ein Beispiel mit zwei Testfällen, jeweils ohne Inhalt:

import Communicator
import Trace
import studentcode as s

@Communicator.decorator
def testcase1():
  # TODO so something with s ...
  pass

@Communicator.decorator
def testcase2():
  # TODO so something with s ...
  pass

if __name__ == '__main__':
    print('Running test case 1...')
    testcase1()
    print('Running test case 2 ...')
    testcase2()

API zum Zurückmelden von Ergebnissen

Innerhalb eines Testfalls werden Ergebnisse über das Modul Trace zurückgeliefert. Dieses Modul sammelt alle Punktzahlen und Feedback auf und berichtet diese am Ende der Ausführung. Es bietet folgende Funktion:

def printResult(result: int, feedback: str = None):
  """
  Reports a result.

  :param result: Points which are reported. Must be between 0 and 100 (both inclusive).
  :param feedback: Optional Feedback presented to the user.
  """
  pass

Wichtig: In jedem Testfall muss printResult genau ein Mal tatsächlich aufgerufen werden!

Schreiben von Testfällen

Der Checker nutzt für das Tracing eine spezielle Kontrollflussanalyse, basierend auf Pythons assert-Schlüsselwort und Exceptions. Mit assert lassen sich Ausdrücke testen, die resultierende Exception lässt sich anschließend fangen und in Feedback umwandeln.

Ein möglicher Testfall 1 für das obige Beispiel wäre dementsprechend:

@Communicator.decorator
def testcase1():
  try:
    # Berechnen des Ergebnisses mit dem "studentcode":
    result = s.add(1, 0)
    # zu prüfender Ausdruck:
    assert result == 1

    # Bei Erfolg: +25 Punkte
    Trace.printResult(25)
  except Exception:
    # Bei Misserfolg: +0 Punkte mit Feedback-Nachricht
    Trace.printResult(0, f"Erwartete Summe: 1, tatsächlich: {result}.")

Das Tracing reagiert auf alle Arten von Exceptions und zeigt im Falle eines AssertionErrors (entweder händisch geworfen oder über assert) eine generische Fehlermeldung an, die mitteilt, dass der Testfall nicht bestanden wurde, sofern kein benutzerdefiniertes Feedback (zweiter Parameter von printResult) vorgegeben wurde. Zusätzlich wird die Variablenbelegung angezeigt und die ausgeführten Zeilen.

Pro Testtreiber werden die Punkte aller Testfälle aufsummiert. Bei einer gleichen Gewichtung von vier Testfällen wird jeder Testfall also 25 Punkte im Erfolg zurückmelden. Die summierten Punkte müssen zwischen 0 und 100 (jeweils inklusiv) liegen.

Ressourcen

Um das Entwickeln von Testfällen zu vereinfachen, haben wir einen Dummy gebaut, der die Funktion des Checkers simuliert. Den Dummy können Sie hier herunterladen. Folgende Dateien sind enthalten:

  • Trace.py und Communicator.py simulieren die Funktion des Checkers.
  • testdriver.py enthält vier Testfälle aus dem Beispiel oben und kann beliebig angepasst werden.
  • studentcode.py enthält eine studentische Einreichung mit einem "Fehler", bei dem einer der Testfälle nicht bestanden wird.
  • run.py ist ein Skript, das den Testtreiber ausführt und ausgibt, wie viele Punkte die Lösung ergeben würde.

Der Dummy kann mit dem Befehl python run.py ausgeführt werden (auf Linux-Systemen kann der Befehl auch python3 lauten, auf Windows auch py).

Hinweis: Der Dummy arbeitet nicht exakt wie der Checker. Es gibt keine Garantie, dass Testtreiber, die mit dem Dummy funktionieren, auch genau so in JACK funktionieren. Testen Sie die Aufgabe deshalb immer auch in JACK!