Dynamischer Python Checker (JACK3): Unterschied zwischen den Versionen
KKeine Bearbeitungszusammenfassung |
(Anleitung für statische Tests hinzugefügt) |
||
| Zeile 17: | Zeile 17: | ||
</syntaxhighlight>Testfälle (s.u.) können diese Funktion dann mit verschiedenen Werten aufrufen und das Ergebnis prüfen. | </syntaxhighlight>Testfälle (s.u.) können diese Funktion dann mit verschiedenen Werten aufrufen und das Ergebnis prüfen. | ||
'''Statische Tests & Codeanalyse''': Derzeit gibt es für Python keinen statischen (GReQL-)Checker. Der eingereichte Code kann allerdings indirekt auf Syntaxelemente überprüft werden, indem er händisch geparst wird. Hierzu können die Python-Bordmittel, insbesondere das [https://docs.python.org/3.11/library/ast.html AST (Abstract Syntax Trees)]-Modul für Syntaxbäume genutzt werden. | |||
Derzeit gibt es für Python keinen statischen (GReQL-)Checker. Der eingereichte Code kann allerdings indirekt auf Syntaxelemente überprüft werden, indem er händisch geparst wird. Hierzu können die Python-Bordmittel, insbesondere das [https://docs.python.org/3.11/library/ast.html AST (Abstract Syntax Trees)]-Modul genutzt werden. | |||
== | ==Testtreiber für den Checker== | ||
Grundsätzlich ist ein Testtreiber ein einzelnes Modul mit einer oder mehreren [https://docs.python.org/3.11/tutorial/controlflow.html#defining-functions 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 [[Python (JACK3)|wird im UI als "Modul-Pfad für studentischen Code"]] festgelegt. | Grundsätzlich ist ein Testtreiber ein einzelnes Modul mit einer oder mehreren [https://docs.python.org/3.11/tutorial/controlflow.html#defining-functions 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 [[Python (JACK3)|wird im UI als "Modul-Pfad für studentischen Code"]] festgelegt. | ||
| Zeile 45: | Zeile 44: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
===API zum Zurückmelden von Ergebnissen=== | ===API zum Zurückmelden von Ergebnissen === | ||
Innerhalb eines Testfalls werden Ergebnisse über das Modul <code>Trace</code> zurückgeliefert. Dieses Modul sammelt alle Punktzahlen und Feedback auf und berichtet diese am Ende der Ausführung. Es bietet folgende Funktion:<syntaxhighlight lang="python3"> | Innerhalb eines Testfalls werden Ergebnisse über das Modul <code>Trace</code> zurückgeliefert. Dieses Modul sammelt alle Punktzahlen und Feedback auf und berichtet diese am Ende der Ausführung. Es bietet folgende Funktion:<syntaxhighlight lang="python3"> | ||
def printResult(result: int, feedback: str = None): | def printResult(result: int, feedback: str = None): | ||
| Zeile 57: | Zeile 56: | ||
</syntaxhighlight>{{Wichtig|In jedem Testfall muss <code>printResult</code> genau ein Mal tatsächlich aufgerufen werden!}} | </syntaxhighlight>{{Wichtig|In jedem Testfall muss <code>printResult</code> genau ein Mal tatsächlich aufgerufen werden!}} | ||
=== Schreiben von Testfällen === | ===Schreiben von Testfällen=== | ||
Der Checker nutzt für das Tracing eine spezielle Kontrollflussanalyse, basierend auf Pythons [https://docs.python.org/3.11/reference/simple_stmts.html#the-assert-statement <code>assert</code>-Schlüsselwort] und Exceptions. Mit <code>assert</code> lassen sich Ausdrücke testen, die resultierende Exception lässt sich anschließend fangen und in Feedback umwandeln. | Der Checker nutzt für das Tracing eine spezielle Kontrollflussanalyse, basierend auf Pythons [https://docs.python.org/3.11/reference/simple_stmts.html#the-assert-statement <code>assert</code>-Schlüsselwort] und Exceptions. Mit <code>assert</code> lassen sich Ausdrücke testen, die resultierende Exception lässt sich anschließend fangen und in Feedback umwandeln. | ||
| Zeile 78: | Zeile 77: | ||
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. | 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 == | In Situationen, bei denen die Punktevergabe von anderen Testfällen abhängig ist (z. B. wenn ausschließlich Punkte vergeben werden sollen, wenn statische Checks erfolgreich waren), kann diese Logik auch in einem einzigen Testfall untergebracht werden. Punktzahl und (ggf. konkatenierte) Feedback(s) müssen dann entsprechend vom Testfall zusammengesetzt werden. | ||
=== Statische Tests=== | |||
Mithilfe des [https://docs.python.org/3.11/library/ast.html AST]-Moduls kann der eingereichte Code in einen Syntaxbaum geparst werden. Dieser Syntaxbaum lässt sich mithilfe von Python-Code prüfen. Der folgende Testfall prüft beispielsweise, ob im eingereichten Code Importe enthalten sind:<syntaxhighlight lang="python3" line="1"> | |||
import Communicator | |||
import Trace | |||
import studentcode | |||
import math | |||
import ast | |||
@Communicator.decorator | |||
def static_check(): | |||
# Parsen in Syntaxbaum | |||
with open(studentcode.__file__, mode="r", encoding="utf-8") as f: | |||
source_code = f.read() | |||
tree = ast.parse(source_code) | |||
# Über Syntaxbaum iterieren und Importe suchen | |||
import_detected = None | |||
for node in ast.walk(tree): | |||
if isinstance(node, (ast.Import, ast.ImportFrom)): | |||
import_detected = node.lineno | |||
print(f"Import in line {import_detected}") | |||
# Feedback erzeugen | |||
error_feedback = None | |||
if import_detected is not None: | |||
error_feedback = f"In Zeile {import_detected} wurde ein Import erkannt. Die Lösung sollte ohne Importe auskommen." | |||
# Rückmeldung | |||
try: | |||
assert (error_feedback is None) | |||
Trace.printResult(40) # Statischer Test bestanden | |||
except Exception: | |||
Trace.printResult(0, error_feedback) # Statischer Test nicht bestanden | |||
if __name__ == "__main__": | |||
static_check() | |||
</syntaxhighlight>Dieser Test könnte beliebig erweitert werden, z. B. for-/while-Schleifen suchen oder Rekursion prüfen, indem Funktionsaufrufe auf eine bestimmte Funktion gesucht werden. Es ist wichtig, dass Aufrufe von <code>ast.walk</code> nicht ineinander verschachtelt werden – stattdessen sollten Elemente, über die ein zweites Mal iteriert werden soll, zwischengespeichert werden. | |||
==Ressourcen== | |||
Um das Entwickeln von Testfällen zu vereinfachen, haben wir einen Dummy gebaut, der die Funktion des Checkers simuliert. [[Medium:Tracing-Python-Checker-Dummy.zip|Den Dummy können Sie hier herunterladen.]] Folgende Dateien sind enthalten: | Um das Entwickeln von Testfällen zu vereinfachen, haben wir einen Dummy gebaut, der die Funktion des Checkers simuliert. [[Medium:Tracing-Python-Checker-Dummy.zip|Den Dummy können Sie hier herunterladen.]] Folgende Dateien sind enthalten: | ||
* <code>Trace.py</code> und <code>Communicator.py</code> simulieren die Funktion des Checkers. | *<code>Trace.py</code> und <code>Communicator.py</code> simulieren die Funktion des Checkers. | ||
* <code>testdriver.py</code> enthält vier Testfälle aus dem Beispiel oben und kann beliebig angepasst werden. | *<code>testdriver.py</code> enthält vier Testfälle aus dem Beispiel oben und kann beliebig angepasst werden. | ||
* <code>studentcode.py</code> enthält eine studentische Einreichung mit einem "Fehler", bei dem einer der Testfälle nicht bestanden wird. | *<code>studentcode.py</code> enthält eine studentische Einreichung mit einem "Fehler", bei dem einer der Testfälle nicht bestanden wird. | ||
* <code>run.py</code> ist ein Skript, das den Testtreiber ausführt und ausgibt, wie viele Punkte die Lösung ergeben würde. | *<code>run.py</code> 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 <code>python run.py</code> ausgeführt werden (auf Linux-Systemen kann der Befehl auch <code>python3</code> lauten, auf Windows auch <code>py</code>). | Der Dummy kann mit dem Befehl <code>python run.py</code> ausgeführt werden (auf Linux-Systemen kann der Befehl auch <code>python3</code> lauten, auf Windows auch <code>py</code>). | ||
Aktuelle Version vom 28. April 2026, 08:40 Uhr
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.
Statische Tests & Codeanalyse: Derzeit gibt es für Python keinen statischen (GReQL-)Checker. Der eingereichte Code kann allerdings indirekt auf Syntaxelemente überprüft werden, indem er händisch geparst wird. Hierzu können die Python-Bordmittel, insbesondere das AST (Abstract Syntax Trees)-Modul für Syntaxbäume genutzt werden.
Testtreiber für den Checker
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.
In Situationen, bei denen die Punktevergabe von anderen Testfällen abhängig ist (z. B. wenn ausschließlich Punkte vergeben werden sollen, wenn statische Checks erfolgreich waren), kann diese Logik auch in einem einzigen Testfall untergebracht werden. Punktzahl und (ggf. konkatenierte) Feedback(s) müssen dann entsprechend vom Testfall zusammengesetzt werden.
Statische Tests
Mithilfe des AST-Moduls kann der eingereichte Code in einen Syntaxbaum geparst werden. Dieser Syntaxbaum lässt sich mithilfe von Python-Code prüfen. Der folgende Testfall prüft beispielsweise, ob im eingereichten Code Importe enthalten sind:
import Communicator
import Trace
import studentcode
import math
import ast
@Communicator.decorator
def static_check():
# Parsen in Syntaxbaum
with open(studentcode.__file__, mode="r", encoding="utf-8") as f:
source_code = f.read()
tree = ast.parse(source_code)
# Über Syntaxbaum iterieren und Importe suchen
import_detected = None
for node in ast.walk(tree):
if isinstance(node, (ast.Import, ast.ImportFrom)):
import_detected = node.lineno
print(f"Import in line {import_detected}")
# Feedback erzeugen
error_feedback = None
if import_detected is not None:
error_feedback = f"In Zeile {import_detected} wurde ein Import erkannt. Die Lösung sollte ohne Importe auskommen."
# Rückmeldung
try:
assert (error_feedback is None)
Trace.printResult(40) # Statischer Test bestanden
except Exception:
Trace.printResult(0, error_feedback) # Statischer Test nicht bestanden
if __name__ == "__main__":
static_check()
Dieser Test könnte beliebig erweitert werden, z. B. for-/while-Schleifen suchen oder Rekursion prüfen, indem Funktionsaufrufe auf eine bestimmte Funktion gesucht werden. Es ist wichtig, dass Aufrufe von ast.walk nicht ineinander verschachtelt werden – stattdessen sollten Elemente, über die ein zweites Mal iteriert werden soll, zwischengespeichert werden.
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.pyundCommunicator.pysimulieren die Funktion des Checkers.testdriver.pyenthält vier Testfälle aus dem Beispiel oben und kann beliebig angepasst werden.studentcode.pyenthält eine studentische Einreichung mit einem "Fehler", bei dem einer der Testfälle nicht bestanden wird.run.pyist 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!