diff --git a/setup.py b/setup.py index ef0bd7193b74d48b24dc179a256649a75077a388..7a0d08d83ee37bc234d1367a23cdb50f86d8c760 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ with open("README.md", "r", encoding="utf-8") as fh: # beamer-slider setuptools.setup( name="coursebox", - version="0.1.18.0", + version="0.1.18.2", author="Tue Herlau", author_email="tuhe@dtu.dk", description="A course management system currently used at DTU", diff --git a/src/coursebox.egg-info/PKG-INFO b/src/coursebox.egg-info/PKG-INFO index 9a8c93d4ce89a230ecf22b5a8e35d95000ef8566..6c31fbb9b73e78b53fe8367d14d6c9e5d6a9aec1 100644 --- a/src/coursebox.egg-info/PKG-INFO +++ b/src/coursebox.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: coursebox -Version: 0.1.17.12 +Version: 0.1.18.2 Summary: A course management system currently used at DTU Home-page: https://lab.compute.dtu.dk/tuhe/coursebox Author: Tue Herlau diff --git a/src/coursebox.egg-info/SOURCES.txt b/src/coursebox.egg-info/SOURCES.txt index a52feee8aeb68fe04b31153f4833158250d6589b..4941d7b6511f261f67a98c8796a31787b27886c4 100644 --- a/src/coursebox.egg-info/SOURCES.txt +++ b/src/coursebox.egg-info/SOURCES.txt @@ -21,4 +21,6 @@ src/coursebox/core/projects_plagiarism.py src/coursebox/material/__init__.py src/coursebox/material/homepage_lectures_exercises.py src/coursebox/material/lecture_questions.py -src/coursebox/material/snipper.py \ No newline at end of file +src/coursebox/material/snipper.py +src/coursebox/testing/__init__.py +src/coursebox/testing/testing.py \ No newline at end of file diff --git a/src/coursebox/__init__.py b/src/coursebox/__init__.py index e5cdb65e6cd3e5dec9a640c4cbce3eebcfe51046..7648fd4ab812fe50071f72882a594bd36fdc5c5b 100644 --- a/src/coursebox/__init__.py +++ b/src/coursebox/__init__.py @@ -3,4 +3,6 @@ # conf = "point to main caller file that defines year, semester, etc." from coursebox.setup_coursebox import setup_coursebox - +from coursebox.core.info_paths import get_paths +from coursebox.core.info import class_information +# from coursebox.core import info_paths diff --git a/src/coursebox/core/info.py b/src/coursebox/core/info.py index 5addb236bfa10febd2c67144b963ed9ba2943d3b..b541f5e2f171e5efb895b6312c989a8ee9e43237 100644 --- a/src/coursebox/core/info.py +++ b/src/coursebox/core/info.py @@ -431,7 +431,7 @@ def class_information(verbose=False, n = l['number'] date = l['date'] - dd = timedelta(days=l['show_solutions_after']) + dd = timedelta(days=l.get('show_solutions_after', 1)) d['release_rules'][str(n)] = dict(start=date+dd, end=date+timedelta(days=2000)) if update_with_core_conf: diff --git a/src/coursebox/testing/__init__.py b/src/coursebox/testing/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/coursebox/testing/testing.py b/src/coursebox/testing/testing.py new file mode 100644 index 0000000000000000000000000000000000000000..83de18f8612604004ce7e5412756f5bd4d1d3ea6 --- /dev/null +++ b/src/coursebox/testing/testing.py @@ -0,0 +1,344 @@ +import subprocess +import time +from collections import defaultdict +import glob +import tabulate +import sys +import warnings + +def check_by_grade_script(student_dir, module): + """ + TH 2023: Must be runable on a minimal system, i.e. no unitgrade_private etc. + + :param student_dir: + :param module: + :return: + """ + import sys + # from unitgrade.utils import + # from unitgrade_private.run import run # Don't use tihs as it is not available during test. + + cmd = f"cd {student_dir} && {sys.executable} -m {module}" + + # output = subprocess.check_output(f"cd {student_dir} && {sys.executable} -m {module}", shell=True, check=False) + from subprocess import run + print("Running command", cmd) + p = run(cmd, capture_output=True, check=False, shell=True) + print('exit status:', p.returncode) + out = p.stdout.decode() + stderr = p.stderr.decode() + # process = run(cmd, print_output=False, check=False) + # stderr = process.stderr.getvalue().strip() + if p.returncode != 0 and not (stderr == "Killed" or stderr == ""): + # print(.stdout.getvalue()) + print(out) + print("-"*50) + print(stderr) + print("-"*50) + raise Exception("Run command gave error: " + cmd) + # out = process.stdout.getvalue() + # s = out[out.rfind("handin"):out.rfind(".token")] + # try: + # p = int(s.split("_")[1]) + # total = int(s.split("_")[-1]) + # except Exception as e: + # print("Encountered problem in", module) + # print(out) + # print(stderr) + # print(s) + # print(cmd) + # raise e + # out = out.decode("utf-8") + # print(out) + s = out[out.rfind("handin"):out.rfind(".token")] + # try: + # p = int(s.split("_")[1]) + # total = int(s.split("_")[-1]) + + a, _, b = s.split("_")[-3:] + + points = int(a), int(b) + + # total = [l for l in out.splitlines() if l.strip().startswith("Total")].pop() + # total.split(" ")[1].split("/") + # points = total.split(" ")[-1] + # a, b = points.split("/") + # points = int(a), int(b) + # points = + return points, out + + + +def check_py_script(student_dir, module): + cmd = f"cd {student_dir} && {sys.executable} -m {module} --unmute" # Don't hardcode 'python' bc. of 'python3' on mac/Linux. + try: + output = subprocess.check_output(cmd, shell=True) + except Exception as e: + print("command that failed was") + print(cmd) + # print("The std. out from the command was") + # print(output.decode("utf-8")) + raise e + + out = output.decode("utf-8") + total = [l for l in out.splitlines() if l.strip().startswith("Total")].pop() + total.split(" ")[1].split("/") + points = total.split(" ")[-1] + a, b = points.split("/") + points = int(a), int(b) + return points, out + +# def check_by_grade_script(student_dir, module): +# output = subprocess.check_output(f"cd {student_dir} && {sys.executable} -m {module}", shell=True) +# out = output.decode("utf-8") +# s = out[out.rfind("handin"):out.rfind(".token")] +# try: +# p = int(s.split("_")[1]) +# total = int(s.split("_")[-1]) +# except Exception as e: +# print("Encountered problem in", module) +# print(out) +# print(s) +# raise e +# return p, total, out + +def check_pyhon_documentation_in_student_repo(student_dir_complete=None, package="cp"): + from interrogate import coverage + if student_dir_complete is None: + # It is important to not import cp_box here during CI/CD. The coursebox packages is not/should not be installed. + from cp_box import cp_main + from coursebox.core.info_paths import get_paths + paths = get_paths() + student_dir_complete = paths['02450students'] + "_complete" + # At this point, also set up the studnets_complete repo. + from cp_box.material.build_documentation import deploy_students_complete + deploy_students_complete() + + from pydocstyle.checker import check + n = 0 + files_ = glob.glob(f"{student_dir_complete}/{package}/ex*/*.py", recursive=True) + glob.glob(f"{student_dir_complete}/{package}/project*/*.py", recursive=True) + files_ = [f for f in files_ if not f.endswith("_grade.py")] + files = [] + for f in files_: + with open(f, "r") as ff: + s = ff.read().splitlines() + if len([l for l in s if l.startswith("class ") and "(Report):" in l]) > 0: + print("Skipping report", f) + continue + files.append(f) + + def _darglint_check(filename): + import darglint + from darglint import analysis + from darglint.config import get_config + from darglint.config import DocstringStyle + + config = get_config() + # config = get_config() + from darglint.driver import parser, get_error_report + args = parser.parse_args() + config.style = DocstringStyle.SPHINX + # pass + + raise_errors_for_syntax = args.raise_syntax or False + # for filename in files: + # filename = student_dir_base + "/cp/project0/fruit_homework.py" + error_report = get_error_report( + filename, + args.verbosity, + raise_errors_for_syntax, + message_template=args.message_template, + ) + # print(error_report) + + return error_report.splitlines() + + + + + def _pydocstyle_check(filename): + all_errs = [] + n = 0 + + o = check([filename]) + errs = [k for k in o] + for err in errs: + # print(f"{f}> ", err) + all_errs.append(str(err)) + n += len(errs) + # if n > 0: + # print("Total problems", n) + return all_errs + + print("="*80) + print("Summary of documentation style errors") + print("=" * 80) + + n = 0 + for file in files: + + errs1 = _darglint_check(file) + errs2 = _pydocstyle_check(file) + + if len(errs2) + len(errs1) > 0: + print(f"{file}> errors were:") + print(" - \n".join(errs1)) + print(" * \n".join(errs2)) + n += len(errs1) + len(errs2) + print("Total errors", n) + return n + + +def _run_student_tests(student_dir_base=None, weeks=None, projects=None, fail_if_no_projects=True, + fail_if_no_weeks=True): + """ + TODO: Refactor this function to accept full module paths of tests as input, and move the cp.* specific stuff out. Possibly alternative is to automatically search for tests + using conventions. The function should ultimately be moved to coursebox. + """ + + # still got that common module. Eventually this should be an argument (probably). + from cp_box.common import projects_all + from cp_box.common import weeks_all + + if projects is None: + projects = projects_all + else: + projects = {k: v for k, v in projects_all.items() if k in projects} + + if weeks is None: + weeks = weeks_all + else: + weeks = {k: v for k, v in weeks_all.items() if k in weeks} + + # if projects is None: + # from coursebox.core.info_paths import core_conf + # projects = list(core_conf['projects_all'].keys()) + + # if weeks is None: + # weeks_all = core_conf['weeks_all'] + # weeks = weeks_all + # else: + # pass + # weeks = {k: weeks_all[k] for k in weeks} + + if student_dir_base is None: + """ Only import this sometimes to avoid messing up the paths """ + + from coursebox.core.info_paths import get_paths + paths = get_paths() + student_dir_base = paths['02450students'] + + bases = {k: projects[k]['module_public'] for k in + projects} # f"cp.project{k}.project{k}_tests" for k in projects if True} + + bases_weekly = [weeks[k]['module_public'] for k in weeks] # f'cp.tests.tests_week{k:02d}' for k in weeks] + + if fail_if_no_weeks and len(bases_weekly) == 0: + raise Exception("No weeks found. Bad configuration.") + + if fail_if_no_projects and len(bases) == 0: + raise Exception("No projects found. Bad configuration.") + + # bases_weekly, bases = get_test_imports(weeks, projects) + rs = {} + for censor_files in [False, True]: + # if not censor_files: + # continue + student_dir = student_dir_base if censor_files else student_dir_base + "_complete" + for project_id, base in bases.items(): + # student_dir = paths['02450students'] + # print(">>> Checking tests...", base, "censor?", censor_files) + t0 = time.time() + (p1, t1), output1 = check_py_script(student_dir, base) + time1 = time.time() - t0 + # print(">>> Checking grade script...") + t0 = time.time() + if base.endswith("_tests"): + base_grade = base[:-len("_tests")] + "_grade" + else: + base_grade = base + "_grade" + + (p2, t2), output2 = check_by_grade_script(student_dir, base_grade) + time2 = time.time() - t0 + if not censor_files: + tokens = glob.glob(student_dir + "/" + "/".join(base.split(".")[:-1]) + "/*.token") + assert len(tokens) == 1 + + rs[(base, censor_files)] = {'p1': p1, 'p2': p2, 't1': t1, 't2': t2, 'time1': time1, 'time2': time2} + """ + These values reflect + p1: Obtained points by project.py-script + t1: total points by project.py-script + + p2: obtained points by project_grade.py-script + t2: Total points by project_grade.py-script + """ + if p1 != p2: + print(output1) + print("Obtained (i.e., from current code) points differ:", p1, p2) + assert False + if t1 != t2: + print("Total (obtainable) points differ:", t1, t2) + assert False + + assert (t1 > 0) + if censor_files: + if p1 != 0: + print("Ran tests with code missing. The student should get 0 points, but instead got: ", p1, "of", + t1) + print(rs[(base, censor_files)]) + + assert p1 == 0 + assert p2 == 0 + else: + if p1 != t1: + print(base, "Wrong number of obtained points by regular check script. Output from script is:") + print(p1, t1) + print(output1) + assert False + if p2 != t2: + print(base, "Wrong number of obtained points by grade script. Output from grade script is:") + print(p2, t2) + print(output2) + assert False + + for base in bases_weekly: + # if not weekly_tests: + # continue + # print(">>> Checking tests...", base) + t0 = time.time() + (p1, t1), output = check_py_script(student_dir, base) + time1 = time.time() - t0 + rs[(base, censor_files)] = {'p1': p1, 't1': t1, 'time1': time1} + assert t1 > 0 + if censor_files: + if p1 != 0: + print(p1, t1, base, "censor_files =", censor_files) + print(output) + assert p1 == 0 + else: + if p1 != t1: + # import warnings + print("=" * 50) + print("Check of student files when files are NOT censored.") + print("p1, t1 are", p1, t1) + print("Base is", base) + print("Failed check for p1 == p2") + print(output) + print(p1, t1, base, censor_files) + print("=" * 50) + + assert p1 == t1 + + print("Main check completed") + dd = defaultdict(list) + for (k, mode), val in rs.items(): + # print(k, mode, val) + dd['Test'].append(k) + dd['censored'].append(mode) + dd['Points obtained'].append(val['p1']) + dd['Points total'].append(val['t1']) + dd['Time taken'].append(int(val['time1'])) + + print(tabulate.tabulate(dd, headers='keys')) +