#!/usr/bin/env python3 import io import os import sys import importlib import textwrap import numpy as np import math as m import multiprocessing as mp import ast import pprint import importlib.util import time import math import numbers max_points = 0 obtained_points = 0 default_permitted_modules = ["matplotlib", "matplotlib.pyplot", "matplotlib.widgets", "functools", "mpl_toolkits.mplot3d"] timeout = 60 numeric_accuracy = 1e-6 int_accuracy = 0 registered_tests = dict() def info(string): print(string) def format_beautifully(*blocks): # step #1: convert short atomics to wrap statements preprocessed_blocks = [] for (kind, object) in blocks: if kind == 'wrap': preprocessed_blocks += [(kind, str(object))] elif (kind == 'atomic') or (kind == 'args'): string = pprint.pformat(object, width=79) if kind == 'args': string = "(" + string[1:-1] + ")" if '\n' in string: preprocessed_blocks += [('atomic', string)] else: preprocessed_blocks += [('wrap', string)] # step #2: merge consecutive wrap statements merged_blocks = [] wrap_strings = [] for (kind, string) in preprocessed_blocks: if kind == 'wrap': wrap_strings += [string] elif kind == 'atomic': merged_blocks += [('wrap', "".join(wrap_strings))] wrap_strings = [] merged_blocks += [(kind, string)] merged_blocks += [('wrap', "".join(wrap_strings))] # step #3: combine the text blocks # in particular merge atomics followed by wrap text lines = [] for (kind, string) in merged_blocks: if kind == 'wrap': if not lines: lines += textwrap.fill(string, width=79).splitlines() else: indent = len(lines[-1]) if lines else 0 new_lines = textwrap.fill("#" * indent + string, width=79).splitlines() lines[-1] = new_lines[0].replace("#" * indent, lines[-1]) lines += new_lines[1:] elif kind == 'atomic': lines += string.splitlines() return "\n".join(lines) def type_equal(a,b): # surprise #1: numpy.int64 and the like are not of type integer! # surprise #2: numpy.float64 and Python float are disjoint! def denumpify(x): return np.asscalar(x) if isinstance(x, np.generic) else x return type(denumpify(a)) == type(denumpify(b)) def equal(a, b): if not type_equal(a,b): return False if isinstance(a, int): return abs(a - b) <= int_accuracy if isinstance(a, float): return abs(a - b) < numeric_accuracy if isinstance(a, np.poly1d): return equal(a.c,b.c) if isinstance(a, np.ndarray): if a.shape != b.shape: return False if a.size == 0: return True return np.amax(abs(a.astype(np.float64) - b.astype(np.float64))) < numeric_accuracy if isinstance(a, tuple) or isinstance(a, list): if len(a) != len(b): return False for (a, b) in zip(a,b): if not equal(a,b): return False return True return a == b class CheckFailure(Exception): def __init__(self, message): self.message = message def __str__(self): return format_beautifully(('wrap', self.message)) class Timeout(CheckFailure): def __init__(self, call): self.call = call def __str__(self): (fname, *args) = self.call blocks = [('wrap', "Der Aufruf {}".format(fname)), ('args', args), ('wrap', " terminiert nicht, oder ist SEHR ineffizient.")] return format_beautifully(*blocks) class WrongOutput(CheckFailure): def __init__(self, call, expected_result, obtained_result): self.call = call self.expected_result = expected_result self.obtained_result = obtained_result def __str__(self): (fname, *args) = self.call blocks = [('wrap', "Der Aufruf {}".format(fname)), ('args', args)] if isinstance(self.obtained_result, Exception): blocks += ([('wrap', " liefert den Fehler: "), ('atomic', self.obtained_result)]) elif isinstance(self.obtained_result, type(None)): blocks += ([('wrap', " hat keine Ausgabe, sollte aber "), ('atomic', self.expected_result), ('wrap', " ausgeben.")]) else: blocks += ([('wrap', " liefert die Ausgabe "), ('atomic', self.obtained_result), ('wrap', ", richtig wäre aber "), ('atomic', self.expected_result), ('wrap', ".")]) return format_beautifully(*blocks) class WrongResult(CheckFailure): def __init__(self, call, expected_result, obtained_result): self.call = call self.expected_result = expected_result self.obtained_result = obtained_result def __str__(self): (fname, *args) = self.call blocks = [('wrap', "Der Aufruf {}".format(fname)), ('args', args)] if isinstance(self.obtained_result, Exception): blocks += ([('wrap', " liefert den Fehler: "), ('atomic', self.obtained_result)]) elif isinstance(self.obtained_result, type(None)): blocks += ([('wrap', " hat keinen Rückgabewert, sollte aber "), ('atomic', self.expected_result), ('wrap', " zurückgeben.")]) elif not type_equal(self.obtained_result, self.expected_result): blocks += ([('wrap', " liefert ein Ergebnis vom Typ "), ('wrap', "'" + type(self.obtained_result).__name__ + "'"), ('wrap', ", erwartet wird aber ein Ergebnis vom Typ "), ('wrap', "'" + type(self.expected_result).__name__ + "'.")]) else: blocks += ([('wrap', " liefert das Ergebnis "), ('atomic', self.obtained_result), ('wrap', ", richtig wäre aber "), ('atomic', self.expected_result), ('wrap', ".")]) return format_beautifully(*blocks) class AstInspector(ast.NodeVisitor): def __init__(self, file="<unknown>", imports=[]): self.file = file self.permitted_modules = set(default_permitted_modules + imports) self.errors = [] def check_module(self, module): if module in self.permitted_modules: return self.errors.append(CheckFailure("Das Modul {} darf für diese " "Aufgabe nicht verwendet werden." .format(module))) def visit_Import(self, node): for m in [n.name for n in node.names]: self.check_module(m) def visit_ImportFrom(self, node): self.check_module(node.module) def subprocess_eval(queue, proc_id, expr, module): stdout = sys.stdout with io.StringIO() as output: sys.stdout = output try: m = importlib.import_module(module) (f, *args) = map(lambda x: eval(x, vars(m)) if isinstance(x, str) else x, expr) result = f(*args) except BaseException as e: result = e queue.put((proc_id, result, output.getvalue())) sys.stdout = stdout def check_calls(forms, module_name): check_output = forms and len(forms[0]) == 3 if check_output: calls, desired_results, desired_outputs = zip(*forms) if forms else ([], [], []) else: calls, desired_results = zip(*forms) if forms else ([], []) queue = mp.Queue() processes = [] process_ids = set(range(len(calls))) for pid in process_ids: p = mp.Process(target=subprocess_eval, args=(queue, pid, calls[pid], module_name)) p.start() processes.append(p) queue_contents = [] try: for _ in process_ids: queue_contents.append(queue.get(True, timeout)) except: pass finally: for p in processes: p.terminate() for pid in process_ids: match = [result for (id, result, _) in queue_contents if id == pid] matchOutput = [out.strip() for (id, _, out) in queue_contents if id == pid] if not match: yield Timeout(calls[pid]) elif not equal(desired_results[pid], match[0]): yield WrongResult(calls[pid], desired_results[pid], match[0]) elif check_output and not equal(desired_outputs[pid], matchOutput[0]): yield WrongOutput(calls[pid], desired_outputs[pid], matchOutput[0]) def check_module(module_name, imports=[], calls=[], nuke=[], inttol=0): try: int_accuracy = inttol spec = importlib.util.find_spec(module_name) if not spec: yield CheckFailure("Die Datei {}.py konnte nicht gefunden werden. " "Sie sollte im selben Ordner liegen wie " "dieses Programm.".format(module_name)) return with open(spec.origin, 'r', encoding='utf-8') as f: source = f.read() st = ast.parse(source, spec.origin) inspector = AstInspector(spec.origin, imports) inspector.visit(st) errors = inspector.errors or check_calls(calls, module_name) for error in errors: yield error except OSError: yield CheckFailure("Die Datei {} konnte nicht geöffnet werden." .format(spec.origin)) except SyntaxError as e: yield CheckFailure("Die Datei {} enthält einen Syntaxfehler " "in Zeile {:d}." .format(spec.origin, e.lineno)) except CheckFailure as e: yield e except Exception as e: yield CheckFailure("Beim Laden des Moduls {} ist ein Fehler " "vom Typ '{}' aufgetreten." .format(module_name, str(e))) def register(name, description, points, module, **kwargs): registered_tests[name] = (description, points, module, kwargs) def check(description, points, module, **kwargs): global max_points global obtained_points pstr = ("1 Punkt" if points == 1 else f"{points:.2f} Punkte") desc = description + " (" + pstr + ")" max_points += points info("=" * (len(desc) + 2)) info(" " + desc) info("=" * (len(desc) + 2)) try: stdout, stderr = sys.stdout, sys.stderr try: with open(os.devnull, 'w') as f: sys.stdout, sys.stderr = f, f errors = list(check_module(module, **kwargs)) finally: sys.stdout, sys.stderr = stdout, stderr except BaseException as e: info("Eine unbehandelte Ausnahme ist aufgetreten: '{}'" .format(str(e))) else: if errors: for e in errors: info(str(e)) info("Diese Teilaufgabe enthält Fehler!") elif not kwargs["calls"]: max_points -= points info("Diese Teilaufgabe hat keine öffentlichen Testcases.") else: obtained_points += points info("Super! Alles scheint zu funktionieren.") info("") parts_cmdline = [] def check_from_cmdline(): global parts_cmdline parts = sys.argv[1:] parts = [p.lower() for p in parts] if len(parts) == 0: parts = registered_tests.keys() else: parts_cmdline = parts for part in parts: if part not in registered_tests: info(f"Teilaufgabe '{part}' gibt es nicht.") info(f"Vorhandene Teilaufgaben: {', '.join(registered_tests.keys())}.") sys.exit(1) for part in parts: description, points, module, kwargs = registered_tests[part] check(description, points, module, **kwargs) def import_modules(modules): for module in modules: importlib.import_module(module) def compute_process_spawn_time(): start = time.time() p = mp.Process(target=import_modules, args=(default_permitted_modules,)) p.start() p.join() end = time.time() return math.ceil(end - start) def report(): if len(parts_cmdline) != 0: print(f"Teilaufgabe{'n' if len(parts_cmdline) > 1 else ''} {', '.join(parts_cmdline)}: ", end="") print(f"{obtained_points:g} von {max_points:g} Punkten.") else: print(f"Insgesamt: {obtained_points:g} von {max_points:g} Punkten.") if __name__ == "__main__": mp.freeze_support() timeout = 2 * compute_process_spawn_time() + 9 ############################### ## Nun die eigentlichen Checks ############################### register("a", "Aufgabe 4a: Enthält meine Zeichenkette ein bestimmtes Zeichen?", 0.125, "strings", imports = [], calls = [ (("contains_char", "'U$:E<t'", "'Y'"), False), (("contains_char", "'v<3:V'", "'m'"), False), (("contains_char", "',2&RO.v[{vAowY,6yS'", "'+'"), False), (("contains_char", "',K3,b90t{f$5~C0eUHn'", "'q'"), False), (("contains_char", "'nyCS'", "'!'"), False), (("contains_char", "'x&WS9J&l1UQ2'", "'V'"), False), (("contains_char", "'(U-\\LJzehD#K,$wNBUu;.~W>S3s3i'", "'B'"), True), (("contains_char", "'%a{gdN`slOCC$.vv<$|aND,}!Tm2'", "'y'"), False), (("contains_char", "'O+s/>MOl,'", "']'"), False), (("contains_char", "'++hBv)<c'", "'P'"), False), (("contains_char", "'p(cLIR.<v)S38e,_&pSup=b$I/='", "'V'"), False), (("contains_char", "'6NutdCOuQzEG<(WJocX6BD,e]lk'", "'G'"), True), (("contains_char", "'&~^!k3dtE'", "'S'"), False), (("contains_char", "'Qg_Z/vNHTBdlbG]'", "'!'"), False), (("contains_char", "'=(V?UValev'", "'Y'"), False), (("contains_char", "',Psty(JPj%%MFE[HErgVN(h*yn8T'", "'('"), True), (("contains_char", "'w*]b5vZ<wWw7XKRb+Qh[J{/gSjg,_'", "'b'"), True), (("contains_char", "'@,9|-k~OsIC,%1j28.bR/|7nnT{'", "'4'"), False), (("contains_char", "'.z%90d57%&}B{_z(Q]&=|K'", "'C'"), False), (("contains_char", "''", "'>'"), False), (("contains_char", "'*:`^}6]r/%aKJQFN%C*'", "'P'"), False), (("contains_char", "'h^97W2Mg-f},k>`'", "'M'"), True), (("contains_char", "'Q)k?(BMh]?TWIu#1.erd-9{x|DY'", "'['"), False), (("contains_char", "'mp%>YNyg#+^w=##=*T)(%6?$t3@L.P]'", "'S'"), False), (("contains_char", "'lJ-+w{C2<paTLJ|+l6.q%JTZ'", "'r'"), False), (("contains_char", "'k|_EL#&=}h'", "'j'"), False), (("contains_char", "'/{>S)MkE|9U=XzLv=<]'", "'}'"), False), (("contains_char", "'xW]=J>_WWMVR:]D2?/+'", "'@'"), False), (("contains_char", "'z#QHmz/AQiq>d'", "'d'"), True), (("contains_char", "'oFKxtGlv:t92I*q,'", "'Z'"), False), (("contains_char", "'3xg~~~}E@=/:G'", "'H'"), False), ]) register("b", "Aufgabe 4b: Palindrome", 0.125, "strings", imports = [], calls = [ (("is_palindrome", "'never odd or even'"), True), (("is_palindrome", "'top spot'"), True), (("is_palindrome", "'ABBA'"), True), (("is_palindrome", "'taco o cat'"), True), (("is_palindrome", "'ABB'"), False), ]) register("c", "Aufgabe 4c: Count Char Frequency", 0.125, "strings", calls = [ (("count_char_frequency", "'ABBA'"), {"A": 2, "B": 2}), (("count_char_frequency", "'n{Yv)@'"), {'n': 1, '{': 1, 'Y': 1, 'v': 1, ')': 1, '@': 1}), (("count_char_frequency", "'uI080'"), {'u': 1, 'I': 1, '0': 2, '8': 1}), (("count_char_frequency", "'WFuZg0>D4-:'"), {'W': 1, 'F': 1, 'u': 1, 'Z': 1, 'g': 1, '0': 1, '>': 1, 'D': 1, '4': 1, '-': 1, ':': 1}), (("count_char_frequency", "'1Z,Cj:}'"), {'1': 1, 'Z': 1, ',': 1, 'C': 1, 'j': 1, ':': 1, '}': 1}), (("count_char_frequency", "'m%lqbv0?F4\*/Dj'"), {'m': 1, '%': 1, 'l': 1, 'q': 1, 'b': 1, 'v': 1, '0': 1, '?': 1, 'F': 1, '4': 1, '\\': 1, '*': 1, '/': 1, 'D': 1, 'j': 1}), (("count_char_frequency", "'>L|B!t[uU#m5'"), {'>': 1, 'L': 1, '|': 1, 'B': 1, '!': 1, 't': 1, '[': 1, 'u': 1, 'U': 1, '#': 1, 'm': 1, '5': 1}), (("count_char_frequency", "'bdZBiDF'"), {'b': 1, 'd': 1, 'Z': 1, 'B': 1, 'i': 1, 'D': 1, 'F': 1}), (("count_char_frequency", "':+2'"), {':': 1, '+': 1, '2': 1}), (("count_char_frequency", "'@?WR<4'"), {'@': 1, '?': 1, 'W': 1, 'R': 1, '<': 1, '4': 1}), (("count_char_frequency", "'}2NA*0'"), {'}': 1, '2': 1, 'N': 1, 'A': 1, '*': 1, '0': 1}), (("count_char_frequency", "'h0E.p\jh/'"), {'h': 2, '0': 1, 'E': 1, '.': 1, 'p': 1, '\\': 1, 'j': 1, '/': 1}), (("count_char_frequency", "'X~yBV-IZ6+YC:U'"), {'X': 1, '~': 1, 'y': 1, 'B': 1, 'V': 1, '-': 1, 'I': 1, 'Z': 1, '6': 1, '+': 1, 'Y': 1, 'C': 1, ':': 1, 'U': 1}), (("count_char_frequency", "'f!O'"), {'f': 1, '!': 1, 'O': 1}), (("count_char_frequency", "'NMiYLzY@-/M3.Z'"), {'N': 1, 'M': 2, 'i': 1, 'Y': 2, 'L': 1, 'z': 1, '@': 1, '-': 1, '/': 1, '3': 1, '.': 1, 'Z': 1}), (("count_char_frequency", "':oc#'"), {':': 1, 'o': 1, 'c': 1, '#': 1}), (("count_char_frequency", "''"), {}), (("count_char_frequency", "'i;ZS=*)ZW'"), {'i': 1, ';': 1, 'Z': 2, 'S': 1, '=': 1, '*': 1, ')': 1, 'W': 1}), (("count_char_frequency", "'(,|'"), {'(': 1, ',': 1, '|': 1}), (("count_char_frequency", "'zLpknQ/x!+kMz3i'"), {'z': 2, 'L': 1, 'p': 1, 'k': 2, 'n': 1, 'Q': 1, '/': 1, 'x': 1, '!': 1, '+': 1, 'M': 1, '3': 1, 'i': 1}), (("count_char_frequency", "'5dUC{Pm[9UXI'"), {'5': 1, 'd': 1, 'U': 2, 'C': 1, '{': 1, 'P': 1, 'm': 1, '[': 1, '9': 1, 'X': 1, 'I': 1}), (("count_char_frequency", "'[x-oB'"), {'[': 1, 'x': 1, '-': 1, 'o': 1, 'B': 1}), (("count_char_frequency", "'Ja>umPDNZ'"), {'J': 1, 'a': 1, '>': 1, 'u': 1, 'm': 1, 'P': 1, 'D': 1, 'N': 1, 'Z': 1}), (("count_char_frequency", "'Mi%]v|VHa-'"), {'M': 1, 'i': 1, '%': 1, ']': 1, 'v': 1, '|': 1, 'V': 1, 'H': 1, 'a': 1, '-': 1}), (("count_char_frequency", "'FJiZ<'"), {'F': 1, 'J': 1, 'i': 1, 'Z': 1, '<': 1}), ]) register("d", "Aufgabe 4d: First Non-Repeating Char", 0.125, "strings", calls = [ (("first_non_repeating_char", "''", False), None), (("first_non_repeating_char", "'Y'", True), None), (("first_non_repeating_char", "''", True), None), (("first_non_repeating_char", "'J'", False), 'J'), (("first_non_repeating_char", "'1,EM,Lq'", False), '1'), (("first_non_repeating_char", "'9+3SMCD/$+5G#]'", False), '9'), (("first_non_repeating_char", "']6I4\e'", True), None), (("first_non_repeating_char", "'?y~M$W~*Kg]1]I'", True), '~'), (("first_non_repeating_char", "'hXCxv2\@Mj|'", True), None), (("first_non_repeating_char", "'@m?'", True), None), (("first_non_repeating_char", "'@EsE!{w-'", False), '@'), (("first_non_repeating_char", "'Eb)@E[W97Nutz~b'", False), ')'), (("first_non_repeating_char", "'|NES{//%kaqzz$E'", True), 'E'), (("first_non_repeating_char", "'LjR%Ruj:WZ'", False), 'L'), (("first_non_repeating_char", "',4Zc<'", True), None), (("first_non_repeating_char", "'p=mS{'", True), None), (("first_non_repeating_char", "'/+H[yqv0'", False), '/'), (("first_non_repeating_char", "'4>'", True), None), (("first_non_repeating_char", "'WC=S^QzD'", True), None), (("first_non_repeating_char", "'r%/f%z'", True), '%'), (("first_non_repeating_char", "'1j7haYStE&'", True), None), (("first_non_repeating_char", "';fwFPi^?Cqkr3'", True), None), (("first_non_repeating_char", "'bMso5Bw&A2kKIC'", False), 'b'), (("first_non_repeating_char", "'ceI'", True), None), (("first_non_repeating_char", "',COZ_<'", False), ','), ]) register("e", "Aufgabe 4e: Rotierende Zeichenketten", 0.25, "strings", calls = [(('rotate_string', "'ABBA'", 0, 0), 'ABBA'), (('rotate_string', "'HELLO'", 1, 3), 'LOHEL'), (('rotate_string', "':8t3+USU8YN0j2qIB>'", 1, 2), '>:8t3+USU8YN0j2qIB'), (('rotate_string', "'E4VV[xVS1a9*Yq9g!d'", 4, 1), 'V[xVS1a9*Yq9g!dE4V'), (('rotate_string', "'[6V%jIaz-6F6%mB'", 0, 0), '[6V%jIaz-6F6%mB'), (('rotate_string', "'4iZhl4UY:'", 1, 0), 'iZhl4UY:4'), (('rotate_string', "'j]?&;&5(k*~do'", 1, 1), 'j]?&;&5(k*~do'), (('rotate_string', "'8Fl,:'", 1, 2), ':8Fl,'), (('rotate_string', "'nghmy'", 4, 4), 'nghmy'), (('rotate_string', "'_ClEL@q|w$w._.ReR;.'", 3, 2), 'ClEL@q|w$w._.ReR;._'), (('rotate_string', "'A8*Or>V@h'", 0, 2), '@hA8*Or>V'), (('rotate_string', "'uBuWsYd:![CC6h<['", 2, 0), 'uWsYd:![CC6h<[uB'), (('rotate_string', "']PpGxG%R'", 4, 2), 'pGxG%R]P'), (('rotate_string', "'4G3-\\wy~e?Axl(d!g'", 2, 0), '3-\\wy~e?Axl(d!g4G'), (('rotate_string', "',7,5{LOG|:fG*'", 4, 4), ',7,5{LOG|:fG*'), (('rotate_string', "'klkt|i){U{*9X2~!niH'", 3, 1), 'kt|i){U{*9X2~!niHkl'), (('rotate_string', "'yN!|V7@:<9'", 4, 0), 'V7@:<9yN!|'), (('rotate_string', "'f@{(vieL#K~T>k,E@'", 2, 0), '{(vieL#K~T>k,E@f@'), (('rotate_string', "'1C@$p:kvHLb.'", 1, 4), 'Lb.1C@$p:kvH'), (('rotate_string', "'>J{piWd:kC8k((>M90I'", 4, 2), '{piWd:kC8k((>M90I>J'), (('rotate_string', "'.7)vE^6C~Dhh'", 3, 1), ')vE^6C~Dhh.7'), (('rotate_string', "'F[HP4fXcZv$'", 2, 1), '[HP4fXcZv$F'), (('rotate_string', "'-)=PX^Jd@|'", 3, 4), '|-)=PX^Jd@'), (('rotate_string', "'U65-:*G.nAGO=rm'", 3, 4), 'mU65-:*G.nAGO=r'), (('rotate_string', "'.Lnv$[kyUEDIX'", 1, 4), 'DIX.Lnv$[kyUE'), ]) register("f", "Aufgabe 4f: Rotationsäquivalenz", 0.25, "strings", calls = [ (("rotationally_equivalent", "''", "''"), True), (("rotationally_equivalent", "'ABBA'", "'BAAB'"), True), (("rotationally_equivalent", "'HELLO'", "'LLOHE'"), True), (("rotationally_equivalent", "'LISTE'", "'ELIST'"), True), (("rotationally_equivalent", "'TAUTAU'", "'UTAUTA'"), True), (("rotationally_equivalent", "'ABBA'", "'BABA'"), False), ]) check_from_cmdline() report()