diff --git a/core/fixtures/testdata-core.json b/core/fixtures/testdata-core.json index ff4385a6ca4c846b14debc333abfeef9452b277d..fa913e9d82a430eb60e4042eb63a026da7ed8115 100644 --- a/core/fixtures/testdata-core.json +++ b/core/fixtures/testdata-core.json @@ -1 +1,92 @@ -[{"model": "core.submissiontype", "pk": 1, "fields": {"name": "Aufgabe 01", "slug": "brezmaphgocfuikw", "full_score": 10, "task_description": "description", "possible_solution": "solution", "correction_guideline": "guideline"}}, {"model": "core.submissiontype", "pk": 2, "fields": {"name": "Aufgabe 02", "slug": "zbjfwldsuhqgxvmn", "full_score": 20, "task_description": "description", "possible_solution": "solution", "correction_guideline": "guideline"}}, {"model": "core.student", "pk": 1, "fields": {"matrikel_no": "12345678", "has_logged_in": false, "name": "Student 01 Vorname und Nachname", "user": 4}}, {"model": "core.student", "pk": 2, "fields": {"matrikel_no": "87654321", "has_logged_in": false, "name": "Student 02 Vorname und Nachname", "user": 5}}, {"model": "core.submission", "pk": 1, "fields": {"slug": "qgleatcwzfxsdnjr", "seen": false, "type": 1, "text": "function generate(timeout){\r\n\r\n\t$('#menu_button_img').attr('src', 'style/menu_blink.gif'); \r\n\r\n\tif(timeout == 0)\t\t\t\t\t\t\t\t\r\n\t\t$('#config_form').attr('action', $('#config_form').attr('action') + '#title'); \t\t\t\t// show directly the question\r\n\telse\r\n\t\ttimeout = 0;\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// disable timeout\r\n\t\r\n\tsetTimeout(function(){ $('#config_form').submit(); }, timeout);\r\n\r\n}", "pre_corrections": "COMPILER", "student": 1}}, {"model": "core.submission", "pk": 2, "fields": {"slug": "mrthqgsloaydjfnc", "seen": false, "type": 2, "text": "function showTextEditor(){\r\n\r\n\t$('.ilc_question_Standard').hide('slow');\r\n\t$('.ilc_question_ml_Standard').hide('slow');\r\n\t$('.text_editor').show('slow');\r\n\t\r\n}\r\n\r\nfunction showConfig(){\r\n\r\n\t$('#config_wrapper').animate(\r\n\t\t{\r\n\t\t\tright: ($('#config_wrapper').css('right') == '0px' ? '-322px' : '0px')\r\n\t\t}, \r\n\t500);\r\n\r\n}", "pre_corrections": "LINKER ERROR", "student": 1}}, {"model": "core.submission", "pk": 3, "fields": {"slug": "hunkgevtcfdobyxw", "seen": false, "type": 2, "text": "$(document).keydown(function(evt){\r\n\r\n\tif(evt.which == 9){\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// #9 = TAB\r\n\t\tgenerate(0);\r\n\t\tevt.preventDefault();\r\n\t}\r\n\t\r\n});", "pre_corrections": "ALL GOOD", "student": 2}}, {"model": "core.submission", "pk": 4, "fields": {"slug": "gurvbyzxjfmhdiep", "seen": false, "type": 1, "text": "function showTextEditor(){\r\n\r\n\t$('.ilc_question_Standard').hide('slow');\r\n\t$('.ilc_question_ml_Standard').hide('slow');\r\n\t$('.text_editor').show('slow');\r\n\t\r\n}\r\n\r\nfunction showConfig(){\r\n\r\n\t$('#config_wrapper').animate(\r\n\t\t{\r\n\t\t\tright: ($('#config_wrapper').css('right') == '0px' ? '-322px' : '0px')\r\n\t\t}, \r\n\t500);\r\n\r\n}", "pre_corrections": "QUACK", "student": 2}}] \ No newline at end of file +[ + { + "fields": { + "full_score": 10, + "name": "Aufgabe 01", + "possible_solution": "solution", + "slug": "brezmaphgocfuikw", + "task_description": "description" + }, + "model": "core.submissiontype", + "pk": 1 + }, + { + "fields": { + "full_score": 20, + "name": "Aufgabe 02", + "possible_solution": "solution", + "slug": "zbjfwldsuhqgxvmn", + "task_description": "description" + }, + "model": "core.submissiontype", + "pk": 2 + }, + { + "fields": { + "has_logged_in": false, + "matrikel_no": "12345678", + "name": "Student 01 Vorname und Nachname", + "user": 4 + }, + "model": "core.student", + "pk": 1 + }, + { + "fields": { + "has_logged_in": false, + "matrikel_no": "87654321", + "name": "Student 02 Vorname und Nachname", + "user": 5 + }, + "model": "core.student", + "pk": 2 + }, + { + "fields": { + "pre_corrections": "COMPILER", + "seen_by_student": false, + "slug": "qgleatcwzfxsdnjr", + "student": 1, + "text": "function generate(timeout){\r\n\r\n\t$('#menu_button_img').attr('src', 'style/menu_blink.gif'); \r\n\r\n\tif(timeout == 0)\t\t\t\t\t\t\t\t\r\n\t\t$('#config_form').attr('action', $('#config_form').attr('action') + '#title'); \t\t\t\t// show directly the question\r\n\telse\r\n\t\ttimeout = 0;\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// disable timeout\r\n\t\r\n\tsetTimeout(function(){ $('#config_form').submit(); }, timeout);\r\n\r\n}", + "type": 1 + }, + "model": "core.submission", + "pk": 1 + }, + { + "fields": { + "pre_corrections": "LINKER ERROR", + "seen_by_student": false, + "slug": "mrthqgsloaydjfnc", + "student": 1, + "text": "function showTextEditor(){\r\n\r\n\t$('.ilc_question_Standard').hide('slow');\r\n\t$('.ilc_question_ml_Standard').hide('slow');\r\n\t$('.text_editor').show('slow');\r\n\t\r\n}\r\n\r\nfunction showConfig(){\r\n\r\n\t$('#config_wrapper').animate(\r\n\t\t{\r\n\t\t\tright: ($('#config_wrapper').css('right') == '0px' ? '-322px' : '0px')\r\n\t\t}, \r\n\t500);\r\n\r\n}", + "type": 2 + }, + "model": "core.submission", + "pk": 2 + }, + { + "fields": { + "pre_corrections": "ALL GOOD", + "seen_by_student": false, + "slug": "hunkgevtcfdobyxw", + "student": 2, + "text": "$(document).keydown(function(evt){\r\n\r\n\tif(evt.which == 9){\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t// #9 = TAB\r\n\t\tgenerate(0);\r\n\t\tevt.preventDefault();\r\n\t}\r\n\t\r\n});", + "type": 2 + }, + "model": "core.submission", + "pk": 3 + }, + { + "fields": { + "pre_corrections": "QUACK", + "seen_by_student": false, + "slug": "gurvbyzxjfmhdiep", + "student": 2, + "text": "function showTextEditor(){\r\n\r\n\t$('.ilc_question_Standard').hide('slow');\r\n\t$('.ilc_question_ml_Standard').hide('slow');\r\n\t$('.text_editor').show('slow');\r\n\t\r\n}\r\n\r\nfunction showConfig(){\r\n\r\n\t$('#config_wrapper').animate(\r\n\t\t{\r\n\t\t\tright: ($('#config_wrapper').css('right') == '0px' ? '-322px' : '0px')\r\n\t\t}, \r\n\t500);\r\n\r\n}", + "type": 1 + }, + "model": "core.submission", + "pk": 4 + } +] diff --git a/grrr.py b/grrr.py index b73afb33cd6a6f84268f485851101591f983a943..4294b4fe1a6820e353225ffb831127a7b67eb127 100644 --- a/grrr.py +++ b/grrr.py @@ -9,28 +9,34 @@ import json import django django.setup() -from django.contrib.auth.models import Group, User - -from core.models import Student - - - +from django.contrib.auth.models import User +from core.models import Student, Submission def parseme(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command") - newstudentpasswordlist = subparsers.add_parser( - 'newstudentpasswordlist', + parser.add_argument( + '-o', '--output', + help='Where the output goes (not info messages)', + default=sys.stdout, + type=argparse.FileType(mode='r'), + ) + + ### parser for printing out new passwordlists ### + passwordlist = subparsers.add_parser( + 'passwordlist', help='all student passwords will be changed and a list of these password will be printed' ) - newstudentpasswordlist.add_argument( + passwordlist.add_argument( 'instance', + default='', help='name of the instance that generated the passwords' ) + ### parser for replacing usernames ### replaceusernames = subparsers.add_parser( 'replaceusernames', help='replaces all usernames based on a matrikel_no -> new_name dict (input should be JSON)' @@ -38,9 +44,11 @@ def parseme(): replaceusernames.add_argument( 'matno2username_dict', help='the mapping as a JSON file', + default=sys.stdin, type=argparse.FileType('r') ) + ### parser for enabling or disabling users ### enableusers = subparsers.add_parser( 'enableusers', help='All user accounts will be disabled' @@ -64,19 +72,16 @@ def parseme(): nargs='+', default=()) - parser.add_argument( - '-o', '--output', - help='Where the output goes (not info messages)', - default=sys.stdout, - type=argparse.FileType(mode='r'), - ) + ### parser for extracting submissions ### + subparsers.add_parser('extractsubmissions') return parser.parse_args() -def handle_newstudentpasswordlist(output=sys.stdout, instance=""): +def handle_passwordlist(output=sys.stdout, instance="", **kwargs): with open('/usr/share/dict/words') as words: - choose_from = list({word.strip().lower() for word in words if 5 < len(word) < 8}) + choose_from = list({word.strip().lower() + for word in words if 5 < len(word) < 8}) writer = csv.writer(output) writer.writerow(['Name', 'Matrikel', 'Username', 'password', 'instance']) @@ -90,7 +95,8 @@ def handle_newstudentpasswordlist(output=sys.stdout, instance=""): writer.writerow([student.name, student.matrikel_no, student.user.username, password, instance]) -def handle_enableusers(switch, exclude, include): + +def handle_enableusers(switch, exclude, include, **kwargs): if include: for user in User.objects.filter(username__in=include): @@ -102,7 +108,8 @@ def handle_enableusers(switch, exclude, include): user.save() -def handle_replace_usernames(matno2username): +def handle_replaceusernames(matno2username_dict, **kwargs): + matno2username = json.JSONDecoder().decode(matno2username_dict.read()) for student in Student.objects.all(): if student.matrikel_no in matno2username: new_name = matno2username[student.matrikel_no] @@ -110,15 +117,15 @@ def handle_replace_usernames(matno2username): student.user.save() +def handle_extractsubmissions(output, **kwargs): + for submission in Submission.objects.filter(feedback__isnull=False).order_by('type'): + print(submission.feedback.score, repr(submission.text), file=open(str(submission.type).replace(' ', '_'), 'a')) + + def main(): args = parseme() - - if args.command == 'newstudentpasswordlist': - handle_newstudentpasswordlist(args.output, args.instance) - if args.command == 'enableusers': - handle_enableusers(args.switch, args.exclude, args.include) - if args.command == 'replaceusernames': - handle_replace_usernames(json.JSONDecoder().decode(args.matno2username_dict.read())) + if args.command: + globals()['handle_' + args.command](**vars(args)) if __name__ == '__main__': main() diff --git a/processing.py b/processing.py deleted file mode 100644 index cc775307ae10dd49b1a100f363d4e29573af70ab..0000000000000000000000000000000000000000 --- a/processing.py +++ /dev/null @@ -1,155 +0,0 @@ -import abc -import hashlib -import json -import tempfile -import os - -TEMP_DIR = tempfile.mkdtemp() -os.chdir(TEMP_DIR) - -before = { - "username": { - "name": "87654321", - "email": "username@example.org", - "matrikel_no": "87654321", - "submissions": [ - { - "type": "[a01] Zeichen und Strings", - "code": "#include <ctype.h>\nint umschalt(char *s)\n\n{\n\tint i = 0, count = 0;\n\twhile(s[i] != '\\0')\n\t{\n\t\tif(islower(s[i]) != 0)\n\t\t{\n\t\t\ts[i] = toupper(s[i]);\n\t\t} else if(isupper(s[i]) != 0)\n\t\t{\n\t\t\ts[i] = tolower(s[i]);\n\t\t} else\n\t\t{\n\t\t\tcount++;\n\t\t}\n\t\ti++;\n\t}\n\treturn count;\n}", - "tests" : {} - }, - { - "type": "[a02] Iteration", - "code": "#include <stdlib.h>\nlong iteP(unsigned int k)\n{\n\tif(k == 0)\n\t{\n\t\treturn 3;\n\t}\n\tif( k == 1)\n\t{\n\t\treturn 0;\n\t}\n\tif( k == 3)\n\t{\n\t\treturn 2;\n\t}\n\tint *p = malloc(k * sizeof(int));\n\tp[0] = 3;\n\tp[1] = 0;\n\tp[2] = 2;\n\tfor(int i = 3; i < k; i++)\n\t{\n\t\tp[i] = p[i-2] + p[i - 3];\n\t}\n\n\treturn p[k];\n}", - "tests" : {} - } - ] - } -} - -after = { - "username": { - "name": "87654321", - "email": "username@example.org", - "matrikel_no": "87654321", - "submissions": [ - { - "type": "[a01] Zeichen und Strings", - "code": "#include <ctype.h>\nint umschalt(char *s)\n\n{\n\tint i = 0, count = 0;\n\twhile(s[i] != '\\0')\n\t{\n\t\tif(islower(s[i]) != 0)\n\t\t{\n\t\t\ts[i] = toupper(s[i]);\n\t\t} else if(isupper(s[i]) != 0)\n\t\t{\n\t\t\ts[i] = tolower(s[i]);\n\t\t} else\n\t\t{\n\t\t\tcount++;\n\t\t}\n\t\ti++;\n\t}\n\treturn count;\n}", - "tests": { - "EmptyTest": { - "name": "EmptyTest", - "annotation": "", - "label": "NOT_EMPTY" - } - } - }, - { - "type": "[a02] Iteration", - "code": "#include <stdlib.h>\nlong iteP(unsigned int k)\n{\n\tif(k == 0)\n\t{\n\t\treturn 3;\n\t}\n\tif( k == 1)\n\t{\n\t\treturn 0;\n\t}\n\tif( k == 3)\n\t{\n\t\treturn 2;\n\t}\n\tint *p = malloc(k * sizeof(int));\n\tp[0] = 3;\n\tp[1] = 0;\n\tp[2] = 2;\n\tfor(int i = 3; i < k; i++)\n\t{\n\t\tp[i] = p[i-2] + p[i - 3];\n\t}\n\n\treturn p[k];\n}", - "tests": {} - } - ] - } -} - - -def all_subclasses(cls): - return cls.__subclasses__() \ - + [g for s in cls.__subclasses__() for g in all_subclasses(s)] - -def sha1_prefix(submission_obj): - return hashlib.sha1( - submission_obj['code'].encode() - ).hexdigest()[:12] - -class Test(metaclass=abc.ABCMeta): - """docstring for IliasQuestion""" - - @classmethod - def available_tests(cls): - return {sub.__name__ : sub for sub in all_subclasses(cls)} - - def __new__(cls, *args, **kwargs): - assert hasattr(cls, 'depends'), "depends not defined" - assert hasattr(cls, 'label_success'), "label_success not defined" - assert hasattr(cls, 'label_failure'), "label_failure not defined" - return super().__new__(cls) - - def __init__(self, submission_obj): - - if not self.dependencies_satisfied(submission_obj): - self.result = self.label_failure - self.annotation = "TEST DEPENDENCY NOT MET" - return - - elif str(self) in submission_obj['tests']: - self.deserialize(submission_obj['tests'][str(self)]) - - else: - self.result, self.annotation = self.run_test(submission_obj) - self.serialize(submission_obj) - - def __bool__(self): - return self.result - - def __str__(self): - return self.__class__.__name__ - - def dependencies_satisfied(self, submission_obj): - return all(dep(submission_obj).result for dep in self.depends) - - def deserialize(self, test): - self.result = test['label'] == self.label_success - self.annotation = test['annotation'] - - def serialize(self, submission_obj): - as_dict = { - 'name' : str(self), - 'annotation' : self.annotation - } - - if self.result: - as_dict['label'] = self.label_success - else: - as_dict['label'] = self.label_failure - - submission_obj['tests'][str(self)] = as_dict - - @abc.abstractmethod - def run_test(self, submission_obj) -> (bool, str): - return NotImplemented - - -class EmptyTest(Test): - """docstring for EmptyTest""" - - depends = () - label_success = 'NOT_EMPTY' - label_failure = 'EMPTY' - - def run_test(self, submission_obj): - return bool(submission_obj['code'].strip()), "" - - -class CompileTest(Test): - - depends = (EmptyTest, ) - label_success = 'COMPILATION_SUCCESSFUL' - label_failure = 'COMPILATION_FAILED' - - def run_test(self, submission_obj): - prefix = sha1_prefix(submission_obj) - os.mkdir(prefix) - - return False, "naa" - - - - - -d = CompileTest(before["username"]['submissions'][0]) - - -with open("/dev/null", "w") as out: - out.write(json.JSONEncoder().encode(after)) diff --git a/scripts/README.rst b/scripts/README.rst index f07fc7c98533c47e545e02655b1c317d173e884e..46af791e4de48682ed54ba493832131d7dae2f81 100644 --- a/scripts/README.rst +++ b/scripts/README.rst @@ -1,7 +1,7 @@ What is this directory about? ============================= -Well, it just servers as a collection of files that currently live in folders +Well, it just serves as a collection of files that currently live in folders not part of the git repository, since they contain volatile or test data. I include them here for the sake of completeness, but they will be removed in later versions, since their work has to be encapsulated in the overall process. diff --git a/scripts/convert.py b/scripts/convert.py new file mode 100755 index 0000000000000000000000000000000000000000..62b10f89a6858dfa7658e99c238a35f3b8c22f90 --- /dev/null +++ b/scripts/convert.py @@ -0,0 +1,141 @@ +#!/usr/local/bin/python3 +""" a simple script that converts ilias exam output to readable json + +The json output will look like this: +{ + "max.mustermann": { <<--- OR all uppercase letter of the name + username/matrikel_no + "matrikel_no": "12345678", + "name": "Mustermann, Max", + "task_list": { + "[task_id_1]": "print Hello World!", + ...., + "[task_id_n]": "#include <stdio.h> etc." + } + }, + ... ans so on +} + +usage: convert.py [-h] [-n NUMBER_OF_TASKS] INFILE OUTFILE + +positional arguments: + INFILE Ilias exam data + OUTFILE Where to write the final file + +optional arguments: + -h, --help show this help message and exit + -n NUMBER_OF_TASKS, --NUMBER_OF_TASKS NUMBER_OF_TASKS + Where to write the final file + + +Author: Jan Maximilian Michal +Date: 30 March 2017 +""" + +import json +import os +import re +import argparse +import urllib.parse +from collections import namedtuple, defaultdict + +from xlrd import open_workbook + +parser = argparse.ArgumentParser() +parser.add_argument('INFILE', help='Ilias exam data') +parser.add_argument('OUTFILE', help='Where to write the final file') +parser.add_argument('-u', '--usernames', help='a json dict matno -> email') +parser.add_argument( + '-n', '--NUMBER_OF_TASKS', + default=0, # don't check + metavar='NUMBER_OF_TASKS', + type=int, + help='Where to write the final file') +args = parser.parse_args() + +# meta sheet contains ilias evaluation names usernames etc - data contains code +meta, *data = open_workbook(args.INFILE, open(os.devnull, 'w')).sheets() + +# one user has one submission (code) per task +# yes, I know it is possible to name match groups via (?P<name>) but +# I like this solution better since it gets the job done nicely +user_head = namedtuple('user_head', 'kohorte, name') +user_head_re = re.compile(r'^Ergebnisse von Testdurchlauf (?P<kohorte>\d+) für (?P<name>[\w\s\.,-]+)$') + +# one task has a title and id and hpfly code +task_head_re = re.compile(r'^Quellcode Frage(?P<title>.*) \d{8}$') + +# nor parsing the weird mat no +matno_re = re.compile(r'^(?P<matrikel_no>\d{8})-(\d{3})-(\d{3})$') + +# Modify these iterators in order to change extraction behaviour + + +def sheet_iter_meta(sheet): + """ yield first and second col entry as tuple of (name, matnr) """ + for row in (sheet.row(i) for i in range(1, sheet.nrows)): + m = re.search(matno_re, row[1].value) + yield row[0].value, m.group('matrikel_no') if m else row[1].value + + +def sheet_iter_data(sheet): + """ yields all rows that are not of empty type as one string """ + for row in (sheet.row(i) for i in range(sheet.nrows)): + if any(map(lambda c: c.ctype, row)): + yield ''.join(c.value for c in row) + +# nice! +name2mat = dict(sheet_iter_meta(meta)) + +# from xls to lists and namedtuples +# [ [user0, task0_h, code0, ..., taskn, coden ], ..., [...] ] +root = [] +for sheet in data: + for row in sheet_iter_data(sheet): + user = re.search(user_head_re, row) + task = re.search(task_head_re, row) + if user: + root.append([user_head(*user.groups())]) + elif task: + root[-1].append(task.group('title')) + else: # should be code + root[-1].append(urllib.parse.unquote(row).strip()) + +if args.NUMBER_OF_TASKS: + for (user, *task_list) in sorted(root, key=lambda u: u[0].name): + assert len(task_list) == args.NUMBER_OF_TASKS * 2 + +mat_to_email = defaultdict(str) +if args.usernames: + with open(args.usernames) as data: + mat_to_email.update(json.JSONDecoder().decode(data.read())) + +def get_username(user): + if name2mat[user.name] in mat_to_email: + return mat_to_email[name2mat[user.name]].split('@')[0] + return ''.join(filter(str.isupper, user.name)) + name2mat[user.name] + +usernames = {user.name : get_username(user) for (user, *_) in root} + +# form list to json_like via comprehension +# the format {userinitials + matrikel_no : {name:, matrikel_no:, tasklist: {id:, ..., id:}}} +json_dict = { + usernames[user.name] : { + 'name' : name2mat[user.name], + 'email' : mat_to_email[name2mat[user.name]], + 'matrikel_no' : name2mat[user.name], + 'submissions' : [ + { + "type" : task, + "code" : code, + "label" : [], + "annotations": {} + } for task, code in zip(task_list[::2], task_list[1::2]) + ] + } for (user, *task_list) in sorted(root, key=lambda u: u[0].name) +} + +# just encode python style +with open(args.OUTFILE, "w") as out: + out.write(json.JSONEncoder().encode(json_dict)) + +print(f"Wrote data to {args.OUTFILE}. Done.") diff --git a/scripts/matrikel_to_email.py b/scripts/matrikel_to_email.py deleted file mode 100644 index 920a278fbbf3c4adae90750c8936aedb414d1831..0000000000000000000000000000000000000000 --- a/scripts/matrikel_to_email.py +++ /dev/null @@ -1,10 +0,0 @@ -from itertools import chain -import re, json - -OUTFILE = 'matno2email.json' - -with \ - open('binf1801-flexnow-20170329.csv') as inf, \ - open('bphy1601-flexnow-20170328.csv') as phy, \ - open(OUTFILE, "w") as out: - out.write(json.JSONEncoder().encode({matno : email for (matno, email) in (re.split(r'[\t;]', line.strip()) for line in chain(inf, phy) if line)})) # i just love one liners diff --git a/util/__init__.py b/util/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/populatedb.py b/util/populatedb.py similarity index 100% rename from populatedb.py rename to util/populatedb.py diff --git a/util/processing.py b/util/processing.py new file mode 100644 index 0000000000000000000000000000000000000000..997cf1b86f70c1b55fe3b7303234512fee8d345b --- /dev/null +++ b/util/processing.py @@ -0,0 +1,224 @@ +import abc +import hashlib +import json +import tempfile +import os +import shutil +import subprocess + +before = { + "username": { + "name": "87654321", + "email": "username@example.org", + "matrikel_no": "87654321", + "submissions": [ + { + "type": "[a01] Zeichen und Strings", + "code": "#include <ctype.h>\nint umschalt(char *s)\n\n{\n\t if (s == 0) return 0;int i = 0, count = 0;\n\twhile(s[i] != '\\0')\n\t{\n\t\tif(islower(s[i]) != 0)\n\t\t{\n\t\t\ts[i] = toupper(s[i]);\n\t\t} else if(isupper(s[i]) != 0)\n\t\t{\n\t\t\ts[i] = tolower(s[i]);\n\t\t} else\n\t\t{\n\t\t\tcount++;\n\t\t}\n\t\ti++;\n\t}\n\treturn count;\n}", + "tests" : {} + }, + { + "type": "[a02] Iteration", + "code": "#include <stdlib.h>\nlong iteP(unsigned int k)\n{\n\tif(k == 0)\n\t{\n\t\treturn 3;\n\t}\n\tif( k == 1)\n\t{\n\t\treturn 0;\n\t}\n\tif( k == 3)\n\t{\n\t\treturn 2;\n\t}\n\tint *p = malloc(k * sizeof(int));\n\tp[0] = 3;\n\tp[1] = 0;\n\tp[2] = 2;\n\tfor(int i = 3; i < k; i++)\n\t{\n\t\tp[i] = p[i-2] + p[i - 3];\n\t}\n\n\treturn p[k];\n}", + "tests" : {} + } + ] + } +} + +after = { + "username": { + "name": "87654321", + "email": "username@example.org", + "matrikel_no": "87654321", + "submissions": [ + { + "type": "[a01] Zeichen und Strings", + "code": "#include <ctype.h>\nint umschalt(char *s)\n\n{\n\tint i = 0, count = 0;\n\twhile(s[i] != '\\0')\n\t{\n\t\tif(islower(s[i]) != 0)\n\t\t{\n\t\t\ts[i] = toupper(s[i]);\n\t\t} else if(isupper(s[i]) != 0)\n\t\t{\n\t\t\ts[i] = tolower(s[i]);\n\t\t} else\n\t\t{\n\t\t\tcount++;\n\t\t}\n\t\ti++;\n\t}\n\treturn count;\n}", + "tests": { + "EmptyTest": { + "name": "EmptyTest", + "annotation": "", + "label": "NOT_EMPTY" + } + } + }, + { + "type": "[a02] Iteration", + "code": "#include <stdlib.h>\nlong iteP(unsigned int k)\n{\n\tif(k == 0)\n\t{\n\t\treturn 3;\n\t}\n\tif( k == 1)\n\t{\n\t\treturn 0;\n\t}\n\tif( k == 3)\n\t{\n\t\treturn 2;\n\t}\n\tint *p = malloc(k * sizeof(int));\n\tp[0] = 3;\n\tp[1] = 0;\n\tp[2] = 2;\n\tfor(int i = 3; i < k; i++)\n\t{\n\t\tp[i] = p[i-2] + p[i - 3];\n\t}\n\n\treturn p[k];\n}", + "tests": {} + } + ] + } +} + + +def sha(s): + return hashlib.sha1(s.encode()).hexdigest() + + +def run_cmd(cmd, stdin=None): + return subprocess.run( + cmd, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + input=stdin, + shell=True, + encoding='utf-8', + timeout=0.5 + ) + + +def testcase(i, args, stdout): + try: + ret = run_cmd("./code %s" % args) + assert ret.stdout == stdout + except AssertionError: + return f"Case #{i}: [ASSERT FAILED] ./umschalt {args} WAS '{ret.stdout}' SHOULD '{stdout}'" + except subprocess.CalledProcessError: + return f"Case #{i}: [FAILED] ./umschalt {args}" + except subprocess.TimeoutExpired: + return f"Case #{i}: [TIMEOUT] ./umschalt {args}" + else: + return f"Case #{i}: [SUCCESS] ./umschalt {args}" + + +def all_subclasses(cls): + return cls.__subclasses__() \ + + [g for s in cls.__subclasses__() for g in all_subclasses(s)] + + +def sha1(submission_obj): + return hashlib.sha1(submission_obj['code'].encode()).hexdigest() + + +class Test(metaclass=abc.ABCMeta): + """docstring for IliasQuestion""" + + @classmethod + def available_tests(cls): + return {sub.__name__ : sub for sub in all_subclasses(cls)} + + def __new__(cls, *args, **kwargs): + assert hasattr(cls, 'depends'), "depends not defined" + assert hasattr(cls, 'label_success'), "label_success not defined" + assert hasattr(cls, 'label_failure'), "label_failure not defined" + return super().__new__(cls) + + def __init__(self, submission_obj, **kwargs): + + if not self.dependencies_satisfied(submission_obj): + self.result = self.label_failure + self.annotation = "TEST DEPENDENCY NOT MET" + return + + elif str(self) in submission_obj['tests']: + self.deserialize(submission_obj['tests'][str(self)]) + + else: + self.result, self.annotation = self.run_test( + submission_obj, **kwargs) + self.serialize(submission_obj) + + def __bool__(self): + return self.result + + def __str__(self): + return self.__class__.__name__ + + def dependencies_satisfied(self, submission_obj): + return all(dep(submission_obj).result for dep in self.depends) + + def deserialize(self, test): + self.result = test['label'] == self.label_success + self.annotation = test['annotation'] + + def serialize(self, submission_obj): + as_dict = { + 'name' : str(self), + 'annotation' : self.annotation + } + + if self.result: + as_dict['label'] = self.label_success + else: + as_dict['label'] = self.label_failure + + submission_obj['tests'][str(self)] = as_dict + + @abc.abstractmethod + def run_test(self, submission_obj) -> (bool, str): + return NotImplemented + + +class EmptyTest(Test): + """docstring for EmptyTest""" + + depends = () + label_success = 'NOT_EMPTY' + label_failure = 'EMPTY' + + def run_test(self, submission_obj): + return bool(submission_obj['code'].strip()), "" + + +class CompileTest(Test): + + depends = (EmptyTest, ) + label_success = 'COMPILATION_SUCCESSFUL' + label_failure = 'COMPILATION_FAILED' + + def run_test(self, submission_obj): + + try: + ret = run_cmd("gcc-7 -c -xc -o code.o -", submission_obj['code']) + except subprocess.CalledProcessError as err: + print('[FATAL] The compiler failed.') + + return not ret.returncode, ret.stderr + + +class LinkTest(Test): + + depends = (CompileTest, ) + label_success = 'LINKING_SUCCESSFUL' + label_failure = 'LINKING_FAILED' + + def run_test(self, submission_obj): + + ret = run_cmd("gcc-7 -o code a01-testing.o code.o") + return not ret.returncode, ret.stderr + + +class UnitTestTest(Test): + """docstring for UnitTestTest""" + + depends = (LinkTest, ) + label_success = 'UNITTEST_SUCCSESSFUL' + label_failure = 'UNITTEST_FAILED' + + def run_test(self, submission_obj): + + test_cases = ( + ('', 'umschalt(NULL)\n== 0\n'), + ('...', 'input : "..."\numschalt("...")\n== 3\nresult: "..."\n'), + ('anV', 'input : "anV"\numschalt("anV")\n== 0\nresult: "ANv"\n'), + ) + + return 1, '\n'.join(testcase(i, *test) for i, test in enumerate(test_cases)) + + +path = tempfile.mkdtemp() +run_cmd(f'cp ../data/testing_facility/klausur_tag_02/objects/* {path}') +os.chdir(path) + +d = UnitTestTest(before["username"]['submissions'][0]) + +# shutil.rmtree(path) + +print(path) +print(d.annotation) + + +with open("/dev/stdout", "w") as out: + out.write(json.JSONEncoder().encode(before)) diff --git a/util/testcases.py b/util/testcases.py new file mode 100644 index 0000000000000000000000000000000000000000..87e24ed44e7cef2ea4f45315d21d2fcf230dfec2 --- /dev/null +++ b/util/testcases.py @@ -0,0 +1,78 @@ +r = '''-- [a01] Zeichen und Strings +USAGE: ./bin/a01 <string> <character> +-- [a02] Iteration +USAGE: ./bin/a02 <unsigned integer> +-- [a03] Rekursion +USAGE: ./bin/a03 <integer> <integer> <integer> <integer> +-- [a04] Strukturen I +USAGE: ./bin/a04 <integer> <integer> ... <integer> <integer> +-- [a05] Strukturen II +NO EXECUTABLE +-- [a06] Speicher auf dem Heap +USAGE: ./bin/a06 <integer> <integer> ... <integer> <integer> +''' + +import re +import random +from string import ascii_letters, digits + +types = ('integer', 'unsigned_integer', 'character', 'string') +list_sep = '...' + +def call_function(name: str, *args, **kwargs): + return globals()[name](*args, **kwargs) + +def integer(bounds=1000): + return random.randint(-bounds, bounds) + +def unsigned_integer(bounds=1000): + return random.randint(0, bounds) + +def character(): + return random.choice(5*ascii_letters + 2*digits + '%*+,-./:?@[]^_{}~') + +def string(lenght=31): + return ''.join(character() for i in range(unsigned_integer(lenght))) + +def type_list(_type): + def generic_list(): + return ' '.join(str(call_function(_type)) for i in range(unsigned_integer(30))) + return generic_list + +def rubbish(): + return ' '.join(string(4)) + +for t in types: + globals()[t + '_list'] = type_list(t) # I fucking love it + + +task = re.compile(r'^-- (?P<title>.*)\n(USAGE: (?P<cmd>[\./\w]+) (?P<syntax>.*)|NO EXECUTABLE)', re.MULTILINE) +args = re.compile(rf"<({'|'.join(types)}|{'|'.join(t + '_list' for t in types)})>") + + +def argument_generator(syntax): + syntax, _ = re.subn(r'<([\w\s]+)> <\1> \.\.\. <\1> <\1>', r'<\1_list>', syntax) + syntax, _ = re.subn(r'<(\w+)\s(\w+)>', r'<\1_\2>', syntax) + + return ' '.join(str(call_function(arg)) for arg in re.findall(args, syntax)) + +def testcases_generator(task, n=10): + syntax = task.group('syntax') + + if not syntax: + return + + yield '' + yield 0 + + for i in range(n//3): + yield rubbish() + + for i in range(n): + yield argument_generator(syntax) + +for task in re.finditer(task, r): + for t in testcases_generator(task): + print(t) + +