From 36f5bf704eaf2ce4da75874fe4965e365d137646 Mon Sep 17 00:00:00 2001 From: Tue Herlau <tuhe@dtu.dk> Date: Fri, 16 Sep 2022 14:30:51 +0200 Subject: [PATCH] Updated documentation + backend to enable the dashboard --- README.md | 149 +++++++++----- devel/example_devel/instructor/cache.db | Bin 32768 -> 32768 bytes devel/example_devel/instructor/cache.db-wal | Bin 556232 -> 0 bytes .../instructor/cs108/report_devel_grade.py | 4 +- .../AnotherTest-test_even_more.json | 1 - .../AnotherTest-test_even_more.json.lock | 0 .../unitgrade_data/AnotherTest-test_more.json | 1 - .../AnotherTest-test_more.json.lock | 0 .../cs108/unitgrade_data/AnotherTest.pkl | Bin 157 -> 157 bytes .../unitgrade_data/Numpy-setUpClass.json | 1 - .../unitgrade_data/Numpy-setUpClass.json.lock | 0 .../cs108/unitgrade_data/Numpy-test_bad.json | 1 - .../unitgrade_data/Numpy-test_bad.json.lock | 0 .../unitgrade_data/Numpy-test_weights.json | 1 - .../Numpy-test_weights.json.lock | 0 .../instructor/cs108/unitgrade_data/Numpy.pkl | Bin 220 -> 553 bytes .../instructor/cs108/unitgrade_data/cache.db | Bin 32768 -> 45056 bytes .../cs108/unitgrade_data/cache.db-shm | Bin 32768 -> 0 bytes .../cs108/unitgrade_data/cache.db-wal | Bin 1371992 -> 0 bytes .../main_config_report_devel.artifacts.pkl | Bin 1035 -> 1374 bytes ...ain_config_report_devel.artifacts.pkl.lock | 0 .../main_config_report_devel.json | 1 - .../main_config_report_devel.json.lock | 0 .../unitgrade_data/report_devel.json.lock | 0 docs/README.jinja.md | 28 +++ docs/snips/0_homework1.py | 6 +- docs/snips/deploy.txt | 64 +++++- docs/snips/deploy_autolab_a.py | 10 +- docs/snips/deploy_autolab_b.py | 18 +- docs/snips/homework1.py | 7 +- docs/snips/report1_all.py | 7 +- docs/snips/report2.py | 10 +- docs/snips/report2_b.py | 30 +-- docs/snips/report2_c.py | 30 +-- docs/unitgrade_devel.bib | 4 +- .../instructor/week5/unitgrade_data/cache.db | Bin 32768 -> 40960 bytes .../cs102/Report2_handin_3_of_16.token | 188 ++++++++++++++++++ requirements.txt | 2 +- setup.py | 2 +- src/unitgrade_private/hidden_create_files.py | 14 +- 40 files changed, 445 insertions(+), 134 deletions(-) delete mode 100644 devel/example_devel/instructor/cache.db-wal delete mode 100644 devel/example_devel/instructor/cs108/unitgrade_data/AnotherTest-test_even_more.json delete mode 100755 devel/example_devel/instructor/cs108/unitgrade_data/AnotherTest-test_even_more.json.lock delete mode 100644 devel/example_devel/instructor/cs108/unitgrade_data/AnotherTest-test_more.json delete mode 100755 devel/example_devel/instructor/cs108/unitgrade_data/AnotherTest-test_more.json.lock delete mode 100644 devel/example_devel/instructor/cs108/unitgrade_data/Numpy-setUpClass.json delete mode 100755 devel/example_devel/instructor/cs108/unitgrade_data/Numpy-setUpClass.json.lock delete mode 100644 devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_bad.json delete mode 100755 devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_bad.json.lock delete mode 100644 devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_weights.json delete mode 100755 devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_weights.json.lock delete mode 100644 devel/example_devel/instructor/cs108/unitgrade_data/cache.db-shm delete mode 100644 devel/example_devel/instructor/cs108/unitgrade_data/cache.db-wal delete mode 100755 devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.artifacts.pkl.lock delete mode 100644 devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.json delete mode 100755 devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.json.lock delete mode 100755 devel/example_devel/instructor/cs108/unitgrade_data/report_devel.json.lock rename devel/example_devel/instructor/cache.db-shm => examples/02631/instructor/week5/unitgrade_data/cache.db (69%) create mode 100644 examples/example_framework/instructor/cs102/Report2_handin_3_of_16.token diff --git a/README.md b/README.md index 34a612b..4aa0623 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Unitgrade is an automatic report and exam evaluation framework that enables inst - Instructors can automatically verify the students solution using a Docker VM and run hidden tests - Automatic Moss anti-plagiarism detection - CMU Autolab integration (Experimental) + - A live dashboard which shows the outcome of the tests ### Install Simply use `pip` @@ -30,6 +31,7 @@ The figure shows an overview of the workflow. - You write exercises and a suite of unittests. - They are then compiled to a version of the exercises without solutions. - The students solve the exercises using the tests and when they are happy, they run an automatically generated `_grade.py`-script to produce a `.token`-file with the number of points they obtain. This file is then uploaded for further verification/evaluation. + - The students can see their progress and review hints using the dashboard (see below) ### Videos Videos where I try to talk and code my way through the examples can be found on youtube: @@ -64,7 +66,7 @@ instructor/cs101/deploy.py # A private file to deploy the tests ### The homework The homework is just any old python code you would give to the students. For instance: ```python -# example_simplest/instructor/cs101/homework1.py +# autolab_example_py_upload/instructor/cs102_autolab/homework1.py def reverse_list(mylist): #!f """ Given a list 'mylist' returns a list consisting of the same elements in reverse order. E.g. @@ -75,10 +77,9 @@ def reverse_list(mylist): #!f def add(a,b): #!f """ Given two numbers `a` and `b` this function should simply return their sum: > add(a,b) = a+b """ - return a+b + return a+b*2 -if __name__ == "__main__": - # Example usage: +if __name__ == "__main__": # Example usage: print(f"Your result of 2 + 2 = {add(2,2)}") print(f"Reversing a small list", reverse_list([2,3,5,7])) ``` @@ -119,7 +120,12 @@ class Report1(Report): pack_imports = [cs101] # Include all .py files in this folder if __name__ == "__main__": - evaluate_report_student(Report1()) + # from HtmlTestRunner import HTMLTestRunner + import HtmlTestRunner + unittest.main(testRunner=HtmlTestRunner.HTMLTestRunner(output='example_dir')) + + + # evaluate_report_student(Report1()) ``` ### Deployment @@ -168,6 +174,32 @@ This runs an identical set of tests and produces the file `Report1_handin_10_of_ - You can easily use the framework to include output of functions. - See below for how to validate the students results + +### Viewing the results using the dashboard +I recommend to monitor and run the tests from the IDE, as this allows you to use the debugger in conjunction with your tests. +However, unitgrade comes with a dashboard that allows students to see the outcome of individual tests + and what is currently recorded in the `token`-file. To start the dashboard, they should simply run the command +``` +unitgrade +``` +from a directory that contains a test (the directory will be searched recursively for test files). + The command will start a small background service and open a webpage: + + + +Features supported in the current version: + - Shows which files need to be edited to solve the problem + - Collect hints given in the homework files and display them for the relevant tests + - fully responsive -- the UI, including the terminal, will update while the test is running regardless of where you launch the test + - Allows students to re-run tests from the UI + - Shows current test status and results captured in `.token`-file + - Tested on Windows/Linux + - Frontend is pure javascript and the backend only depends on python packages. + +The frontend is automatically enabled the moment your classes inherits from the `UTestCase`-class; no configuration files required, and there are no known bugs. + +Note the frontend is currently not provided in the pypi `unitgrade` package, but only through the gitlab repository (install using `git clone` and then `pip install -e ./`) -- it seems ready, but I want to test it on mac and a few more systems before publishing it. + ## How safe is Unitgrade? There are three principal ways of cheating: - Break the framework and submit a `.token` file that 'lies' about the true number of points @@ -197,13 +229,19 @@ One of the main advantages of `unitgrade` over web-based autograders it that tes # example_framework/instructor/cs102/report2.py from unitgrade import UTestCase, cache + + class Week1(UTestCase): + @classmethod + def setUpClass(cls) -> None: + a = 234 + def test_add(self): self.assertEqualC(add(2,2)) self.assertEqualC(add(-100, 5)) - def test_reverse(self): - self.assertEqualC(reverse_list([1, 2, 3])) + # def test_reverse(self): + # self.assertEqualC(reverse_list([1, 2, 3])) ``` Note we have changed the test-function to `self.assertEqualC` (the `C` is for cache) and dropped the expected result. What `unitgrade` will do is to evaluate the test *on the working version of the code*, compute the results of the test, @@ -213,21 +251,21 @@ is to evaluate the test *on the working version of the code*, compute the result Titles can be set either using python docstrings or programmatically. An example: ```python # example_framework/instructor/cs102/report2.py -class Week1Titles(UTestCase): - """ The same problem as before with nicer titles """ - def test_add(self): - """ Test the addition method add(a,b) """ - self.assertEqualC(add(2,2)) - print("output generated by test") - self.assertEqualC(add(-100, 5)) - # self.assertEqual(2,3, msg="This test automatically fails.") - - def test_reverse(self): - ls = [1, 2, 3] - reverse = reverse_list(ls) - self.assertEqualC(reverse) - # Although the title is set after the test potentially fails, it will *always* show correctly for the student. - self.title = f"Checking if reverse_list({ls}) = {reverse}" # Programmatically set the title +# class Week1Titles(UTestCase): +# """ The same problem as before with nicer titles """ +# def test_add(self): +# """ Test the addition method add(a,b) """ +# self.assertEqualC(add(2,2)) +# print("output generated by test") +# self.assertEqualC(add(-100, 5)) +# # self.assertEqual(2,3, msg="This test automatically fails.") +# +# def test_reverse(self): +# ls = [1, 2, 3] +# reverse = reverse_list(ls) +# self.assertEqualC(reverse) +# # Although the title is set after the test potentially fails, it will *always* show correctly for the student. +# self.title = f"Checking if reverse_list({ls}) = {reverse}" # Programmatically set the title ``` When this is run, the titles are shown as follows: ```terminal @@ -236,7 +274,7 @@ When this is run, the titles are shown as follows: | | | |_ __ _| |_| | \/_ __ __ _ __| | ___ | | | | '_ \| | __| | __| '__/ _` |/ _` |/ _ \ | |_| | | | | | |_| |_\ \ | | (_| | (_| | __/ - \___/|_| |_|_|\__|\____/_| \__,_|\__,_|\___| v0.1.17, started: 19/05/2022 15:14:09 + \___/|_| |_|_|\__|\____/_| \__,_|\__,_|\___| v0.1.22, started: 15/06/2022 09:18:15 CS 102 Report 2 Question 1: Week1 @@ -250,9 +288,10 @@ Question 2: The same problem as before with nicer titles * q2.2) Checking if reverse_list([1, 2, 3]) = [3, 2, 1]............................................................PASS * q2) Total...................................................................................................... 6/6 -Total points at 15:14:09 (0 minutes, 0 seconds)....................................................................16/16 +Total points at 09:18:16 (0 minutes, 0 seconds)....................................................................16/16 Including files in upload... +path.: _NamespacePath(['C:\\Users\\tuhe\\Documents\\unitgrade_private\\examples\\example_framework\\instructor\\cs102', 'C:\\Users\\tuhe\\Documents\\unitgrade_private\\examples\\example_framework\\instructor\\cs102']) * cs102 > Testing token file integrity... Done! @@ -267,21 +306,21 @@ What happens behind the scenes when we set `self.title` is that the result is pr The `@cache`-decorator offers a direct ways to compute the correct result on an instructors computer and submit it to the student. For instance: ```python # example_framework/instructor/cs102/report2.py -class Question2(UTestCase): - @cache - def my_reversal(self, ls): - # The '@cache' decorator ensures the function is not run on the *students* computer - # Instead the code is run on the teachers computer and the result is passed on with the - # other pre-computed results -- i.e. this function will run regardless of how the student happens to have - # implemented reverse_list. - return reverse_list(ls) - - def test_reverse_tricky(self): - ls = (2,4,8) - ls2 = self.my_reversal(tuple(ls)) # This will always produce the right result, [8, 4, 2] - print("The correct answer is supposed to be", ls2) # Show students the correct answer - self.assertEqualC(reverse_list(ls)) # This will actually test the students code. - return "Buy world!" # This value will be stored in the .token file +# class Question2(UTestCase): +# @cache +# def my_reversal(self, ls): +# # The '@cache' decorator ensures the function is not run on the *students* computer +# # Instead the code is run on the teachers computer and the result is passed on with the +# # other pre-computed results -- i.e. this function will run regardless of how the student happens to have +# # implemented reverse_list. +# return reverse_list(ls) +# +# def test_reverse_tricky(self): +# ls = (2,4,8) +# ls2 = self.my_reversal(tuple(ls)) # This will always produce the right result, [8, 4, 2] +# print("The correct answer is supposed to be", ls2) # Show students the correct answer +# self.assertEqualC(reverse_list(ls)) # This will actually test the students code. +# return "Buy world!" # This value will be stored in the .token file ``` The `@cache` decorator will make sure the output of the function is pre-computed when the test is set up, and that the function will simply return the correct result regardless of the function body. This is very helpful in a few situations: @@ -503,26 +542,30 @@ The code for the example can be found in `examples/autolab_example`. It consists Concretely, the following code will download and build the image (note this code must be run on the same machine that you have installed Autolab on) ```python -# autolab_token_upload/deploy_autolab.py +# autolab_example_py_upload/instructor/cs102_autolab/deploy_autolab.py # Step 1: Download and compile docker grading image. You only need to do this once. - download_docker_images("./docker") # Download docker images from gitlab (only do this once. - dockerfile = f"./docker/docker_tango_python/Dockerfile" - autograde_image = 'tango_python_tue' - compile_docker_image(Dockerfile=dockerfile, tag=autograde_image) # Compile docker image. + download_docker_images("../docker") # Download docker images from gitlab (only do this once). + dockerfile = f"../docker/docker_tango_python/Dockerfile" + autograde_image = 'tango_python_tue2' # Tag given to the image in case you have multiple images. + compile_docker_image(Dockerfile=dockerfile, tag=autograde_image, no_cache=False) # Compile docker image. ``` Next, simply call the framework to compile any `_grade.py`-file into an Autolab-compatible `.tar` file that can be imported from the web interface. The script requires you to specify both the instructor-directory and the directory with the files the student have been handed out (i.e., the same file-system format we have seen earlier). ```python -# autolab_token_upload/deploy_autolab.py +# autolab_example_py_upload/instructor/cs102_autolab/deploy_autolab.py # Step 2: Create the cs102.tar file from the grade scripts. - instructor_base = f"../example_framework/instructor" - student_base = f"../example_framework/students" - output_tar = deploy_assignment("cs102", # Autolab name of assignment (and name of .tar file) + instructor_base = f"." + student_base = f"../../students/cs102_autolab" + + from report2_test import Report2 + # INSTRUCTOR_GRADE_FILE = + output_tar = new_deploy_assignment("cs105h", # Autolab name of assignment (and name of .tar file) INSTRUCTOR_BASE=instructor_base, - INSTRUCTOR_GRADE_FILE=f"{instructor_base}/cs102/report2_grade.py", + INSTRUCTOR_GRADE_FILE=f"{instructor_base}/report2_test_grade.py", STUDENT_BASE=student_base, - STUDENT_GRADE_FILE=f"{student_base}/cs102/report2_grade.py", - autograde_image_tag=autograde_image) + STUDENT_GRADE_FILE=f"{instructor_base}/report2_test.py", + autograde_image_tag=autograde_image, + homework_file="homework1.py") ``` This will produce a file `cs102.tar`. Whereas you needed to build the Docker image on the machine where you are running Autolab, you can build the lab assignments on any computer. ### Step 3: Upload the `.tar` lab-assignment file @@ -548,9 +591,9 @@ and TAs can choose to annotate the students code directly in Autolab -- we are h # Citing ```bibtex @online{unitgrade_devel, - title={Unitgrade-devel (0.1.39): \texttt{pip install unitgrade-devel}}, + title={Unitgrade-devel (0.1.42): \texttt{pip install unitgrade-devel}}, url={https://lab.compute.dtu.dk/tuhe/unitgrade_private}, - urldate = {2022-06-15}, + urldate = {2022-09-16}, month={9}, publisher={Technical University of Denmark (DTU)}, author={Tue Herlau}, diff --git a/devel/example_devel/instructor/cache.db b/devel/example_devel/instructor/cache.db index eba28aab5e607cfee36521a00079738cc07361f5..e9ae36147afd797558b6b77f20a38a305540b7b1 100644 GIT binary patch literal 32768 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 C4HB&Y delta 575 zcma)&J#W)M7{||kocNs9cdj7KONA0og;pVo*c*`88M-i3I$0N&9QY;8ovXH!7d`;; z)-Lc3s&-`|I}#IX7N(3ytQB^erm0xip8x-O{XJ=_r>TCawM~SOLnbJ5+I38(<R|$~ zCfBZWPM%o~G;k=S^rcCmiibi;ljMOEXFj%}hIyPss=l5*@7JD`>TA%0fsh|qC?a+! z0-qIyC`Hvu*6MCHE$A5XC=kkLVd#zKStDQHz}cH_bKiunzDRSA4{y8Ms9J`eweY<} z#q8MQNfMG0xEfD_fCVBHs<L5#YZUu#^~-$jHe_GBPhacM!peKcL^SYEYz<s2m8XPM zg3HRBeuk(?ujmflpg+hTG9x$i8@-?(X-MDFSM)dil|N)cvf18t*6G&|);FQ2{Y$!5 z*o#Z$X~c))IEoWlc?_L9d&zCp;KiN!K2FAw7qBoM_&EF0uLkDIq!s2edB9vM<UO+S t2zE99Sa2och>hYv@Kdm%r7hug8(ixi-p=8XlcP=Pr_WpE;urrb{{mfYr)U5G diff --git a/devel/example_devel/instructor/cache.db-wal b/devel/example_devel/instructor/cache.db-wal deleted file mode 100644 index 34e446fccbfd0910c31bfcdac9d965261a56f7d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 556232 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 diff --git a/devel/example_devel/instructor/cs108/report_devel_grade.py b/devel/example_devel/instructor/cs108/report_devel_grade.py index 3237fe6..3a545e1 100644 --- a/devel/example_devel/instructor/cs108/report_devel_grade.py +++ b/devel/example_devel/instructor/cs108/report_devel_grade.py @@ -488,8 +488,8 @@ def source_instantiate(name, report1_source, payload): -report1_source = '# from unitgrade import hide\n# from unitgrade import utils\n# import os\n# import lzma\n# import pickle\n\n# DONT\'t import stuff here since install script requires __version__\n\n# def cache_write(object, file_name, verbose=True):\n# # raise Exception("bad")\n# # import compress_pickle\n# dn = os.path.dirname(file_name)\n# if not os.path.exists(dn):\n# os.mkdir(dn)\n# if verbose: print("Writing cache...", file_name)\n# with lzma.open(file_name, \'wb\', ) as f:\n# pickle.dump(object, f)\n# if verbose: print("Done!")\n#\n#\n# def cache_exists(file_name):\n# # file_name = cn_(file_name) if cache_prefix else file_name\n# return os.path.exists(file_name)\n#\n#\n# def cache_read(file_name):\n# # import compress_pickle # Import here because if you import in top the __version__ tag will fail.\n# # file_name = cn_(file_name) if cache_prefix else file_name\n# if os.path.exists(file_name):\n# try:\n# with lzma.open(file_name, \'rb\') as f:\n# return pickle.load(f)\n# except Exception as e:\n# print("Tried to load a bad pickle file at", file_name)\n# print("If the file appears to be automatically generated, you can try to delete it, otherwise download a new version")\n# print(e)\n# # return pickle.load(f)\n# else:\n# return None\n\n\n\nimport re\nimport sys\nimport threading\nimport time\nimport lzma\nimport hashlib\nimport pickle\nimport base64\nfrom collections import namedtuple\nfrom io import StringIO\nimport numpy as np\nimport tqdm\nfrom colorama import Fore\nfrom functools import _make_key\n\n_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])\n\n\ndef gprint(s):\n print(f"{Fore.LIGHTGREEN_EX}{s}")\n\n\nmyround = lambda x: np.round(x) # required for obfuscation.\nmsum = lambda x: sum(x)\nmfloor = lambda x: np.floor(x)\n\n\n"""\nClean up the various output-related helper classes.\n"""\nclass Logger(object):\n def __init__(self, buffer, write_to_stdout=True):\n # assert False\n self.terminal = sys.stdout\n self.write_to_stdout = write_to_stdout\n self.log = buffer\n\n def write(self, message):\n if self.write_to_stdout:\n self.terminal.write(message)\n self.log.write(message)\n\n def flush(self):\n # this flush method is needed for python 3 compatibility.\n pass\n\n\nclass Capturing(list):\n def __init__(self, *args, stdout=None, unmute=False, **kwargs):\n self._stdout = stdout\n self.unmute = unmute\n super().__init__(*args, **kwargs)\n\n def __enter__(self, capture_errors=True): # don\'t put arguments here.\n self._stdout = sys.stdout if self._stdout == None else self._stdout\n self._stringio = StringIO()\n if self.unmute:\n sys.stdout = Logger(self._stringio)\n else:\n sys.stdout = self._stringio\n\n if capture_errors:\n self._sterr = sys.stderr\n sys.sterr = StringIO() # memory hole it\n self.capture_errors = capture_errors\n return self\n\n def __exit__(self, *args):\n self.extend(self._stringio.getvalue().splitlines())\n del self._stringio # free up some memory\n sys.stdout = self._stdout\n if self.capture_errors:\n sys.sterr = self._sterr\n\n\nclass Capturing2(Capturing):\n def __exit__(self, *args):\n lines = self._stringio.getvalue().splitlines()\n txt = "\\n".join(lines)\n numbers = extract_numbers(rm_progress_bar(txt))\n self.extend(lines)\n del self._stringio # free up some memory\n sys.stdout = self._stdout\n if self.capture_errors:\n sys.sterr = self._sterr\n\n self.output = txt\n self.numbers = numbers\n\n\ndef rm_progress_bar(txt):\n # More robust version. Apparently length of bar can depend on various factors, so check for order of symbols.\n nlines = []\n for l in txt.splitlines():\n pct = l.find("%")\n ql = False\n if pct > 0:\n i = l.find("|", pct + 1)\n if i > 0 and l.find("|", i + 1) > 0:\n ql = True\n if not ql:\n nlines.append(l)\n return "\\n".join(nlines)\n\n\nclass ActiveProgress():\n def __init__(self, t, start=True, title="my progress bar", show_progress_bar=True, file=None, mute_stdout=False):\n if file == None:\n file = sys.stdout\n self.file = file\n self.mute_stdout = mute_stdout\n self._running = False\n self.title = title\n self.dt = 0.025\n self.n = max(1, int(np.round(t / self.dt)))\n self.show_progress_bar = show_progress_bar\n self.pbar = None\n\n if start:\n self.start()\n\n def start(self):\n if self.mute_stdout:\n import io\n # from unitgrade.utils import Logger\n self._stdout = sys.stdout\n sys.stdout = Logger(io.StringIO(), write_to_stdout=False)\n\n self._running = True\n if self.show_progress_bar:\n self.thread = threading.Thread(target=self.run)\n self.thread.start()\n self.time_started = time.time()\n\n def terminate(self):\n if not self._running:\n print("Stopping a progress bar which is not running (class unitgrade.utils.ActiveProgress")\n pass\n # raise Exception("Stopping a stopped progress bar. ")\n self._running = False\n if self.show_progress_bar:\n self.thread.join()\n if self.pbar is not None:\n self.pbar.update(1)\n self.pbar.close()\n self.pbar = None\n\n self.file.flush()\n\n if self.mute_stdout:\n import io\n # from unitgrade.utils import Logger\n sys.stdout = self._stdout #= sys.stdout\n\n # sys.stdout = Logger(io.StringIO(), write_to_stdout=False)\n\n return time.time() - self.time_started\n\n def run(self):\n self.pbar = tqdm.tqdm(total=self.n, file=self.file, position=0, leave=False, desc=self.title, ncols=100,\n bar_format=\'{l_bar}{bar}| [{elapsed}<{remaining}]\')\n t_ = time.time()\n for _ in range(self.n - 1): # Don\'t terminate completely; leave bar at 99% done until terminate.\n if not self._running:\n self.pbar.close()\n self.pbar = None\n break\n tc = time.time()\n tic = max(0, self.dt - (tc - t_))\n if tic > 0:\n time.sleep(tic)\n t_ = time.time()\n self.pbar.update(1)\n\n\ndef dprint(first, last, nL, extra = "", file=None, dotsym=\'.\', color=\'white\'):\n if file == None:\n file = sys.stdout\n dot_parts = (dotsym * max(0, nL - len(last) - len(first)))\n print(first + dot_parts, end="", file=file)\n last += extra\n print(last, file=file)\n\n\ndef hide(func):\n return func\n\n\ndef makeRegisteringDecorator(foreignDecorator):\n """\n Returns a copy of foreignDecorator, which is identical in every\n way(*), except also appends a .decorator property to the callable it\n spits out.\n """\n\n def newDecorator(func):\n # Call to newDecorator(method)\n # Exactly like old decorator, but output keeps track of what decorated it\n R = foreignDecorator(func) # apply foreignDecorator, like call to foreignDecorator(method) would have done\n R.decorator = newDecorator # keep track of decorator\n # R.original = func # might as well keep track of everything!\n return R\n\n newDecorator.__name__ = foreignDecorator.__name__\n newDecorator.__doc__ = foreignDecorator.__doc__\n return newDecorator\n\n\nhide = makeRegisteringDecorator(hide)\n\n\ndef extract_numbers(txt):\n numeric_const_pattern = r\'[-+]? (?: (?: \\d* \\. \\d+ ) | (?: \\d+ \\.? ) )(?: [Ee] [+-]? \\d+ ) ?\'\n rx = re.compile(numeric_const_pattern, re.VERBOSE)\n all = rx.findall(txt)\n all = [float(a) if (\'.\' in a or "e" in a) else int(a) for a in all]\n if len(all) > 500:\n print(txt)\n raise Exception("unitgrade_v1.unitgrade_v1.py: Warning, too many numbers!", len(all))\n return all\n\n\ndef cache(foo, typed=False):\n """ Magic cache wrapper\n https://github.com/python/cpython/blob/main/Lib/functools.py\n """\n maxsize = None\n def wrapper(self, *args, **kwargs):\n key = (self.cache_id(), ("@cache", foo.__name__, _make_key(args, kwargs, typed)))\n # print(self._cache.keys())\n # for k in self._cache:\n # print(k)\n if not self._cache_contains(key):\n value = foo(self, *args, **kwargs)\n self._cache_put(key, value)\n else:\n value = self._cache_get(key)\n # This appears to be required since there are two caches. Otherwise, when deploy method is run twice,\n # the cache will not be set correctly.\n self._cache_put(key, value)\n return value\n\n return wrapper\n\n\ndef methodsWithDecorator(cls, decorator):\n """\n Returns all methods in CLS with DECORATOR as the\n outermost decorator.\n\n DECORATOR must be a "registering decorator"; one\n can make any decorator "registering" via the\n makeRegisteringDecorator function.\n\n import inspect\n ls = list(methodsWithDecorator(GeneratorQuestion, deco))\n for f in ls:\n print(inspect.getsourcelines(f) ) # How to get all hidden questions.\n """\n for maybeDecorated in cls.__dict__.values():\n if hasattr(maybeDecorated, \'decorator\'):\n if maybeDecorated.decorator == decorator:\n print(maybeDecorated)\n yield maybeDecorated\n\n\n""" Methods responsible for turning a dictionary into a string that can be pickled or put into a json file. """\ndef dict2picklestring(dd):\n """\n Turns a dictionary into a string with some compression.\n\n :param dd:\n :return:\n """\n b = lzma.compress(pickle.dumps(dd))\n b_hash = hashlib.blake2b(b).hexdigest()\n return base64.b64encode(b).decode("utf-8"), b_hash\n\ndef picklestring2dict(picklestr):\n """ Reverse of the above method: Turns the string back into a dictionary. """\n b = base64.b64decode(picklestr)\n hash = hashlib.blake2b(b).hexdigest()\n dictionary = pickle.loads(lzma.decompress(b))\n return dictionary, hash\n\ntoken_sep = "-"*70 + " ..ooO0Ooo.. " + "-"*70\ndef load_token(file_in):\n """ We put this one here to allow loading of token files for the dashboard. """\n with open(file_in, \'r\') as f:\n s = f.read()\n splt = s.split(token_sep)\n data = splt[-1]\n info = splt[-2]\n head = token_sep.join(splt[:-2])\n plain_text=head.strip()\n hash, l1 = info.split(" ")\n data = "".join( data.strip()[1:-1].splitlines() )\n l1 = int(l1)\n dictionary, b_hash = picklestring2dict(data)\n assert len(data) == l1\n assert b_hash == hash.strip()\n return dictionary, plain_text\n\n\n\n## Key/value store related.\n\n\nimport io\nimport sys\nimport time\nimport unittest\nfrom unittest.runner import _WritelnDecorator\nimport numpy as np\n\n\nclass UTextResult(unittest.TextTestResult):\n nL = 80\n number = -1 # HAcky way to set question number.\n show_progress_bar = True\n unmute = False # Whether to redirect stdout.\n cc = None\n setUpClass_time = 3 # Estimated time to run setUpClass in TestCase. Must be set externally. See key (("ClassName", "setUpClass"), "time") in _cache.\n\n def __init__(self, stream, descriptions, verbosity):\n super().__init__(stream, descriptions, verbosity)\n self.successes = []\n\n def printErrors(self) -> None:\n # TODO: Fix here. probably also needs to flush stdout.\n self.printErrorList(\'ERROR\', [(test, res[\'stderr\']) for test, res in self.errors])\n self.printErrorList(\'FAIL\', [(test, res[\'stderr\']) for test, res in self.failures])\n\n def addError(self, test, err):\n super(unittest.TextTestResult, self).addError(test, err)\n err = self.errors[-1][1]\n if hasattr(sys.stdout, \'log\'):\n stdout = sys.stdout.log.readlines() # Only works because we set sys.stdout to a unitgrade.Logger\n else:\n stdout = ""\n self.errors[-1] = (self.errors[-1][0], {\'return\': None,\n \'stderr\': err,\n \'stdout\': stdout\n })\n\n if not hasattr(self, \'item_title_print\'):\n # In case setUpClass() fails with an error the short description may not be set. This will fix that problem.\n self.item_title_print = test.shortDescription()\n if self.item_title_print is None: # In case the short description is not set either...\n self.item_title_print = test.id()\n\n\n self.cc_terminate(success=False)\n\n def addFailure(self, test, err):\n super(unittest.TextTestResult, self).addFailure(test, err)\n err = self.failures[-1][1]\n stdout = sys.stdout.log.readlines() # Only works because we set sys.stdout to a unitgrade.Logger\n self.failures[-1] = (self.failures[-1][0], {\'return\': None,\n \'stderr\': err,\n \'stdout\': stdout\n })\n self.cc_terminate(success=False)\n\n\n def addSuccess(self, test: unittest.case.TestCase) -> None:\n msg = None\n stdout = sys.stdout.log.readlines() # Only works because we set sys.stdout to a unitgrade.Logger\n\n if hasattr(test, \'_get_outcome\'):\n o = test._get_outcome()\n if isinstance(o, dict):\n key = (test.cache_id(), "return")\n if key in o:\n msg = test._get_outcome()[key]\n\n # print(sys.stdout.readlines())\n self.successes.append((test, None)) # (test, message) (to be consistent with failures and errors).\n self.successes[-1] = (self.successes[-1][0], {\'return\': msg,\n \'stdout\': stdout,\n \'stderr\': None})\n\n self.cc_terminate()\n\n def cc_terminate(self, success=True):\n if self.show_progress_bar or True:\n tsecs = np.round(self.cc.terminate(), 2)\n self.cc.file.flush()\n ss = self.item_title_print\n\n state = "PASS" if success else "FAILED"\n\n dot_parts = (\'.\' * max(0, self.nL - len(state) - len(ss)))\n if self.show_progress_bar or True:\n print(self.item_title_print + dot_parts, end="", file=self.cc.file)\n else:\n print(dot_parts, end="", file=self.cc.file)\n\n if tsecs >= 0.5:\n state += " (" + str(tsecs) + " seconds)"\n print(state, file=self.cc.file)\n\n def startTest(self, test):\n name = test.__class__.__name__\n if self.testsRun == 0 and hasattr(test.__class__, \'_cache2\'): # Disable this if the class is pure unittest.TestCase\n # This is the first time we are running a test. i.e. we can time the time taken to call setupClass.\n if test.__class__._cache2 is None:\n test.__class__._cache2 = {}\n test.__class__._cache2[((name, \'setUpClass\'), \'time\')] = time.time() - self.t_start\n\n self.testsRun += 1\n item_title = test.shortDescription() # Better for printing (get from cache).\n\n if item_title == None:\n # For unittest framework where getDescription may return None.\n item_title = self.getDescription(test)\n self.item_title_print = " * q%i.%i) %s" % (UTextResult.number + 1, self.testsRun, item_title)\n # if self.show_progress_bar or True:\n estimated_time = test.__class__._cache.get(((name, test._testMethodName), \'time\'), 100) if hasattr(test.__class__, \'_cache\') else 4\n self.cc = ActiveProgress(t=estimated_time, title=self.item_title_print, show_progress_bar=self.show_progress_bar)\n # else:\n # print(self.item_title_print + (\'.\' * max(0, self.nL - 4 - len(self.item_title_print))), end="")\n self._test = test\n # if not self.unmute:\n self._stdout = sys.stdout # Redundant. remove later.\n sys.stdout = Logger(io.StringIO(), write_to_stdout=self.unmute)\n\n def stopTest(self, test):\n # if not self.unmute:\n buff = sys.stdout.log\n sys.stdout = self._stdout # redundant.\n buff.close()\n super().stopTest(test)\n\n def _setupStdout(self):\n if self._previousTestClass == None:\n self.t_start = time.time()\n if hasattr(self.__class__, \'q_title_print\'):\n q_title_print = self.__class__.q_title_print\n else:\n q_title_print = "<unnamed test. See unitgrade.framework.py>"\n\n cc = ActiveProgress(t=self.setUpClass_time, title=q_title_print, show_progress_bar=self.show_progress_bar, mute_stdout=not self.unmute)\n self.cc = cc\n\n\n def _restoreStdout(self): # Used when setting up the test.\n if self._previousTestClass is None:\n q_time = self.cc.terminate()\n q_time = np.round(q_time, 2)\n sys.stdout.flush()\n if self.show_progress_bar:\n print(self.cc.title, end="")\n print(" " * max(0, self.nL - len(self.cc.title)) + (" (" + str(q_time) + " seconds)" if q_time >= 0.5 else ""))\n\n\nclass UTextTestRunner(unittest.TextTestRunner):\n def __init__(self, *args, **kwargs):\n stream = io.StringIO()\n super().__init__(*args, stream=stream, **kwargs)\n\n def _makeResult(self):\n # stream = self.stream # not you!\n stream = sys.stdout\n stream = _WritelnDecorator(stream)\n return self.resultclass(stream, self.descriptions, self.verbosity)\n\nimport importnb\nimport numpy as np\nimport sys\nimport pickle\nimport os\nimport inspect\nimport colorama\nimport unittest\nimport time\nimport textwrap\nimport urllib.parse\nimport requests\nimport ast\nimport numpy\nfrom diskcache import Cache\n\ncolorama.init(autoreset=True) # auto resets your settings after every output\nnumpy.seterr(all=\'raise\')\n\n\ndef setup_dir_by_class(C, base_dir):\n name = C.__class__.__name__\n return base_dir, name\n\n\nclass DKPupDB:\n def __init__(self, artifact_file, use_pupdb=True):\n # Make a double-headed disk cache thingy.\n self.dk = Cache(os.path.dirname(artifact_file)) # Start in this directory.\n self.name_ = os.path.basename(artifact_file[:-5])\n if self.name_ not in self.dk:\n self.dk[self.name_] = dict()\n self.use_pupdb = use_pupdb\n if self.use_pupdb:\n from pupdb.core import PupDB\n self.db_ = PupDB(artifact_file)\n\n def __setitem__(self, key, value):\n if self.use_pupdb:\n self.db_.set(key, value)\n with self.dk.transact():\n d = self.dk[self.name_]\n d[key] = value\n self.dk[self.name_] = d\n self.dk[self.name_ + "-updated"] = True\n\n def __getitem__(self, item):\n v = self.dk[self.name_][item]\n if self.use_pupdb:\n v2 = self.db_.get(item)\n if v != v2:\n print("Mismatch v1, v2 for ", item)\n return v\n\n def keys(self): # This one is also deprecated.\n return tuple(self.dk[self.name_].keys()) #.iterkeys())\n # return self.db_.keys()\n\n def set(self, item, value): # This one is deprecated.\n self[item] = value\n\n def get(self, item, default=None):\n return self[item] if item in self else default\n\n def __contains__(self, item):\n return item in self.dk[self.name_] #keys()\n # return item in self.dk\n\n\n_DASHBOARD_COMPLETED_MESSAGE = "Dashboard> Evaluation completed."\n\n# Consolidate this code.\nclass classmethod_dashboard(classmethod):\n def __init__(self, f):\n def dashboard_wrap(cls: UTestCase):\n if not cls._generate_artifacts:\n f(cls)\n return\n\n db = DKPupDB(cls._artifact_file_for_setUpClass())\n r = np.random.randint(1000 * 1000)\n db.set(\'run_id\', r)\n db.set(\'coverage_files_changed\', None)\n\n state_ = \'fail\'\n try:\n _stdout = sys.stdout\n _stderr = sys.stderr\n std_capture = StdCapturing(stdout=sys.stdout, stderr=sys.stderr, db=db, mute=False)\n\n # Run this unittest and record all of the output.\n # This is probably where we should hijack the stdout output and save it -- after all, this is where the test is actually run.\n # sys.stdout = stdout_capture\n sys.stderr = std_capture.dummy_stderr\n sys.stdout = std_capture.dummy_stdout\n db.set("state", "running")\n f(cls)\n state_ = \'pass\'\n except Exception as e:\n from werkzeug.debug.tbtools import DebugTraceback, _process_traceback\n state_ = \'fail\'\n db.set(\'state\', state_)\n exi = e\n dbt = DebugTraceback(exi)\n sys.stderr.write(dbt.render_traceback_text())\n html = dbt.render_traceback_html(include_title="hello world")\n db.set(\'wz_stacktrace\', html)\n raise e\n finally:\n db.set(\'state\', state_)\n std_capture.dummy_stdout.write_mute(_DASHBOARD_COMPLETED_MESSAGE)\n sys.stdout = _stdout\n sys.stderr = _stderr\n std_capture.close()\n super().__init__(dashboard_wrap)\n\nclass Report:\n title = "report title"\n abbreviate_questions = False # Should the test items start with \'Question ...\' or just be q1).\n version = None # A version number of the report (1.0). Used to compare version numbers with online resources.\n url = None # Remote location of this problem.\n\n questions = []\n pack_imports = []\n individual_imports = []\n\n _remote_check_cooldown_seconds = 1 # Seconds between remote check of report.\n nL = 120 # Maximum line width\n _config = None # Private variable. Used when collecting results from student computers. Should only be read/written by teacher and never used for regular evaluation.\n _setup_mode = False # True if test is being run in setup-mode, i.e. will not fail because of bad configurations, etc.\n\n @classmethod\n def reset(cls):\n for (q, _) in cls.questions:\n if hasattr(q, \'reset\'):\n q.reset()\n\n @classmethod\n def mfile(clc):\n return inspect.getfile(clc)\n\n def _file(self):\n return inspect.getfile(type(self))\n\n def _artifact_file(self):\n """ File for the artifacts DB (thread safe). This file is optinal. Note that it is a pupdb database file.\n Note the file is shared between all sub-questions. """\n return os.path.join(os.path.dirname(self._file()), "unitgrade_data/main_config_"+ os.path.basename(self._file()[:-3]) + ".artifacts.pkl")\n\n def _is_run_in_grade_mode(self):\n """ True if this report is being run as part of a grade run. """\n return self._file().endswith("_grade.py") # Not sure I love this convention.\n\n def _import_base_relative(self):\n if hasattr(self.pack_imports[0], \'__path__\'):\n root_dir = self.pack_imports[0].__path__[0]\n else:\n root_dir = self.pack_imports[0].__file__\n\n root_dir = os.path.dirname(root_dir)\n relative_path = os.path.relpath(self._file(), root_dir)\n modules = os.path.normpath(relative_path[:-3]).split(os.sep)\n relative_path = relative_path.replace("\\\\", "/")\n return root_dir, relative_path, modules\n\n def __init__(self, strict=False, payload=None):\n working_directory = os.path.abspath(os.path.dirname(self._file()))\n self.wdir, self.name = setup_dir_by_class(self, working_directory)\n # self.computed_answers_file = os.path.join(self.wdir, self.name + "_resources_do_not_hand_in.dat")\n for (q, _) in self.questions:\n q.nL = self.nL # Set maximum line length.\n\n if payload is not None:\n self.set_payload(payload, strict=strict)\n\n def main(self, verbosity=1):\n # Run all tests using standard unittest (nothing fancy).\n loader = unittest.TestLoader()\n for q, _ in self.questions:\n start = time.time() #\n suite = loader.loadTestsFromTestCase(q)\n unittest.TextTestRunner(verbosity=verbosity).run(suite)\n total = time.time() - start\n q.time = total\n\n def _setup_answers(self, with_coverage=False, verbose=True):\n if with_coverage:\n for q, _ in self.questions:\n q._with_coverage = True\n q._report = self\n for q, _ in self.questions:\n q._setup_answers_mode = True\n # q._generate_artifacts = False # Disable artifact generation when the report is being set up.\n\n evaluate_report_student(self, unmute=verbose, noprogress=not verbose, generate_artifacts=False) # Disable artifact generation.\n\n # self.main() # Run all tests in class just to get that out of the way...\n report_cache = {}\n for q, _ in self.questions:\n # print(self.questions)\n if hasattr(q, \'_save_cache\'):\n q()._save_cache()\n # print("q is", q())\n report_cache[q.__qualname__] = q._cache2\n else:\n report_cache[q.__qualname__] = {\'no cache see _setup_answers in framework.py\': True}\n if with_coverage:\n for q, _ in self.questions:\n q._with_coverage = False\n\n # report_cache is saved on a per-question basis.\n # it could also contain additional information such as runtime metadata etc. This may not be appropriate to store with the invidivual questions(?).\n # In this case, the function should be re-defined.\n return report_cache\n\n def set_payload(self, payloads, strict=False):\n for q, _ in self.questions:\n q._cache = payloads[q.__qualname__]\n self._config = payloads[\'config\']\n\n def _check_remote_versions(self):\n if self.url is None:\n return\n url = self.url\n if not url.endswith("/"):\n url += "/"\n snapshot_file = os.path.dirname(self._file()) + "/unitgrade_data/.snapshot"\n if os.path.isfile(snapshot_file):\n with open(snapshot_file, \'r\') as f:\n t = f.read()\n if (time.time() - float(t)) < self._remote_check_cooldown_seconds:\n return\n\n if self.url.startswith("https://gitlab"):\n # Try to turn url into a \'raw\' format.\n # "https://gitlab.compute.dtu.dk/tuhe/unitgrade_private/-/raw/master/examples/autolab_example_py_upload/instructor/cs102_autolab/report2_test.py?inline=false"\n # url = self.url\n url = url.replace("-/tree", "-/raw")\n # print(url)\n # "https://gitlab.compute.dtu.dk/tuhe/unitgrade_private/-/tree/master/examples/autolab_example_py_upload/instructor/cs102_autolab"\n # "https://gitlab.compute.dtu.dk/tuhe/unitgrade_private/-/raw/master/examples/autolab_example_py_upload/instructor/report2_test.py?inline=false"\n # "https://gitlab.compute.dtu.dk/tuhe/unitgrade_private/-/raw/master/examples/autolab_example_py_upload/instructor/cs102_autolab/report2_test.py?inline=false"\n raw_url = urllib.parse.urljoin(url, os.path.basename(self._file()) + "?inline=false")\n # print("Is this file run in local mode?", self._is_run_in_grade_mode())\n if self._is_run_in_grade_mode():\n remote_source = requests.get(raw_url).text\n with open(self._file(), \'r\') as f:\n local_source = f.read()\n if local_source != remote_source:\n print("\\nThe local version of this report is not identical to the remote version which can be found at")\n print(self.url)\n print("The most likely reason for this is that the remote version was updated by the teacher due to some issue.")\n print("You should check if there was an announcement and update the test to the most recent version; most likely")\n print("This can be done by running the command")\n print("> git pull")\n print("You can find the most recent code here:")\n print(self.url)\n raise Exception(f"Version of grade script does not match the remote version. Please update using git pull")\n else:\n text = requests.get(raw_url).text\n node = ast.parse(text)\n classes = [n for n in node.body if isinstance(n, ast.ClassDef) if n.name == self.__class__.__name__][0]\n for b in classes.body:\n # print(b.)\n if b.targets[0].id == "version":\n # print(b)\n # print(b.value)\n version_remote = b.value.value\n break\n if version_remote != self.version:\n print("\\nThe version of this report", self.version, "does not match the version of the report on git", version_remote)\n print("The most likely reason for this is that the remote version was updated by the teacher due to some issue.")\n print("You should check if there was an announcement and update the test to the most recent version; most likely")\n print("This can be done by running the command")\n print("> git pull")\n print("You can find the most recent code here:")\n print(self.url)\n raise Exception(f"Version of test on remote is {version_remote}, which is different than this version of the test {self.version}. Please update your test to the most recent version.")\n\n for (q,_) in self.questions:\n qq = q(skip_remote_check=True)\n cfile = q._cache_file()\n\n relpath = os.path.relpath(cfile, os.path.dirname(self._file()))\n relpath = relpath.replace("\\\\", "/")\n raw_url = urllib.parse.urljoin(url, relpath + "?inline=false")\n # requests.get(raw_url)\n\n with open(cfile, \'rb\') as f:\n b1 = f.read()\n\n b2 = requests.get(raw_url).content\n if b1 != b2:\n print("\\nQuestion ", qq.title, "relies on the data file", cfile)\n print("However, it appears that this file is missing or in a different version than the most recent found here:")\n print(self.url)\n print("The most likely reason for this is that the remote version was updated by the teacher due to some issue.")\n print("You should check if there was an announcement and update the test to the most recent version; most likely")\n print("This can be done by simply running the command")\n print("> git pull")\n print("to avoid running bad tests against good code, the program will now stop. Please update and good luck!")\n raise Exception("The data file for the question", qq.title, "did not match remote source found on git. The test will therefore automatically fail. Please update your test/data files.")\n\n t = time.time()\n if os.path.isdir(os.path.dirname(self._file()) + "/unitgrade_data"):\n with open(snapshot_file, \'w\') as f:\n f.write(f"{t}")\n\ndef get_hints(ss):\n """ Extract all blocks of the forms:\n\n Hints:\n bla-bla.\n\n and returns the content unaltered.\n """\n if ss == None:\n return None\n try:\n ss = textwrap.dedent(ss)\n ss = ss.replace(\'\'\'"""\'\'\', "").strip()\n hints = ["hints:", "hint:"]\n indexes = [ss.lower().find(h) for h in hints]\n j = np.argmax(indexes)\n if indexes[j] == -1:\n return None\n h = hints[j]\n ss = ss[ss.lower().find(h) + len(h) + 1:]\n ss = "\\n".join([l for l in ss.split("\\n") if not l.strip().startswith(":")])\n ss = textwrap.dedent(ss).strip()\n # if ss.startswith(\'*\'):\n # ss = ss[1:].strip()\n return ss\n except Exception as e:\n print("bad hints", ss, e)\n\n\nclass UTestCase(unittest.TestCase):\n # a = 234\n _outcome = None # A dictionary which stores the user-computed outcomes of all the tests. This differs from the cache.\n _cache = None # Read-only cache. Ensures method always produce same result.\n _cache2 = None # User-written cache.\n _with_coverage = False\n _covcache = None # Coverage cache. Written to if _with_coverage is true.\n _report = None # The report used. This is very, very hacky and should always be None. Don\'t rely on it!\n _run_in_report_mode = True\n\n _generate_artifacts = True # Whether the file will generate the artifact .json files. This is used in the _grade-script mode.\n # If true, the tests will not fail when cache is used. This is necesary since otherwise the cache will not be updated\n # during setup, and the deploy script must be run many times.\n _setup_answers_mode = False\n\n def capture(self):\n if hasattr(self, \'_stdout\') and self._stdout is not None:\n file = self._stdout\n else:\n file = sys.stdout\n return Capturing2(stdout=file)\n\n @classmethod\n def question_title(cls):\n """ Return the question title """\n if cls.__doc__ is not None:\n title = cls.__doc__.strip().splitlines()[0].strip()\n if not (title.startswith("Hints:") or title.startswith("Hint:") ):\n return title\n return cls.__qualname__\n\n def run(self, result):\n # print("Run called in test framework...", self._generate_artifacts)\n if not self._generate_artifacts:\n return super().run(result)\n from unittest.case import TestCase\n\n\n db = DKPupDB(self._artifact_file())\n db.set("state", "running")\n db.set(\'run_id\', np.random.randint(1000*1000))\n db.set(\'coverage_files_changed\', None)\n\n\n _stdout = sys.stdout\n _stderr = sys.stderr\n\n std_capture = StdCapturing(stdout=sys.stdout, stderr=sys.stderr, db=db, mute=False)\n\n # stderr_capture = StdCapturing(sys.stderr, db=db)\n # std_err_capture = StdCapturing(sys.stderr, "stderr", db=db)\n state_ = None\n try:\n # Run this unittest and record all of the output.\n # This is probably where we should hijack the stdout output and save it -- after all, this is where the test is actually run.\n # sys.stdout = stdout_capture\n sys.stderr = std_capture.dummy_stderr\n sys.stdout = std_capture.dummy_stdout\n\n result_ = TestCase.run(self, result)\n\n from werkzeug.debug.tbtools import DebugTraceback, _process_traceback\n # print(result_._excinfo[0])\n actual_errors = []\n for test, err in self._error_fed_during_run:\n if err is None:\n continue\n else:\n import traceback\n # traceback.print_tb(err[2])\n actual_errors.append(err)\n\n if len(actual_errors) > 0:\n ex, exi, tb = actual_errors[0]\n exi.__traceback__ = tb\n dbt = DebugTraceback(exi)\n sys.stderr.write(dbt.render_traceback_text())\n html = dbt.render_traceback_html(include_title="hello world")\n db.set(\'wz_stacktrace\', html)\n # db.set(\'state\', \'fail\')\n state_ = "fail"\n else:\n state_ = "pass"\n except Exception as e:\n state_ = "fail"\n import traceback\n traceback.print_exc()\n raise e\n finally:\n db.set(\'state\', state_)\n std_capture.dummy_stdout.write_mute(_DASHBOARD_COMPLETED_MESSAGE)\n sys.stdout = _stdout\n sys.stderr = _stderr\n std_capture.close()\n return result_\n\n def _callSetUp(self):\n if self._with_coverage:\n if self._covcache is None:\n self._covcache = {}\n import coverage\n self.cov = coverage.Coverage(data_file=None)\n self.cov.start()\n self.setUp()\n\n def _callTearDown(self):\n self.tearDown()\n # print("Teardown.")\n if self._with_coverage:\n # print("with cov")\n from pathlib import Path\n from snipper import snipper_main\n try:\n self.cov.stop()\n except Exception as e:\n print("Something went wrong while tearing down coverage test")\n print(e)\n data = self.cov.get_data()\n base, _, _ = self._report._import_base_relative()\n for file in data.measured_files():\n file = os.path.normpath(file)\n root = Path(base)\n child = Path(file)\n if root in child.parents:\n # print("Reading file", child)\n with open(child, \'r\') as f:\n s = f.read()\n lines = s.splitlines()\n garb = \'GARBAGE\'\n lines2 = snipper_main.censor_code(lines, keep=True)\n # print("\\n".join(lines2))\n if len(lines) != len(lines2):\n for k in range(len(lines)):\n print(k, ">", lines[k], "::::::::", lines2[k])\n print("Snipper failure; line lenghts do not agree. Exiting..")\n print(child, "len(lines) == len(lines2)", len(lines), len(lines2))\n import sys\n sys.exit()\n\n assert len(lines) == len(lines2)\n for ll in data.contexts_by_lineno(file):\n l = ll-1\n if l < len(lines2) and lines2[l].strip() == garb:\n # print("Got a hit at l", l)\n rel = os.path.relpath(child, root)\n cc = self._covcache\n j = 0\n for j in range(l, -1, -1):\n if "def" in lines2[j] or "class" in lines2[j]:\n break\n from snipper.legacy import gcoms\n\n fun = lines2[j]\n comments, _ = gcoms("\\n".join(lines2[j:l]))\n if rel not in cc:\n cc[rel] = {}\n cc[rel][fun] = (l, "\\n".join(comments))\n # print("found", rel, fun)\n self._cache_put((self.cache_id(), \'coverage\'), self._covcache)\n\n def shortDescriptionStandard(self):\n sd = super().shortDescription()\n if sd is None or sd.strip().startswith("Hints:") or sd.strip().startswith("Hint:"):\n sd = self._testMethodName\n return sd\n\n def shortDescription(self):\n sd = self.shortDescriptionStandard()\n title = self._cache_get((self.cache_id(), \'title\'), sd)\n return title if title is not None else sd\n\n @property\n def title(self):\n return self.shortDescription()\n\n @title.setter\n def title(self, value):\n self._cache_put((self.cache_id(), \'title\'), value)\n\n def _get_outcome(self):\n if not hasattr(self.__class__, \'_outcome\') or self.__class__._outcome is None:\n self.__class__._outcome = {}\n return self.__class__._outcome\n\n def _callTestMethod(self, testMethod):\n t = time.time()\n self._ensure_cache_exists() # Make sure cache is there.\n if self._testMethodDoc is not None:\n self._cache_put((self.cache_id(), \'title\'), self.shortDescriptionStandard())\n\n self._cache2[(self.cache_id(), \'assert\')] = {}\n res = testMethod()\n elapsed = time.time() - t\n self._get_outcome()[ (self.cache_id(), "return") ] = res\n self._cache_put((self.cache_id(), "time"), elapsed)\n\n\n def cache_id(self):\n c = self.__class__.__qualname__\n m = self._testMethodName\n return c, m\n\n def __init__(self, *args, skip_remote_check=False, **kwargs):\n super().__init__(*args, **kwargs)\n self._load_cache()\n self._assert_cache_index = 0\n # Perhaps do a sanity check here to see if the cache is up to date? To do that, we must make sure the\n # cache exists locally.\n # Find the report class this class is defined within.\n if skip_remote_check:\n return\n import importlib, inspect\n found_reports = []\n # print("But do I have report", self._report)\n # print("I think I am module", self.__module__)\n # print("Importlib says", importlib.import_module(self.__module__))\n # This will delegate you to the wrong main clsas when running in grade mode.\n for name, cls in inspect.getmembers(importlib.import_module(self.__module__), inspect.isclass):\n # print("checking", cls)\n if issubclass(cls, Report):\n for q,_ in cls.questions:\n if q == self.__class__:\n found_reports.append(cls)\n if len(found_reports) == 0:\n pass # This case occurs when the report _grade script is being run.\n # raise Exception("This question is not a member of a report. Very, very odd.")\n if len(found_reports) > 1:\n raise Exception("This question is a member of multiple reports. That should not be the case -- don\'t get too creative.")\n if len(found_reports) > 0:\n report = found_reports[0]\n report()._check_remote_versions()\n\n\n def _ensure_cache_exists(self):\n if not hasattr(self.__class__, \'_cache\') or self.__class__._cache == None:\n self.__class__._cache = dict()\n if not hasattr(self.__class__, \'_cache2\') or self.__class__._cache2 == None:\n self.__class__._cache2 = dict()\n\n def _cache_get(self, key, default=None):\n self._ensure_cache_exists()\n return self.__class__._cache.get(key, default)\n\n def _cache_put(self, key, value):\n self._ensure_cache_exists()\n self.__class__._cache2[key] = value\n\n def _cache_contains(self, key):\n self._ensure_cache_exists()\n return key in self.__class__._cache\n\n def get_expected_test_value(self):\n key = (self.cache_id(), \'assert\')\n id = self._assert_cache_index\n cache = self._cache_get(key)\n _expected = cache.get(id, f"Key {id} not found in cache; framework files missing. Please run deploy()")\n return _expected\n\n def wrap_assert(self, assert_fun, first, *args, **kwargs):\n key = (self.cache_id(), \'assert\')\n if not self._cache_contains(key):\n print("Warning, framework missing", key)\n self.__class__._cache[key] = {} # A new dict. We manually insert it because we have to use that the dict is mutable.\n cache = self._cache_get(key)\n id = self._assert_cache_index\n _expected = cache.get(id, f"Key {id} not found in cache; framework files missing. Please run deploy()")\n if not id in cache:\n print("Warning, framework missing cache index", key, "id =", id, " - The test will be skipped for now.")\n if self._setup_answers_mode:\n _expected = first # Bypass by setting equal to first. This is in case multiple self.assertEqualC\'s are run in a row and have to be set.\n\n # The order of these calls is important. If the method assert fails, we should still store the correct result in cache.\n cache[id] = first\n self._cache_put(key, cache)\n self._assert_cache_index += 1\n if not self._setup_answers_mode:\n assert_fun(first, _expected, *args, **kwargs)\n else:\n try:\n assert_fun(first, _expected, *args, **kwargs)\n except Exception as e:\n print("Mumble grumble. Cache function failed during class setup. Most likely due to old cache. Re-run deploy to check it pass.", id)\n print("> first", first)\n print("> expected", _expected)\n print(e)\n\n\n def assertEqualC(self, first, msg=None):\n self.wrap_assert(self.assertEqual, first, msg)\n\n def _shape_equal(self, first, second):\n a1 = np.asarray(first).squeeze()\n a2 = np.asarray(second).squeeze()\n msg = None\n msg = "" if msg is None else msg\n if len(msg) > 0:\n msg += "\\n"\n self.assertEqual(a1.shape, a2.shape, msg=msg + "Dimensions of input data does not agree.")\n assert(np.all(np.isinf(a1) == np.isinf(a2))) # Check infinite part.\n a1[np.isinf(a1)] = 0\n a2[np.isinf(a2)] = 0\n diff = np.abs(a1 - a2)\n return diff\n\n def assertLinf(self, first, second=None, tol=1e-5, msg=None):\n """ Test in the L_infinity norm.\n :param first:\n :param second:\n :param tol:\n :param msg:\n :return:\n """\n if second is None:\n return self.wrap_assert(self.assertLinf, first, tol=tol, msg=msg)\n else:\n diff = self._shape_equal(first, second)\n np.testing.assert_allclose(first, second, atol=tol)\n \n max_diff = max(diff.flat)\n if max_diff >= tol:\n from unittest.util import safe_repr\n # msg = f\'{safe_repr(first)} != {safe_repr(second)} : Not equal within tolerance {tol}\'\n # print(msg)\n # np.testing.assert_almost_equal\n # import numpy as np\n print(f"|first - second|_max = {max_diff} > {tol} ")\n np.testing.assert_almost_equal(first, second)\n # If the above fail, make sure to throw an error:\n self.assertFalse(max_diff >= tol, msg=f\'Input arrays are not equal within tolerance {tol}\')\n # self.assertEqual(first, second, msg=f\'Not equal within tolerance {tol}\')\n\n def assertL2(self, first, second=None, tol=1e-5, msg=None, relative=False):\n if second is None:\n return self.wrap_assert(self.assertL2, first, tol=tol, msg=msg, relative=relative)\n else:\n # We first test using numpys build-in testing method to see if one coordinate deviates a great deal.\n # This gives us better output, and we know that the coordinate wise difference is lower than the norm difference.\n if not relative:\n np.testing.assert_allclose(first, second, atol=tol)\n diff = self._shape_equal(first, second)\n diff = ( ( np.asarray( diff.flatten() )**2).sum() )**.5\n\n scale = (2/(np.linalg.norm(np.asarray(first).flat) + np.linalg.norm(np.asarray(second).flat)) ) if relative else 1\n max_diff = diff*scale\n if max_diff >= tol:\n msg = "" if msg is None else msg\n print(f"|first - second|_2 = {max_diff} > {tol} ")\n # Deletage to numpy. Let numpy make nicer messages.\n np.testing.assert_almost_equal(first, second) # This function does not take a msg parameter.\n # Make sure to throw an error no matter what.\n self.assertFalse(max_diff >= tol, msg=f\'Input arrays are not equal within tolerance {tol}\')\n # self.assertEqual(first, second, msg=msg + f"Not equal within tolerance {tol}")\n\n @classmethod\n def _cache_file(cls):\n return os.path.dirname(inspect.getabsfile(cls)) + "/unitgrade_data/" + cls.__name__ + ".pkl"\n\n @classmethod\n def _artifact_file_for_setUpClass(cls):\n file = os.path.join(os.path.dirname(cls._cache_file()), ""+cls.__name__+"-setUpClass.json")\n print("_artifact_file_for_setUpClass(cls): will return", file, "__class__", cls)\n # cf = os.path.dirname(inspect.getabsfile(cls)) + "/unitgrade_data/" + cls.__name__\n return file\n\n def _artifact_file(self):\n """ File for the artifacts DB (thread safe). This file is optinal. Note that it is a pupdb database file.\n Note the file is shared between all sub-questions. """\n return os.path.join(os.path.dirname(self.__class__._cache_file()), \'-\'.join(self.cache_id()) + ".json")\n\n def _save_cache(self):\n # get the class name (i.e. what to save to).\n cfile = self.__class__._cache_file()\n if not os.path.isdir(os.path.dirname(cfile)):\n os.makedirs(os.path.dirname(cfile))\n\n if hasattr(self.__class__, \'_cache2\'):\n with open(cfile, \'wb\') as f:\n pickle.dump(self.__class__._cache2, f)\n\n # But you can also set cache explicitly.\n def _load_cache(self):\n if self._cache is not None: # Cache already loaded. We will not load it twice.\n return\n # raise Exception("Loaded cache which was already set. What is going on?!")\n cfile = self.__class__._cache_file()\n if os.path.exists(cfile):\n try:\n with open(cfile, \'rb\') as f:\n data = pickle.load(f)\n self.__class__._cache = data\n except Exception as e:\n print("Cache file did not exist:", cfile)\n print(e)\n else:\n print("Warning! data file not found", cfile)\n\n def _get_coverage_files(self):\n key = (self.cache_id(), \'coverage\')\n # CC = None\n # if self._cache_contains(key):\n return self._cache_get(key, []) # Anything wrong with the empty list?\n # return CC\n\n def _get_hints(self):\n """\n This code is run when the test is set up to generate the hints and store them in an artifact file. It may be beneficial to simple compute them beforehand\n and store them in the local unitgrade pickle file. This code is therefore expected to superceede the alterative code later.\n """\n hints = []\n # print("Getting hint")\n key = (self.cache_id(), \'coverage\')\n if self._cache_contains(key):\n CC = self._cache_get(key)\n # cl, m = self.cache_id()\n # print("Getting hint using", CC)\n # Insert newline to get better formatting.\n # gprint(\n # f"\\n> An error occured during the test: {cl}.{m}. The following files/methods has code in them you are supposed to edit and may therefore be the cause of the problem:")\n for file in CC:\n rec = CC[file]\n # gprint(f"> * {file}")\n for l in rec:\n _, comments = CC[file][l]\n hint = get_hints(comments)\n\n if hint != None:\n hints.append((hint, file, l))\n\n doc = self._testMethodDoc\n # print("doc", doc)\n if doc is not None:\n hint = get_hints(self._testMethodDoc)\n if hint is not None:\n hints = [(hint, None, self.cache_id()[1])] + hints\n\n return hints\n\n def _feedErrorsToResult(self, result, errors):\n """ Use this to show hints on test failure.\n It feeds error to the result -- so if there are errors, they will crop up here\n """\n self._error_fed_during_run = errors.copy() # import to copy the error list.\n\n # result._test._error_fed_during_run = errors.copy()\n\n if not isinstance(result, UTextResult):\n er = [e for e, v in errors if v != None]\n # print("Errors are", errors)\n if len(er) > 0:\n hints = []\n key = (self.cache_id(), \'coverage\')\n if self._cache_contains(key):\n CC = self._cache_get(key)\n cl, m = self.cache_id()\n # Insert newline to get better formatting.\n gprint(f"\\n> An error occured during the test: {cl}.{m}. The following files/methods has code in them you are supposed to edit and may therefore be the cause of the problem:")\n for file in CC:\n rec = CC[file]\n gprint(f"> * {file}")\n for l in rec:\n _, comments = CC[file][l]\n hint = get_hints(comments)\n\n if hint != None:\n hints.append((hint, file, l) )\n gprint(f"> - {l}")\n\n er = er[0]\n\n doc = er._testMethodDoc\n # print("doc", doc)\n if doc is not None:\n hint = get_hints(er._testMethodDoc)\n if hint is not None:\n hints = [(hint, None, self.cache_id()[1] )] + hints\n if len(hints) > 0:\n # print(hints)\n for hint, file, method in hints:\n s = (f"\'{method.strip()}\'" if method is not None else "")\n if method is not None and file is not None:\n s += " in "\n try:\n s += (file.strip() if file is not None else "")\n gprint(">")\n gprint("> Hints (from " + s + ")")\n gprint(textwrap.indent(hint, "> "))\n except Exception as e:\n print("Bad stuff in hints. ")\n print(hints)\n # result._last_errors = errors\n super()._feedErrorsToResult(result, errors)\n b = 234\n\n def startTestRun(self):\n super().startTestRun()\n\nclass Required:\n pass\n\nclass ParticipationTest(UTestCase,Required):\n max_group_size = None\n students_in_group = None\n workload_assignment = {\'Question 1\': [1, 0, 0]}\n\n def test_students(self):\n pass\n\n def test_workload(self):\n pass\n\n# 817, 705\nclass NotebookTestCase(UTestCase):\n notebook = None\n _nb = None\n @classmethod\n def setUpClass(cls) -> None:\n with Capturing():\n cls._nb = importnb.Notebook.load(cls.notebook)\n\n @property\n def nb(self):\n return self.__class__._nb\n # 870.\n\nimport hashlib\nimport io\nimport tokenize\nimport numpy as np\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport pyfiglet\nimport unittest\nimport inspect\nimport os\nimport argparse\nimport time\n\nparser = argparse.ArgumentParser(description=\'Evaluate your report.\', epilog="""Example: \nTo run all tests in a report: \n\n> python assignment1_dp.py\n\nTo run only question 2 or question 2.1\n\n> python assignment1_dp.py -q 2\n> python assignment1_dp.py -q 2.1\n\nNote this scripts does not grade your report. To grade your report, use:\n\n> python report1_grade.py\n\nFinally, note that if your report is part of a module (package), and the report script requires part of that package, the -m option for python may be useful.\nFor instance, if the report file is in Documents/course_package/report3_complete.py, and `course_package` is a python package, then change directory to \'Documents/` and run:\n\n> python -m course_package.report1\n\nsee https://docs.python.org/3.9/using/cmdline.html\n""", formatter_class=argparse.RawTextHelpFormatter)\nparser.add_argument(\'-q\', nargs=\'?\', type=str, default=None, help=\'Only evaluate this question (e.g.: -q 2)\')\nparser.add_argument(\'--showexpected\', action="store_true", help=\'Show the expected/desired result\')\nparser.add_argument(\'--showcomputed\', action="store_true", help=\'Show the answer your code computes\')\nparser.add_argument(\'--unmute\', action="store_true", help=\'Show result of print(...) commands in code\')\nparser.add_argument(\'--passall\', action="store_true", help=\'Automatically pass all tests. Useful when debugging.\')\nparser.add_argument(\'--noprogress\', action="store_true", help=\'Disable progress bars.\')\n\ndef evaluate_report_student(report, question=None, qitem=None, unmute=None, passall=None, ignore_missing_file=False,\n show_tol_err=False, show_privisional=True, noprogress=None,\n generate_artifacts=True):\n args = parser.parse_args()\n if noprogress is None:\n noprogress = args.noprogress\n\n if question is None and args.q is not None:\n question = args.q\n if "." in question:\n question, qitem = [int(v) for v in question.split(".")]\n else:\n question = int(question)\n\n if hasattr(report, "computed_answer_file") and not os.path.isfile(report.computed_answers_file) and not ignore_missing_file:\n raise Exception("> Error: The pre-computed answer file", os.path.abspath(report.computed_answers_file), "does not exist. Check your package installation")\n\n if unmute is None:\n unmute = args.unmute\n if passall is None:\n passall = args.passall\n\n results, table_data = evaluate_report(report, question=question, show_progress_bar=not unmute and not noprogress, qitem=qitem,\n verbose=False, passall=passall, show_expected=args.showexpected, show_computed=args.showcomputed,unmute=unmute,\n show_tol_err=show_tol_err,\n generate_artifacts=generate_artifacts)\n\n\n if question is None and show_privisional:\n print("Provisional evaluation")\n tabulate(table_data)\n table = table_data\n print(tabulate(table))\n print(" ")\n\n fr = inspect.getouterframes(inspect.currentframe())[1].filename\n gfile = os.path.basename(fr)[:-3] + "_grade.py"\n if os.path.exists(gfile):\n print("Note your results have not yet been registered. \\nTo register your results, please run the file:")\n print(">>>", gfile)\n print("In the same manner as you ran this file.")\n\n\n return results\n\n\ndef upack(q):\n # h = zip([(i[\'w\'], i[\'possible\'], i[\'obtained\']) for i in q.values()])\n h =[(i[\'w\'], i[\'possible\'], i[\'obtained\']) for i in q.values()]\n h = np.asarray(h)\n return h[:,0], h[:,1], h[:,2],\n\nclass SequentialTestLoader(unittest.TestLoader):\n def getTestCaseNames(self, testCaseClass):\n test_names = super().getTestCaseNames(testCaseClass)\n # testcase_methods = list(testCaseClass.__dict__.keys())\n ls = []\n for C in testCaseClass.mro():\n if issubclass(C, unittest.TestCase):\n ls = list(C.__dict__.keys()) + ls\n testcase_methods = ls\n test_names.sort(key=testcase_methods.index)\n return test_names\n\ndef evaluate_report(report, question=None, qitem=None, passall=False, verbose=False, show_expected=False, show_computed=False,unmute=False, show_help_flag=True, silent=False,\n show_progress_bar=True,\n show_tol_err=False,\n generate_artifacts=True, # Generate the artifact .json files. These are exclusively used by the dashboard.\n big_header=True):\n\n now = datetime.now()\n if big_header:\n ascii_banner = pyfiglet.figlet_format("UnitGrade", font="doom")\n b = "\\n".join( [l for l in ascii_banner.splitlines() if len(l.strip()) > 0] )\n else:\n b = "Unitgrade"\n dt_string = now.strftime("%d/%m/%Y %H:%M:%S")\n print(b + " v" + __version__ + ", started: " + dt_string+ "\\n")\n # print("Started: " + dt_string)\n report._check_remote_versions() # Check (if report.url is present) that remote files exist and are in sync.\n s = report.title\n if hasattr(report, "version") and report.version is not None:\n s += f" version {report.version}"\n print(s, "(use --help for options)" if show_help_flag else "")\n # print(f"Loaded answers from: ", report.computed_answers_file, "\\n")\n table_data = []\n t_start = time.time()\n score = {}\n loader = SequentialTestLoader()\n\n for n, (q, w) in enumerate(report.questions):\n q._generate_artifacts = generate_artifacts # Set whether artifact .json files will be generated.\n if question is not None and n+1 != question:\n continue\n suite = loader.loadTestsFromTestCase(q)\n qtitle = q.question_title() if hasattr(q, \'question_title\') else q.__qualname__\n if not report.abbreviate_questions:\n q_title_print = "Question %i: %s"%(n+1, qtitle)\n else:\n q_title_print = "q%i) %s" % (n + 1, qtitle)\n\n print(q_title_print, end="")\n q.possible = 0\n q.obtained = 0\n # q_ = {} # Gather score in this class.\n UTextResult.q_title_print = q_title_print # Hacky\n UTextResult.show_progress_bar = show_progress_bar # Hacky.\n UTextResult.number = n\n UTextResult.nL = report.nL\n UTextResult.unmute = unmute # Hacky as well.\n UTextResult.setUpClass_time = q._cache.get(((q.__name__, \'setUpClass\'), \'time\'), 3) if hasattr(q, \'_cache\') and q._cache is not None else 3\n\n\n res = UTextTestRunner(verbosity=2, resultclass=UTextResult).run(suite)\n details = {}\n for s, msg in res.successes + res.failures + res.errors:\n # from unittest.suite import _ErrorHolder\n # from unittest import _Err\n # if isinstance(s, _ErrorHolder)\n if hasattr(s, \'_testMethodName\'):\n key = (q.__name__, s._testMethodName)\n else:\n # In case s is an _ErrorHolder (unittest.suite)\n key = (q.__name__, s.id())\n # key = (q.__name__, s._testMethodName) # cannot use the cache_id method bc. it is not compatible with plain unittest.\n\n detail = {}\n if (s,msg) in res.successes:\n detail[\'status\'] = "pass"\n elif (s,msg) in res.failures:\n detail[\'status\'] = \'fail\'\n elif (s,msg) in res.errors:\n detail[\'status\'] = \'error\'\n else:\n raise Exception("Status not known.")\n\n nice_title = s.title\n detail = {**detail, **msg, \'nice_title\': nice_title}#[\'message\'] = msg\n details[key] = detail\n\n # q_[s._testMethodName] = ("pass", None)\n # for (s,msg) in res.failures:\n # q_[s._testMethodName] = ("fail", msg)\n # for (s,msg) in res.errors:\n # q_[s._testMethodName] = ("error", msg)\n # res.successes[0]._get_outcome()\n\n possible = res.testsRun\n obtained = len(res.successes)\n\n # assert len(res.successes) + len(res.errors) + len(res.failures) == res.testsRun\n\n obtained = int(w * obtained * 1.0 / possible ) if possible > 0 else 0\n score[n] = {\'w\': w, \'possible\': w, \'obtained\': obtained, \'items\': details, \'title\': qtitle, \'name\': q.__name__,\n }\n q.obtained = obtained\n q.possible = possible\n # print(q._cache)\n # print(q._covcache)\n s1 = f" * q{n+1}) Total"\n s2 = f" {q.obtained}/{w}"\n print(s1 + ("."* (report.nL-len(s1)-len(s2) )) + s2 )\n print(" ")\n table_data.append([f"q{n+1}) Total", f"{q.obtained}/{w}"])\n\n ws, possible, obtained = upack(score)\n possible = int( msum(possible) )\n obtained = int( msum(obtained) ) # Cast to python int\n report.possible = possible\n report.obtained = obtained\n now = datetime.now()\n dt_string = now.strftime("%H:%M:%S")\n\n dt = int(time.time()-t_start)\n minutes = dt//60\n seconds = dt - minutes*60\n plrl = lambda i, s: str(i) + " " + s + ("s" if i != 1 else "")\n\n dprint(first = "Total points at "+ dt_string + " (" + plrl(minutes, "minute") + ", "+ plrl(seconds, "second") +")",\n last=""+str(report.obtained)+"/"+str(report.possible), nL = report.nL)\n\n # print(f"Completed at "+ dt_string + " (" + plrl(minutes, "minute") + ", "+ plrl(seconds, "second") +"). Total")\n\n table_data.append(["Total", ""+str(report.obtained)+"/"+str(report.possible) ])\n results = {\'total\': (obtained, possible), \'details\': score}\n return results, table_data\n\n\ndef python_code_str_id(python_code, strip_comments_and_docstring=True):\n s = python_code\n\n if strip_comments_and_docstring:\n try:\n s = remove_comments_and_docstrings(s)\n except Exception as e:\n print("--"*10)\n print(python_code)\n print(e)\n\n s = "".join([c.strip() for c in s.split()])\n hash_object = hashlib.blake2b(s.encode())\n return hash_object.hexdigest()\n\n\ndef file_id(file, strip_comments_and_docstring=True):\n with open(file, \'r\') as f:\n # s = f.read()\n return python_code_str_id(f.read())\n\n\ndef remove_comments_and_docstrings(source):\n """\n Returns \'source\' minus comments and docstrings.\n """\n io_obj = io.StringIO(source)\n out = ""\n prev_toktype = tokenize.INDENT\n last_lineno = -1\n last_col = 0\n for tok in tokenize.generate_tokens(io_obj.readline):\n token_type = tok[0]\n token_string = tok[1]\n start_line, start_col = tok[2]\n end_line, end_col = tok[3]\n ltext = tok[4]\n # The following two conditionals preserve indentation.\n # This is necessary because we\'re not using tokenize.untokenize()\n # (because it spits out code with copious amounts of oddly-placed\n # whitespace).\n if start_line > last_lineno:\n last_col = 0\n if start_col > last_col:\n out += (" " * (start_col - last_col))\n # Remove comments:\n if token_type == tokenize.COMMENT:\n pass\n # This series of conditionals removes docstrings:\n elif token_type == tokenize.STRING:\n if prev_toktype != tokenize.INDENT:\n # This is likely a docstring; double-check we\'re not inside an operator:\n if prev_toktype != tokenize.NEWLINE:\n # Note regarding NEWLINE vs NL: The tokenize module\n # differentiates between newlines that start a new statement\n # and newlines inside of operators such as parens, brackes,\n # and curly braces. Newlines inside of operators are\n # NEWLINE and newlines that start new code are NL.\n # Catch whole-module docstrings:\n if start_col > 0:\n # Unlabelled indentation means we\'re inside an operator\n out += token_string\n # Note regarding the INDENT token: The tokenize module does\n # not label indentation inside of an operator (parens,\n # brackets, and curly braces) as actual indentation.\n # For example:\n # def foo():\n # "The spaces before this docstring are tokenize.INDENT"\n # test = [\n # "The spaces before this string do not get a token"\n # ]\n else:\n out += token_string\n prev_toktype = token_type\n last_col = end_col\n last_lineno = end_line\n return out\n\nimport textwrap\nimport bz2\nimport pickle\nimport os\nimport zipfile\nimport io\n\ndef bzwrite(json_str, token): # to get around obfuscation issues\n with getattr(bz2, \'open\')(token, "wt") as f:\n f.write(json_str)\n\ndef gather_imports(imp):\n resources = {}\n m = imp\n f = m.__file__\n if hasattr(m, \'__file__\') and not hasattr(m, \'__path__\'):\n top_package = os.path.dirname(m.__file__)\n module_import = True\n else:\n im = __import__(m.__name__.split(\'.\')[0])\n if isinstance(im, list):\n print("im is a list")\n print(im)\n # the __path__ attribute *may* be a string in some cases. I had to fix this.\n print("path.:", __import__(m.__name__.split(\'.\')[0]).__path__)\n # top_package = __import__(m.__name__.split(\'.\')[0]).__path__._path[0]\n top_package = __import__(m.__name__.split(\'.\')[0]).__path__[0]\n module_import = False\n\n found_hashes = {}\n # pycode = {}\n resources[\'pycode\'] = {}\n zip_buffer = io.BytesIO()\n with zipfile.ZipFile(zip_buffer, \'w\') as zip:\n for root, dirs, files in os.walk(top_package):\n for file in files:\n if file.endswith(".py"):\n fpath = os.path.join(root, file)\n v = os.path.relpath(fpath, os.path.dirname(top_package) if not module_import else top_package)\n zip.write(fpath, v)\n if not fpath.endswith("_grade.py"): # Exclude grade files.\n with open(fpath, \'r\') as f:\n s = f.read()\n found_hashes[v] = python_code_str_id(s)\n resources[\'pycode\'][v] = s\n\n resources[\'zipfile\'] = zip_buffer.getvalue()\n resources[\'top_package\'] = top_package\n resources[\'module_import\'] = module_import\n resources[\'blake2b_file_hashes\'] = found_hashes\n return resources, top_package\n\n\nimport argparse\nparser = argparse.ArgumentParser(description=\'Evaluate your report.\', epilog="""Use this script to get the score of your report. Example:\n\n> python report1_grade.py\n\nFinally, note that if your report is part of a module (package), and the report script requires part of that package, the -m option for python may be useful.\nFor instance, if the report file is in Documents/course_package/report3_complete.py, and `course_package` is a python package, then change directory to \'Documents/` and run:\n\n> python -m course_package.report1\n\nsee https://docs.python.org/3.9/using/cmdline.html\n""", formatter_class=argparse.RawTextHelpFormatter)\nparser.add_argument(\'--noprogress\', action="store_true", help=\'Disable progress bars\')\nparser.add_argument(\'--autolab\', action="store_true", help=\'Show Autolab results\')\n\ndef gather_report_source_include(report):\n sources = {}\n # print("")\n # if not args.autolab:\n if len(report.individual_imports) > 0:\n print("By uploading the .token file, you verify the files:")\n for m in report.individual_imports:\n print(">", m.__file__)\n print("Are created/modified individually by you in agreement with DTUs exam rules")\n report.pack_imports += report.individual_imports\n\n if len(report.pack_imports) > 0:\n print("Including files in upload...")\n for k, m in enumerate(report.pack_imports):\n nimp, top_package = gather_imports(m)\n _, report_relative_location, module_import = report._import_base_relative()\n\n nimp[\'report_relative_location\'] = report_relative_location\n nimp[\'report_module_specification\'] = module_import\n nimp[\'name\'] = m.__name__\n sources[k] = nimp\n print(f" * {m.__name__}")\n return sources\n\ndef gather_upload_to_campusnet(report, output_dir=None, token_include_plaintext_source=False):\n # n = report.nL\n args = parser.parse_args()\n results, table_data = evaluate_report(report, show_help_flag=False, show_expected=False, show_computed=False, silent=True,\n show_progress_bar=not args.noprogress,\n big_header=not args.autolab,\n generate_artifacts=False,\n )\n print("")\n sources = {}\n if not args.autolab:\n results[\'sources\'] = sources = gather_report_source_include(report)\n\n token_plain = """\n# This file contains your results. Do not edit its content. Simply upload it as it is. """\n\n s_include = [token_plain]\n known_hashes = []\n cov_files = []\n use_coverage = True\n if report._config is not None:\n known_hashes = report._config[\'blake2b_file_hashes\']\n for Q, _ in report.questions:\n use_coverage = use_coverage and isinstance(Q, UTestCase)\n for key in Q._cache:\n if len(key) >= 2 and key[1] == "coverage":\n for f in Q._cache[key]:\n cov_files.append(f)\n\n for s in sources.values():\n for f_rel, hash in s[\'blake2b_file_hashes\'].items():\n if hash in known_hashes and f_rel not in cov_files and use_coverage:\n print("Skipping", f_rel)\n else:\n if token_include_plaintext_source:\n s_include.append("#"*3 +" Content of " + f_rel +" " + "#"*3)\n s_include.append("")\n s_include.append(s[\'pycode\'][f_rel])\n s_include.append("")\n\n if output_dir is None:\n output_dir = os.getcwd()\n\n payload_out_base = report.__class__.__name__ + "_handin"\n\n obtain, possible = results[\'total\']\n vstring = f"_v{report.version}" if report.version is not None else ""\n token = "%s_%i_of_%i%s.token"%(payload_out_base, obtain, possible,vstring)\n token = os.path.normpath(os.path.join(output_dir, token))\n\n save_token(results, "\\n".join(s_include), token)\n\n if not args.autolab:\n print("> Testing token file integrity...", sep="")\n load_token(token)\n print("Done!")\n print(" ")\n print("To get credit for your results, please upload the single unmodified file: ")\n print(">", token)\n\n\ndef save_token(dictionary, plain_text, file_out):\n if plain_text is None:\n plain_text = ""\n if len(plain_text) == 0:\n plain_text = "Start token file"\n plain_text = plain_text.strip()\n b, b_hash = dict2picklestring(dictionary)\n b_l1 = len(b)\n b = "."+b+"."\n b = "\\n".join( textwrap.wrap(b, 180))\n\n out = [plain_text, token_sep, f"{b_hash} {b_l1}", token_sep, b]\n with open(file_out, \'w\') as f:\n f.write("\\n".join(out))\n\n\n\n\ndef source_instantiate(name, report1_source, payload):\n # print("Executing sources", report1_source)\n eval("exec")(report1_source, globals())\n # print("Loaind gpayload..")\n pl = pickle.loads(bytes.fromhex(payload))\n report = eval(name)(payload=pl, strict=True)\n return report\n\n\n__version__ = "0.1.28.8"\n\nfrom cs108.homework1 import add, reverse_list, linear_regression_weights, linear_predict, foo\nimport time\nimport numpy as np\nimport pickle\nimport os\n# from unitgrade.framework import dash\n\ndef mk_bad():\n with open(os.path.dirname(__file__)+"/db.pkl", \'wb\') as f:\n d = {\'x1\': 100, \'x2\': 300}\n pickle.dump(d, f)\n\ndef mk_ok():\n with open(os.path.dirname(__file__)+"/db.pkl", \'wb\') as f:\n d = {\'x1\': 1, \'x2\': 2}\n pickle.dump(d, f)\n\nclass Numpy(UTestCase):\n z = 234\n\n # def __getattr__(self, item):\n # print("hi there ", item)\n # return super().__getattr__(item)\n #\n # def __getattribute__(self, item):\n # print("oh hello sexy. ", item)\n # return super().__getattribute__(item)\n\n @classmethod_dashboard\n def setUpClass(cls) -> None:\n print("Dum di dai, I am running some setup code here.")\n for i in range(10):\n print("Hello world", i)\n print("Set up.") # must be handled seperately.\n # assert False\n\n # @cache\n # def make_primes(self, n):\n # return primes(n)\n\n # def setUp(self) -> None:\n # print("We are doing the setup thing.")\n\n def test_bad(self):\n """\n Hints:\n * Remember to properly de-indent your code.\n * Do more stuff which works.\n """\n # raise Exception("This ended poorly")\n # print("Here we go")\n # return\n # self.assertEqual(1, 1)\n with open(os.path.dirname(__file__)+"/db.pkl", \'rb\') as f:\n d = pickle.load(f)\n # print(d)\n # assert False\n # for i in range(10):\n from tqdm import tqdm\n for i in tqdm(range(100)):\n # print("The current number is", i)\n time.sleep(.01)\n self.assertEqual(1, d[\'x1\'])\n for b in range(10):\n self.assertEqualC(add(3, b))\n\n\n def test_weights(self):\n """\n Hints:\n * Try harder!\n * Check the chapter on linear regression.\n """\n n = 3\n m = 2\n np.random.seed(5)\n # from numpy import asdfaskdfj\n # X = np.random.randn(n, m)\n # y = np.random.randn(n)\n foo()\n # assert 2 == 3\n # raise Exception("Bad exit")\n # self.assertEqual(2, np.random.randint(1000))\n # self.assertEqual(2, np.random.randint(1000))\n # self.assertL2(linear_regression_weights(X, y), msg="the message")\n self.assertEqual(1, 1)\n # self.assertEqual(1,2)\n return "THE RESULT OF THE TEST"\n\n\nclass AnotherTest(UTestCase):\n def test_more(self):\n self.assertEqual(2,2)\n\n def test_even_more(self):\n self.assertEqual(2,2)\n\nimport cs108\nclass Report2(Report):\n title = "CS 101 Report 2"\n questions = [\n (Numpy, 10), (AnotherTest, 20)\n ]\n pack_imports = [cs108]' -report1_payload = '80049502030000000000007d94288c054e756d7079947d942868018c0a7365745570436c6173739486948c0474696d65948694473f38e8000000000068018c08746573745f6261649486948c057469746c6594869468076801680786948c066173736572749486947d94284b004b034b014b044b024b054b034b064b044b074b054b084b064b094b074b0a4b084b0b4b094b0c7568016807869468058694473ff06c5e0000000068018c0c746573745f77656967687473948694680986946811680168118694680c86947d9468016811869468058694473efa400000000000758c0b416e6f7468657254657374947d942868196803869468058694473f1470000000000068198c09746573745f6d6f7265948694680c86947d946819681d869468058694473ed700000000000068198c0e746573745f6576656e5f6d6f7265948694680c86947d9468196823869468058694473ed5000000000000758c06636f6e666967947d948c13626c616b6532625f66696c655f686173686573945d94288c806362363363336235383635306636313037643763663138646136303635666135373835666261626564643135316639653761633335313139323635623039393838623266653335373632303961333932616133656236633134636131316439646335393937343831633531373863313533393665656662313539653163373536948c803434656331613338643134373639626433653234323663386232366539303830356336313361386161653266333966663665633433363133666562363465303739373435323062306536353134353063303637623763633637636631366134313835653736346334383331373763333335303063626563626362336234646466948c803638306336353638323633623832303737313365616434306539323663643265363835336130613936353861386338343738393564363633643730643262343666616163333336396133636564366239623964303436346563316366656465326235306265376432626636313432313638383936663332306338353232313066946573752e' +report1_source = '# from unitgrade import hide\n# from unitgrade import utils\n# import os\n# import lzma\n# import pickle\n\n# DONT\'t import stuff here since install script requires __version__\n\n# def cache_write(object, file_name, verbose=True):\n# # raise Exception("bad")\n# # import compress_pickle\n# dn = os.path.dirname(file_name)\n# if not os.path.exists(dn):\n# os.mkdir(dn)\n# if verbose: print("Writing cache...", file_name)\n# with lzma.open(file_name, \'wb\', ) as f:\n# pickle.dump(object, f)\n# if verbose: print("Done!")\n#\n#\n# def cache_exists(file_name):\n# # file_name = cn_(file_name) if cache_prefix else file_name\n# return os.path.exists(file_name)\n#\n#\n# def cache_read(file_name):\n# # import compress_pickle # Import here because if you import in top the __version__ tag will fail.\n# # file_name = cn_(file_name) if cache_prefix else file_name\n# if os.path.exists(file_name):\n# try:\n# with lzma.open(file_name, \'rb\') as f:\n# return pickle.load(f)\n# except Exception as e:\n# print("Tried to load a bad pickle file at", file_name)\n# print("If the file appears to be automatically generated, you can try to delete it, otherwise download a new version")\n# print(e)\n# # return pickle.load(f)\n# else:\n# return None\n\n\n\nimport re\nimport sys\nimport threading\nimport time\nimport lzma\nimport hashlib\nimport pickle\nimport base64\nimport os\nfrom collections import namedtuple\nfrom io import StringIO\nimport numpy as np\nimport tqdm\nfrom colorama import Fore\nfrom functools import _make_key\nfrom diskcache import Cache\n\n_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])\n\ndef gprint(s):\n print(f"{Fore.LIGHTGREEN_EX}{s}")\n\nmyround = lambda x: np.round(x) # required for obfuscation.\nmsum = lambda x: sum(x)\nmfloor = lambda x: np.floor(x)\n\n"""\nClean up the various output-related helper classes.\n"""\nclass Logger(object):\n def __init__(self, buffer, write_to_stdout=True):\n # assert False\n self.terminal = sys.stdout\n self.write_to_stdout = write_to_stdout\n self.log = buffer\n\n def write(self, message):\n if self.write_to_stdout:\n self.terminal.write(message)\n self.log.write(message)\n\n def flush(self):\n # this flush method is needed for python 3 compatibility.\n pass\n\n\nclass Capturing(list):\n def __init__(self, *args, stdout=None, unmute=False, **kwargs):\n self._stdout = stdout\n self.unmute = unmute\n super().__init__(*args, **kwargs)\n\n def __enter__(self, capture_errors=True): # don\'t put arguments here.\n self._stdout = sys.stdout if self._stdout == None else self._stdout\n self._stringio = StringIO()\n if self.unmute:\n sys.stdout = Logger(self._stringio)\n else:\n sys.stdout = self._stringio\n\n if capture_errors:\n self._sterr = sys.stderr\n sys.sterr = StringIO() # memory hole it\n self.capture_errors = capture_errors\n return self\n\n def __exit__(self, *args):\n self.extend(self._stringio.getvalue().splitlines())\n del self._stringio # free up some memory\n sys.stdout = self._stdout\n if self.capture_errors:\n sys.sterr = self._sterr\n\n\nclass Capturing2(Capturing):\n def __exit__(self, *args):\n lines = self._stringio.getvalue().splitlines()\n txt = "\\n".join(lines)\n numbers = extract_numbers(rm_progress_bar(txt))\n self.extend(lines)\n del self._stringio # free up some memory\n sys.stdout = self._stdout\n if self.capture_errors:\n sys.sterr = self._sterr\n\n self.output = txt\n self.numbers = numbers\n\n\ndef rm_progress_bar(txt):\n # More robust version. Apparently length of bar can depend on various factors, so check for order of symbols.\n nlines = []\n for l in txt.splitlines():\n pct = l.find("%")\n ql = False\n if pct > 0:\n i = l.find("|", pct + 1)\n if i > 0 and l.find("|", i + 1) > 0:\n ql = True\n if not ql:\n nlines.append(l)\n return "\\n".join(nlines)\n\n\nclass ActiveProgress():\n def __init__(self, t, start=True, title="my progress bar", show_progress_bar=True, file=None, mute_stdout=False):\n if file == None:\n file = sys.stdout\n self.file = file\n self.mute_stdout = mute_stdout\n self._running = False\n self.title = title\n self.dt = 0.025\n self.n = max(1, int(np.round(t / self.dt)))\n self.show_progress_bar = show_progress_bar\n self.pbar = None\n\n if start:\n self.start()\n\n def start(self):\n if self.mute_stdout:\n import io\n # from unitgrade.utils import Logger\n self._stdout = sys.stdout\n sys.stdout = Logger(io.StringIO(), write_to_stdout=False)\n\n self._running = True\n if self.show_progress_bar:\n self.thread = threading.Thread(target=self.run)\n self.thread.start()\n self.time_started = time.time()\n\n def terminate(self):\n if not self._running:\n print("Stopping a progress bar which is not running (class unitgrade.utils.ActiveProgress")\n pass\n # raise Exception("Stopping a stopped progress bar. ")\n self._running = False\n if self.show_progress_bar:\n self.thread.join()\n if self.pbar is not None:\n self.pbar.update(1)\n self.pbar.close()\n self.pbar = None\n\n self.file.flush()\n\n if self.mute_stdout:\n import io\n # from unitgrade.utils import Logger\n sys.stdout = self._stdout #= sys.stdout\n\n # sys.stdout = Logger(io.StringIO(), write_to_stdout=False)\n\n return time.time() - self.time_started\n\n def run(self):\n self.pbar = tqdm.tqdm(total=self.n, file=self.file, position=0, leave=False, desc=self.title, ncols=100,\n bar_format=\'{l_bar}{bar}| [{elapsed}<{remaining}]\')\n t_ = time.time()\n for _ in range(self.n - 1): # Don\'t terminate completely; leave bar at 99% done until terminate.\n if not self._running:\n self.pbar.close()\n self.pbar = None\n break\n tc = time.time()\n tic = max(0, self.dt - (tc - t_))\n if tic > 0:\n time.sleep(tic)\n t_ = time.time()\n self.pbar.update(1)\n\n\ndef dprint(first, last, nL, extra = "", file=None, dotsym=\'.\', color=\'white\'):\n if file == None:\n file = sys.stdout\n dot_parts = (dotsym * max(0, nL - len(last) - len(first)))\n print(first + dot_parts, end="", file=file)\n last += extra\n print(last, file=file)\n\n\ndef hide(func):\n return func\n\n\ndef makeRegisteringDecorator(foreignDecorator):\n """\n Returns a copy of foreignDecorator, which is identical in every\n way(*), except also appends a .decorator property to the callable it\n spits out.\n """\n\n def newDecorator(func):\n # Call to newDecorator(method)\n # Exactly like old decorator, but output keeps track of what decorated it\n R = foreignDecorator(func) # apply foreignDecorator, like call to foreignDecorator(method) would have done\n R.decorator = newDecorator # keep track of decorator\n # R.original = func # might as well keep track of everything!\n return R\n\n newDecorator.__name__ = foreignDecorator.__name__\n newDecorator.__doc__ = foreignDecorator.__doc__\n return newDecorator\n\n\nhide = makeRegisteringDecorator(hide)\n\n\ndef extract_numbers(txt):\n numeric_const_pattern = r\'[-+]? (?: (?: \\d* \\. \\d+ ) | (?: \\d+ \\.? ) )(?: [Ee] [+-]? \\d+ ) ?\'\n rx = re.compile(numeric_const_pattern, re.VERBOSE)\n all = rx.findall(txt)\n all = [float(a) if (\'.\' in a or "e" in a) else int(a) for a in all]\n if len(all) > 500:\n print(txt)\n raise Exception("unitgrade_v1.unitgrade_v1.py: Warning, too many numbers!", len(all))\n return all\n\n\ndef cache(foo, typed=False):\n """ Magic cache wrapper\n https://github.com/python/cpython/blob/main/Lib/functools.py\n """\n maxsize = None\n def wrapper(self, *args, **kwargs):\n key = (self.cache_id(), ("@cache", foo.__name__, _make_key(args, kwargs, typed)))\n # print(self._cache.keys())\n # for k in self._cache:\n # print(k)\n if not self._cache_contains(key):\n value = foo(self, *args, **kwargs)\n self._cache_put(key, value)\n else:\n value = self._cache_get(key)\n # This appears to be required since there are two caches. Otherwise, when deploy method is run twice,\n # the cache will not be set correctly.\n self._cache_put(key, value)\n return value\n\n return wrapper\n\n\ndef methodsWithDecorator(cls, decorator):\n """\n Returns all methods in CLS with DECORATOR as the\n outermost decorator.\n\n DECORATOR must be a "registering decorator"; one\n can make any decorator "registering" via the\n makeRegisteringDecorator function.\n\n import inspect\n ls = list(methodsWithDecorator(GeneratorQuestion, deco))\n for f in ls:\n print(inspect.getsourcelines(f) ) # How to get all hidden questions.\n """\n for maybeDecorated in cls.__dict__.values():\n if hasattr(maybeDecorated, \'decorator\'):\n if maybeDecorated.decorator == decorator:\n print(maybeDecorated)\n yield maybeDecorated\n\n\n""" Methods responsible for turning a dictionary into a string that can be pickled or put into a json file. """\ndef dict2picklestring(dd):\n """\n Turns a dictionary into a string with some compression.\n\n :param dd:\n :return:\n """\n b = lzma.compress(pickle.dumps(dd))\n b_hash = hashlib.blake2b(b).hexdigest()\n return base64.b64encode(b).decode("utf-8"), b_hash\n\ndef picklestring2dict(picklestr):\n """ Reverse of the above method: Turns the string back into a dictionary. """\n b = base64.b64decode(picklestr)\n hash = hashlib.blake2b(b).hexdigest()\n dictionary = pickle.loads(lzma.decompress(b))\n return dictionary, hash\n\ntoken_sep = "-"*70 + " ..ooO0Ooo.. " + "-"*70\ndef load_token(file_in):\n """ We put this one here to allow loading of token files for the dashboard. """\n with open(file_in, \'r\') as f:\n s = f.read()\n splt = s.split(token_sep)\n data = splt[-1]\n info = splt[-2]\n head = token_sep.join(splt[:-2])\n plain_text=head.strip()\n hash, l1 = info.split(" ")\n data = "".join( data.strip()[1:-1].splitlines() )\n l1 = int(l1)\n dictionary, b_hash = picklestring2dict(data)\n assert len(data) == l1\n assert b_hash == hash.strip()\n return dictionary, plain_text\n\n\n\n## Key/value store related.\nclass DKPupDB:\n """ This key/value store store artifacts (associated with a specific question) in a dictionary. """\n def __init__(self, artifact_file, use_pupdb=False):\n # Make a double-headed disk cache thingy.\n self.dk = Cache(os.path.dirname(artifact_file)) # Start in this directory.\n self.name_ = os.path.basename(artifact_file[:-5])\n if self.name_ not in self.dk:\n self.dk[self.name_] = dict()\n self.use_pupdb = use_pupdb\n if self.use_pupdb:\n from pupdb.core import PupDB\n self.db_ = PupDB(artifact_file)\n\n def __setitem__(self, key, value):\n if self.use_pupdb:\n self.db_.set(key, value)\n with self.dk.transact():\n d = self.dk[self.name_]\n d[key] = value\n self.dk[self.name_] = d\n self.dk[self.name_ + "-updated"] = True\n\n def __getitem__(self, item):\n v = self.dk[self.name_][item]\n if self.use_pupdb:\n v2 = self.db_.get(item)\n if v != v2:\n print("Mismatch v1, v2 for ", item)\n return v\n\n def keys(self): # This one is also deprecated.\n return tuple(self.dk[self.name_].keys()) #.iterkeys())\n # return self.db_.keys()\n\n def set(self, item, value): # This one is deprecated.\n self[item] = value\n\n def get(self, item, default=None):\n return self[item] if item in self else default\n\n def __contains__(self, item):\n return item in self.dk[self.name_] #keys()\n # return item in self.dk\n\n\nimport io\nimport sys\nimport time\nimport unittest\nfrom unittest.runner import _WritelnDecorator\nimport numpy as np\n\n\nclass UTextResult(unittest.TextTestResult):\n nL = 80\n number = -1 # HAcky way to set question number.\n show_progress_bar = True\n unmute = False # Whether to redirect stdout.\n cc = None\n setUpClass_time = 3 # Estimated time to run setUpClass in TestCase. Must be set externally. See key (("ClassName", "setUpClass"), "time") in _cache.\n\n def __init__(self, stream, descriptions, verbosity):\n super().__init__(stream, descriptions, verbosity)\n self.successes = []\n\n def printErrors(self) -> None:\n # TODO: Fix here. probably also needs to flush stdout.\n self.printErrorList(\'ERROR\', [(test, res[\'stderr\']) for test, res in self.errors])\n self.printErrorList(\'FAIL\', [(test, res[\'stderr\']) for test, res in self.failures])\n\n def addError(self, test, err):\n super(unittest.TextTestResult, self).addError(test, err)\n err = self.errors[-1][1]\n if hasattr(sys.stdout, \'log\'):\n stdout = sys.stdout.log.readlines() # Only works because we set sys.stdout to a unitgrade.Logger\n else:\n stdout = ""\n self.errors[-1] = (self.errors[-1][0], {\'return\': None,\n \'stderr\': err,\n \'stdout\': stdout\n })\n\n if not hasattr(self, \'item_title_print\'):\n # In case setUpClass() fails with an error the short description may not be set. This will fix that problem.\n self.item_title_print = test.shortDescription()\n if self.item_title_print is None: # In case the short description is not set either...\n self.item_title_print = test.id()\n\n\n self.cc_terminate(success=False)\n\n def addFailure(self, test, err):\n super(unittest.TextTestResult, self).addFailure(test, err)\n err = self.failures[-1][1]\n stdout = sys.stdout.log.readlines() # Only works because we set sys.stdout to a unitgrade.Logger\n self.failures[-1] = (self.failures[-1][0], {\'return\': None,\n \'stderr\': err,\n \'stdout\': stdout\n })\n self.cc_terminate(success=False)\n\n\n def addSuccess(self, test: unittest.case.TestCase) -> None:\n msg = None\n stdout = sys.stdout.log.readlines() # Only works because we set sys.stdout to a unitgrade.Logger\n\n if hasattr(test, \'_get_outcome\'):\n o = test._get_outcome()\n if isinstance(o, dict):\n key = (test.cache_id(), "return")\n if key in o:\n msg = test._get_outcome()[key]\n\n # print(sys.stdout.readlines())\n self.successes.append((test, None)) # (test, message) (to be consistent with failures and errors).\n self.successes[-1] = (self.successes[-1][0], {\'return\': msg,\n \'stdout\': stdout,\n \'stderr\': None})\n\n self.cc_terminate()\n\n def cc_terminate(self, success=True):\n if self.show_progress_bar or True:\n tsecs = np.round(self.cc.terminate(), 2)\n self.cc.file.flush()\n ss = self.item_title_print\n\n state = "PASS" if success else "FAILED"\n\n dot_parts = (\'.\' * max(0, self.nL - len(state) - len(ss)))\n if self.show_progress_bar or True:\n print(self.item_title_print + dot_parts, end="", file=self.cc.file)\n else:\n print(dot_parts, end="", file=self.cc.file)\n\n if tsecs >= 0.5:\n state += " (" + str(tsecs) + " seconds)"\n print(state, file=self.cc.file)\n\n def startTest(self, test):\n name = test.__class__.__name__\n if self.testsRun == 0 and hasattr(test.__class__, \'_cache2\'): # Disable this if the class is pure unittest.TestCase\n # This is the first time we are running a test. i.e. we can time the time taken to call setupClass.\n if test.__class__._cache2 is None:\n test.__class__._cache2 = {}\n test.__class__._cache2[((name, \'setUpClass\'), \'time\')] = time.time() - self.t_start\n\n self.testsRun += 1\n item_title = test.shortDescription() # Better for printing (get from cache).\n\n if item_title == None:\n # For unittest framework where getDescription may return None.\n item_title = self.getDescription(test)\n self.item_title_print = " * q%i.%i) %s" % (UTextResult.number + 1, self.testsRun, item_title)\n # if self.show_progress_bar or True:\n estimated_time = test.__class__._cache.get(((name, test._testMethodName), \'time\'), 100) if hasattr(test.__class__, \'_cache\') else 4\n self.cc = ActiveProgress(t=estimated_time, title=self.item_title_print, show_progress_bar=self.show_progress_bar)\n # else:\n # print(self.item_title_print + (\'.\' * max(0, self.nL - 4 - len(self.item_title_print))), end="")\n self._test = test\n # if not self.unmute:\n self._stdout = sys.stdout # Redundant. remove later.\n sys.stdout = Logger(io.StringIO(), write_to_stdout=self.unmute)\n\n def stopTest(self, test):\n # if not self.unmute:\n buff = sys.stdout.log\n sys.stdout = self._stdout # redundant.\n buff.close()\n super().stopTest(test)\n\n def _setupStdout(self):\n if self._previousTestClass == None:\n self.t_start = time.time()\n if hasattr(self.__class__, \'q_title_print\'):\n q_title_print = self.__class__.q_title_print\n else:\n q_title_print = "<unnamed test. See unitgrade.framework.py>"\n\n cc = ActiveProgress(t=self.setUpClass_time, title=q_title_print, show_progress_bar=self.show_progress_bar, mute_stdout=not self.unmute)\n self.cc = cc\n\n\n def _restoreStdout(self): # Used when setting up the test.\n if self._previousTestClass is None:\n q_time = self.cc.terminate()\n q_time = np.round(q_time, 2)\n sys.stdout.flush()\n if self.show_progress_bar:\n print(self.cc.title, end="")\n print(" " * max(0, self.nL - len(self.cc.title)) + (" (" + str(q_time) + " seconds)" if q_time >= 0.5 else ""))\n\n\nclass UTextTestRunner(unittest.TextTestRunner):\n def __init__(self, *args, **kwargs):\n stream = io.StringIO()\n super().__init__(*args, stream=stream, **kwargs)\n\n def _makeResult(self):\n # stream = self.stream # not you!\n stream = sys.stdout\n stream = _WritelnDecorator(stream)\n return self.resultclass(stream, self.descriptions, self.verbosity)\n\nimport importnb\nimport numpy as np\nimport sys\nimport pickle\nimport os\nimport inspect\nimport colorama\nimport unittest\nimport time\nimport textwrap\nimport urllib.parse\nimport requests\nimport ast\nimport numpy\nfrom unittest.case import TestCase\n\n\ncolorama.init(autoreset=True) # auto resets your settings after every output\nnumpy.seterr(all=\'raise\')\n\ndef setup_dir_by_class(C, base_dir):\n name = C.__class__.__name__\n return base_dir, name\n\n\n_DASHBOARD_COMPLETED_MESSAGE = "Dashboard> Evaluation completed."\n\n# Consolidate this code.\nclass classmethod_dashboard(classmethod):\n def __init__(self, f):\n def dashboard_wrap(cls: UTestCase):\n if not cls._generate_artifacts:\n f(cls)\n return\n db = DKPupDB(cls._artifact_file_for_setUpClass())\n r = np.random.randint(1000 * 1000)\n db.set(\'run_id\', r)\n db.set(\'coverage_files_changed\', None)\n\n state_ = \'fail\'\n try:\n _stdout = sys.stdout\n _stderr = sys.stderr\n std_capture = StdCapturing(stdout=sys.stdout, stderr=sys.stderr, db=db, mute=False)\n\n # Run this unittest and record all of the output.\n # This is probably where we should hijack the stdout output and save it -- after all, this is where the test is actually run.\n # sys.stdout = stdout_capture\n sys.stderr = std_capture.dummy_stderr\n sys.stdout = std_capture.dummy_stdout\n db.set("state", "running")\n f(cls)\n state_ = \'pass\'\n except Exception as e:\n from werkzeug.debug.tbtools import DebugTraceback, _process_traceback\n state_ = \'fail\'\n db.set(\'state\', state_)\n exi = e\n dbt = DebugTraceback(exi)\n sys.stderr.write(dbt.render_traceback_text())\n html = dbt.render_traceback_html(include_title="hello world")\n db.set(\'wz_stacktrace\', html)\n raise e\n finally:\n db.set(\'state\', state_)\n std_capture.dummy_stdout.write_mute(_DASHBOARD_COMPLETED_MESSAGE)\n sys.stdout = _stdout\n sys.stderr = _stderr\n std_capture.close()\n super().__init__(dashboard_wrap)\n\nclass Report:\n title = "report title"\n abbreviate_questions = False # Should the test items start with \'Question ...\' or just be q1).\n version = None # A version number of the report (1.0). Used to compare version numbers with online resources.\n url = None # Remote location of this problem.\n\n questions = []\n pack_imports = []\n individual_imports = []\n\n _remote_check_cooldown_seconds = 1 # Seconds between remote check of report.\n nL = 120 # Maximum line width\n _config = None # Private variable. Used when collecting results from student computers. Should only be read/written by teacher and never used for regular evaluation.\n _setup_mode = False # True if test is being run in setup-mode, i.e. will not fail because of bad configurations, etc.\n\n @classmethod\n def reset(cls):\n for (q, _) in cls.questions:\n if hasattr(q, \'reset\'):\n q.reset()\n\n @classmethod\n def mfile(clc):\n return inspect.getfile(clc)\n\n def _file(self):\n return inspect.getfile(type(self))\n\n def _artifact_file(self):\n """ File for the artifacts DB (thread safe). This file is optinal. Note that it is a pupdb database file.\n Note the file is shared between all sub-questions. """\n return os.path.join(os.path.dirname(self._file()), "unitgrade_data/main_config_"+ os.path.basename(self._file()[:-3]) + ".artifacts.pkl")\n\n def _is_run_in_grade_mode(self):\n """ True if this report is being run as part of a grade run. """\n return self._file().endswith("_grade.py") # Not sure I love this convention.\n\n def _import_base_relative(self):\n if hasattr(self.pack_imports[0], \'__path__\'):\n root_dir = self.pack_imports[0].__path__[0]\n else:\n root_dir = self.pack_imports[0].__file__\n\n root_dir = os.path.dirname(root_dir)\n relative_path = os.path.relpath(self._file(), root_dir)\n modules = os.path.normpath(relative_path[:-3]).split(os.sep)\n relative_path = relative_path.replace("\\\\", "/")\n return root_dir, relative_path, modules\n\n def __init__(self, strict=False, payload=None):\n working_directory = os.path.abspath(os.path.dirname(self._file()))\n self.wdir, self.name = setup_dir_by_class(self, working_directory)\n # self.computed_answers_file = os.path.join(self.wdir, self.name + "_resources_do_not_hand_in.dat")\n for (q, _) in self.questions:\n q.nL = self.nL # Set maximum line length.\n\n if payload is not None:\n self.set_payload(payload, strict=strict)\n\n def main(self, verbosity=1):\n # Run all tests using standard unittest (nothing fancy).\n loader = unittest.TestLoader()\n for q, _ in self.questions:\n start = time.time() #\n suite = loader.loadTestsFromTestCase(q)\n unittest.TextTestRunner(verbosity=verbosity).run(suite)\n total = time.time() - start\n q.time = total\n\n def _setup_answers(self, with_coverage=False, verbose=True):\n if with_coverage:\n for q, _ in self.questions:\n q._with_coverage = True\n q._report = self\n for q, _ in self.questions:\n q._setup_answers_mode = True\n # q._generate_artifacts = False # Disable artifact generation when the report is being set up.\n\n evaluate_report_student(self, unmute=verbose, noprogress=not verbose, generate_artifacts=False) # Disable artifact generation.\n\n # self.main() # Run all tests in class just to get that out of the way...\n report_cache = {}\n for q, _ in self.questions:\n # print(self.questions)\n if hasattr(q, \'_save_cache\'):\n q()._save_cache()\n # print("q is", q())\n report_cache[q.__qualname__] = q._cache2\n else:\n report_cache[q.__qualname__] = {\'no cache see _setup_answers in framework.py\': True}\n if with_coverage:\n for q, _ in self.questions:\n q._with_coverage = False\n\n # report_cache is saved on a per-question basis.\n # it could also contain additional information such as runtime metadata etc. This may not be appropriate to store with the invidivual questions(?).\n # In this case, the function should be re-defined.\n return report_cache\n\n def set_payload(self, payloads, strict=False):\n for q, _ in self.questions:\n q._cache = payloads[q.__qualname__]\n self._config = payloads[\'config\']\n\n def _check_remote_versions(self):\n if self.url is None:\n return\n url = self.url\n if not url.endswith("/"):\n url += "/"\n snapshot_file = os.path.dirname(self._file()) + "/unitgrade_data/.snapshot"\n if os.path.isfile(snapshot_file):\n with open(snapshot_file, \'r\') as f:\n t = f.read()\n if (time.time() - float(t)) < self._remote_check_cooldown_seconds:\n return\n\n if self.url.startswith("https://gitlab"):\n # Try to turn url into a \'raw\' format.\n # "https://gitlab.compute.dtu.dk/tuhe/unitgrade_private/-/raw/master/examples/autolab_example_py_upload/instructor/cs102_autolab/report2_test.py?inline=false"\n # url = self.url\n url = url.replace("-/tree", "-/raw")\n # print(url)\n # "https://gitlab.compute.dtu.dk/tuhe/unitgrade_private/-/tree/master/examples/autolab_example_py_upload/instructor/cs102_autolab"\n # "https://gitlab.compute.dtu.dk/tuhe/unitgrade_private/-/raw/master/examples/autolab_example_py_upload/instructor/report2_test.py?inline=false"\n # "https://gitlab.compute.dtu.dk/tuhe/unitgrade_private/-/raw/master/examples/autolab_example_py_upload/instructor/cs102_autolab/report2_test.py?inline=false"\n raw_url = urllib.parse.urljoin(url, os.path.basename(self._file()) + "?inline=false")\n # print("Is this file run in local mode?", self._is_run_in_grade_mode())\n if self._is_run_in_grade_mode():\n remote_source = requests.get(raw_url).text\n with open(self._file(), \'r\') as f:\n local_source = f.read()\n if local_source != remote_source:\n print("\\nThe local version of this report is not identical to the remote version which can be found at")\n print(self.url)\n print("The most likely reason for this is that the remote version was updated by the teacher due to some issue.")\n print("You should check if there was an announcement and update the test to the most recent version; most likely")\n print("This can be done by running the command")\n print("> git pull")\n print("You can find the most recent code here:")\n print(self.url)\n raise Exception(f"Version of grade script does not match the remote version. Please update using git pull")\n else:\n text = requests.get(raw_url).text\n node = ast.parse(text)\n classes = [n for n in node.body if isinstance(n, ast.ClassDef) if n.name == self.__class__.__name__][0]\n for b in classes.body:\n # print(b.)\n if b.targets[0].id == "version":\n # print(b)\n # print(b.value)\n version_remote = b.value.value\n break\n if version_remote != self.version:\n print("\\nThe version of this report", self.version, "does not match the version of the report on git", version_remote)\n print("The most likely reason for this is that the remote version was updated by the teacher due to some issue.")\n print("You should check if there was an announcement and update the test to the most recent version; most likely")\n print("This can be done by running the command")\n print("> git pull")\n print("You can find the most recent code here:")\n print(self.url)\n raise Exception(f"Version of test on remote is {version_remote}, which is different than this version of the test {self.version}. Please update your test to the most recent version.")\n\n for (q,_) in self.questions:\n qq = q(skip_remote_check=True)\n cfile = q._cache_file()\n\n relpath = os.path.relpath(cfile, os.path.dirname(self._file()))\n relpath = relpath.replace("\\\\", "/")\n raw_url = urllib.parse.urljoin(url, relpath + "?inline=false")\n # requests.get(raw_url)\n\n with open(cfile, \'rb\') as f:\n b1 = f.read()\n\n b2 = requests.get(raw_url).content\n if b1 != b2:\n print("\\nQuestion ", qq.title, "relies on the data file", cfile)\n print("However, it appears that this file is missing or in a different version than the most recent found here:")\n print(self.url)\n print("The most likely reason for this is that the remote version was updated by the teacher due to some issue.")\n print("You should check if there was an announcement and update the test to the most recent version; most likely")\n print("This can be done by simply running the command")\n print("> git pull")\n print("to avoid running bad tests against good code, the program will now stop. Please update and good luck!")\n raise Exception("The data file for the question", qq.title, "did not match remote source found on git. The test will therefore automatically fail. Please update your test/data files.")\n\n t = time.time()\n if os.path.isdir(os.path.dirname(self._file()) + "/unitgrade_data"):\n with open(snapshot_file, \'w\') as f:\n f.write(f"{t}")\n\ndef get_hints(ss):\n """ Extract all blocks of the forms:\n\n Hints:\n bla-bla.\n\n and returns the content unaltered.\n """\n if ss == None:\n return None\n try:\n ss = textwrap.dedent(ss)\n ss = ss.replace(\'\'\'"""\'\'\', "").strip()\n hints = ["hints:", "hint:"]\n indexes = [ss.lower().find(h) for h in hints]\n j = np.argmax(indexes)\n if indexes[j] == -1:\n return None\n h = hints[j]\n ss = ss[ss.lower().find(h) + len(h) + 1:]\n ss = "\\n".join([l for l in ss.split("\\n") if not l.strip().startswith(":")])\n ss = textwrap.dedent(ss).strip()\n # if ss.startswith(\'*\'):\n # ss = ss[1:].strip()\n return ss\n except Exception as e:\n print("bad hints", ss, e)\n\n\nclass UTestCase(unittest.TestCase):\n # a = 234\n _outcome = None # A dictionary which stores the user-computed outcomes of all the tests. This differs from the cache.\n _cache = None # Read-only cache. Ensures method always produce same result.\n _cache2 = None # User-written cache.\n _with_coverage = False\n _covcache = None # Coverage cache. Written to if _with_coverage is true.\n _report = None # The report used. This is very, very hacky and should always be None. Don\'t rely on it!\n _run_in_report_mode = True\n\n _generate_artifacts = True # Whether the file will generate the artifact .json files. This is used in the _grade-script mode.\n # If true, the tests will not fail when cache is used. This is necesary since otherwise the cache will not be updated\n # during setup, and the deploy script must be run many times.\n _setup_answers_mode = False\n\n def capture(self):\n if hasattr(self, \'_stdout\') and self._stdout is not None:\n file = self._stdout\n else:\n file = sys.stdout\n return Capturing2(stdout=file)\n\n @classmethod\n def question_title(cls):\n """ Return the question title """\n if cls.__doc__ is not None:\n title = cls.__doc__.strip().splitlines()[0].strip()\n if not (title.startswith("Hints:") or title.startswith("Hint:") ):\n return title\n return cls.__qualname__\n\n def run(self, result):\n # print("Run called in test framework...", self._generate_artifacts)\n if not self._generate_artifacts:\n return super().run(result)\n\n db = DKPupDB(self._artifact_file())\n db.set("state", "running")\n db.set(\'run_id\', np.random.randint(1000*1000))\n db.set(\'coverage_files_changed\', None)\n\n\n _stdout = sys.stdout\n _stderr = sys.stderr\n\n std_capture = StdCapturing(stdout=sys.stdout, stderr=sys.stderr, db=db, mute=False)\n\n # stderr_capture = StdCapturing(sys.stderr, db=db)\n # std_err_capture = StdCapturing(sys.stderr, "stderr", db=db)\n state_ = None\n try:\n # Run this unittest and record all of the output.\n # This is probably where we should hijack the stdout output and save it -- after all, this is where the test is actually run.\n # sys.stdout = stdout_capture\n sys.stderr = std_capture.dummy_stderr\n sys.stdout = std_capture.dummy_stdout\n\n result_ = TestCase.run(self, result)\n\n from werkzeug.debug.tbtools import DebugTraceback, _process_traceback\n # print(result_._excinfo[0])\n actual_errors = []\n for test, err in self._error_fed_during_run:\n if err is None:\n continue\n else:\n import traceback\n # traceback.print_tb(err[2])\n actual_errors.append(err)\n\n if len(actual_errors) > 0:\n ex, exi, tb = actual_errors[0]\n exi.__traceback__ = tb\n dbt = DebugTraceback(exi)\n sys.stderr.write(dbt.render_traceback_text())\n html = dbt.render_traceback_html(include_title="hello world")\n db.set(\'wz_stacktrace\', html)\n # db.set(\'state\', \'fail\')\n state_ = "fail"\n else:\n state_ = "pass"\n except Exception as e:\n state_ = "fail"\n import traceback\n traceback.print_exc()\n raise e\n finally:\n db.set(\'state\', state_)\n std_capture.dummy_stdout.write_mute(_DASHBOARD_COMPLETED_MESSAGE)\n sys.stdout = _stdout\n sys.stderr = _stderr\n std_capture.close()\n return result_\n\n def _callSetUp(self):\n if self._with_coverage:\n if self._covcache is None:\n self._covcache = {}\n import coverage\n self.cov = coverage.Coverage(data_file=None)\n self.cov.start()\n self.setUp()\n\n def _callTearDown(self):\n self.tearDown()\n # print("Teardown.")\n if self._with_coverage:\n # print("with cov")\n from pathlib import Path\n from snipper import snipper_main\n try:\n self.cov.stop()\n except Exception as e:\n print("Something went wrong while tearing down coverage test")\n print(e)\n data = self.cov.get_data()\n base, _, _ = self._report._import_base_relative()\n for file in data.measured_files():\n file = os.path.normpath(file)\n root = Path(base)\n child = Path(file)\n if root in child.parents:\n # print("Reading file", child)\n with open(child, \'r\') as f:\n s = f.read()\n lines = s.splitlines()\n garb = \'GARBAGE\'\n lines2 = snipper_main.censor_code(lines, keep=True)\n # print("\\n".join(lines2))\n if len(lines) != len(lines2):\n for k in range(len(lines)):\n print(k, ">", lines[k], "::::::::", lines2[k])\n print("Snipper failure; line lenghts do not agree. Exiting..")\n print(child, "len(lines) == len(lines2)", len(lines), len(lines2))\n import sys\n sys.exit()\n\n assert len(lines) == len(lines2)\n for ll in data.contexts_by_lineno(file):\n l = ll-1\n if l < len(lines2) and lines2[l].strip() == garb:\n # print("Got a hit at l", l)\n rel = os.path.relpath(child, root)\n cc = self._covcache\n j = 0\n for j in range(l, -1, -1):\n if "def" in lines2[j] or "class" in lines2[j]:\n break\n from snipper.legacy import gcoms\n\n fun = lines2[j]\n comments, _ = gcoms("\\n".join(lines2[j:l]))\n if rel not in cc:\n cc[rel] = {}\n cc[rel][fun] = (l, "\\n".join(comments))\n # print("found", rel, fun)\n self._cache_put((self.cache_id(), \'coverage\'), self._covcache)\n\n def shortDescriptionStandard(self):\n sd = super().shortDescription()\n if sd is None or sd.strip().startswith("Hints:") or sd.strip().startswith("Hint:"):\n sd = self._testMethodName\n return sd\n\n def shortDescription(self):\n sd = self.shortDescriptionStandard()\n title = self._cache_get((self.cache_id(), \'title\'), sd)\n return title if title is not None else sd\n\n @property\n def title(self):\n return self.shortDescription()\n\n @title.setter\n def title(self, value):\n self._cache_put((self.cache_id(), \'title\'), value)\n\n def _get_outcome(self):\n if not hasattr(self.__class__, \'_outcome\') or self.__class__._outcome is None:\n self.__class__._outcome = {}\n return self.__class__._outcome\n\n def _callTestMethod(self, testMethod):\n t = time.time()\n self._ensure_cache_exists() # Make sure cache is there.\n if self._testMethodDoc is not None:\n self._cache_put((self.cache_id(), \'title\'), self.shortDescriptionStandard())\n\n self._cache2[(self.cache_id(), \'assert\')] = {}\n res = testMethod()\n elapsed = time.time() - t\n self._get_outcome()[ (self.cache_id(), "return") ] = res\n self._cache_put((self.cache_id(), "time"), elapsed)\n\n\n def cache_id(self):\n c = self.__class__.__qualname__\n m = self._testMethodName\n return c, m\n\n def __init__(self, *args, skip_remote_check=False, **kwargs):\n super().__init__(*args, **kwargs)\n self._load_cache()\n self._assert_cache_index = 0\n # Perhaps do a sanity check here to see if the cache is up to date? To do that, we must make sure the\n # cache exists locally.\n # Find the report class this class is defined within.\n if skip_remote_check:\n return\n import importlib, inspect\n found_reports = []\n # print("But do I have report", self._report)\n # print("I think I am module", self.__module__)\n # print("Importlib says", importlib.import_module(self.__module__))\n # This will delegate you to the wrong main clsas when running in grade mode.\n for name, cls in inspect.getmembers(importlib.import_module(self.__module__), inspect.isclass):\n # print("checking", cls)\n if issubclass(cls, Report):\n for q,_ in cls.questions:\n if q == self.__class__:\n found_reports.append(cls)\n if len(found_reports) == 0:\n pass # This case occurs when the report _grade script is being run.\n # raise Exception("This question is not a member of a report. Very, very odd.")\n if len(found_reports) > 1:\n raise Exception("This question is a member of multiple reports. That should not be the case -- don\'t get too creative.")\n if len(found_reports) > 0:\n report = found_reports[0]\n report()._check_remote_versions()\n\n\n def _ensure_cache_exists(self):\n if not hasattr(self.__class__, \'_cache\') or self.__class__._cache == None:\n self.__class__._cache = dict()\n if not hasattr(self.__class__, \'_cache2\') or self.__class__._cache2 == None:\n self.__class__._cache2 = dict()\n\n def _cache_get(self, key, default=None):\n self._ensure_cache_exists()\n return self.__class__._cache.get(key, default)\n\n def _cache_put(self, key, value):\n self._ensure_cache_exists()\n self.__class__._cache2[key] = value\n\n def _cache_contains(self, key):\n self._ensure_cache_exists()\n return key in self.__class__._cache\n\n def get_expected_test_value(self):\n key = (self.cache_id(), \'assert\')\n id = self._assert_cache_index\n cache = self._cache_get(key)\n _expected = cache.get(id, f"Key {id} not found in cache; framework files missing. Please run deploy()")\n return _expected\n\n def wrap_assert(self, assert_fun, first, *args, **kwargs):\n key = (self.cache_id(), \'assert\')\n if not self._cache_contains(key):\n print("Warning, framework missing", key)\n self.__class__._cache[key] = {} # A new dict. We manually insert it because we have to use that the dict is mutable.\n cache = self._cache_get(key)\n id = self._assert_cache_index\n _expected = cache.get(id, f"Key {id} not found in cache; framework files missing. Please run deploy()")\n if not id in cache:\n print("Warning, framework missing cache index", key, "id =", id, " - The test will be skipped for now.")\n if self._setup_answers_mode:\n _expected = first # Bypass by setting equal to first. This is in case multiple self.assertEqualC\'s are run in a row and have to be set.\n\n # The order of these calls is important. If the method assert fails, we should still store the correct result in cache.\n cache[id] = first\n self._cache_put(key, cache)\n self._assert_cache_index += 1\n if not self._setup_answers_mode:\n assert_fun(first, _expected, *args, **kwargs)\n else:\n try:\n assert_fun(first, _expected, *args, **kwargs)\n except Exception as e:\n print("Mumble grumble. Cache function failed during class setup. Most likely due to old cache. Re-run deploy to check it pass.", id)\n print("> first", first)\n print("> expected", _expected)\n print(e)\n\n\n def assertEqualC(self, first, msg=None):\n self.wrap_assert(self.assertEqual, first, msg)\n\n def _shape_equal(self, first, second):\n a1 = np.asarray(first).squeeze()\n a2 = np.asarray(second).squeeze()\n msg = None\n msg = "" if msg is None else msg\n if len(msg) > 0:\n msg += "\\n"\n self.assertEqual(a1.shape, a2.shape, msg=msg + "Dimensions of input data does not agree.")\n assert(np.all(np.isinf(a1) == np.isinf(a2))) # Check infinite part.\n a1[np.isinf(a1)] = 0\n a2[np.isinf(a2)] = 0\n diff = np.abs(a1 - a2)\n return diff\n\n def assertLinf(self, first, second=None, tol=1e-5, msg=None):\n """ Test in the L_infinity norm.\n :param first:\n :param second:\n :param tol:\n :param msg:\n :return:\n """\n if second is None:\n return self.wrap_assert(self.assertLinf, first, tol=tol, msg=msg)\n else:\n diff = self._shape_equal(first, second)\n np.testing.assert_allclose(first, second, atol=tol)\n \n max_diff = max(diff.flat)\n if max_diff >= tol:\n from unittest.util import safe_repr\n # msg = f\'{safe_repr(first)} != {safe_repr(second)} : Not equal within tolerance {tol}\'\n # print(msg)\n # np.testing.assert_almost_equal\n # import numpy as np\n print(f"|first - second|_max = {max_diff} > {tol} ")\n np.testing.assert_almost_equal(first, second)\n # If the above fail, make sure to throw an error:\n self.assertFalse(max_diff >= tol, msg=f\'Input arrays are not equal within tolerance {tol}\')\n # self.assertEqual(first, second, msg=f\'Not equal within tolerance {tol}\')\n\n def assertL2(self, first, second=None, tol=1e-5, msg=None, relative=False):\n if second is None:\n return self.wrap_assert(self.assertL2, first, tol=tol, msg=msg, relative=relative)\n else:\n # We first test using numpys build-in testing method to see if one coordinate deviates a great deal.\n # This gives us better output, and we know that the coordinate wise difference is lower than the norm difference.\n if not relative:\n np.testing.assert_allclose(first, second, atol=tol)\n diff = self._shape_equal(first, second)\n diff = ( ( np.asarray( diff.flatten() )**2).sum() )**.5\n\n scale = (2/(np.linalg.norm(np.asarray(first).flat) + np.linalg.norm(np.asarray(second).flat)) ) if relative else 1\n max_diff = diff*scale\n if max_diff >= tol:\n msg = "" if msg is None else msg\n print(f"|first - second|_2 = {max_diff} > {tol} ")\n # Deletage to numpy. Let numpy make nicer messages.\n np.testing.assert_almost_equal(first, second) # This function does not take a msg parameter.\n # Make sure to throw an error no matter what.\n self.assertFalse(max_diff >= tol, msg=f\'Input arrays are not equal within tolerance {tol}\')\n # self.assertEqual(first, second, msg=msg + f"Not equal within tolerance {tol}")\n\n @classmethod\n def _cache_file(cls):\n return os.path.dirname(inspect.getabsfile(cls)) + "/unitgrade_data/" + cls.__name__ + ".pkl"\n\n @classmethod\n def _artifact_file_for_setUpClass(cls):\n file = os.path.join(os.path.dirname(cls._cache_file()), ""+cls.__name__+"-setUpClass.json")\n print("_artifact_file_for_setUpClass(cls): will return", file, "__class__", cls)\n # cf = os.path.dirname(inspect.getabsfile(cls)) + "/unitgrade_data/" + cls.__name__\n return file\n\n def _artifact_file(self):\n """ File for the artifacts DB (thread safe). This file is optinal. Note that it is a pupdb database file.\n Note the file is shared between all sub-questions. """\n return os.path.join(os.path.dirname(self.__class__._cache_file()), \'-\'.join(self.cache_id()) + ".json")\n\n def _save_cache(self):\n # get the class name (i.e. what to save to).\n cfile = self.__class__._cache_file()\n if not os.path.isdir(os.path.dirname(cfile)):\n os.makedirs(os.path.dirname(cfile))\n\n if hasattr(self.__class__, \'_cache2\'):\n with open(cfile, \'wb\') as f:\n pickle.dump(self.__class__._cache2, f)\n\n # But you can also set cache explicitly.\n def _load_cache(self):\n if self._cache is not None: # Cache already loaded. We will not load it twice.\n return\n # raise Exception("Loaded cache which was already set. What is going on?!")\n cfile = self.__class__._cache_file()\n if os.path.exists(cfile):\n try:\n with open(cfile, \'rb\') as f:\n data = pickle.load(f)\n self.__class__._cache = data\n except Exception as e:\n print("Cache file did not exist:", cfile)\n print(e)\n else:\n print("Warning! data file not found", cfile)\n\n def _get_coverage_files(self):\n key = (self.cache_id(), \'coverage\')\n # CC = None\n # if self._cache_contains(key):\n return self._cache_get(key, []) # Anything wrong with the empty list?\n # return CC\n\n def _get_hints(self):\n """\n This code is run when the test is set up to generate the hints and store them in an artifact file. It may be beneficial to simple compute them beforehand\n and store them in the local unitgrade pickle file. This code is therefore expected to superceede the alterative code later.\n """\n hints = []\n # print("Getting hint")\n key = (self.cache_id(), \'coverage\')\n if self._cache_contains(key):\n CC = self._cache_get(key)\n # cl, m = self.cache_id()\n # print("Getting hint using", CC)\n # Insert newline to get better formatting.\n # gprint(\n # f"\\n> An error occured during the test: {cl}.{m}. The following files/methods has code in them you are supposed to edit and may therefore be the cause of the problem:")\n for file in CC:\n rec = CC[file]\n # gprint(f"> * {file}")\n for l in rec:\n _, comments = CC[file][l]\n hint = get_hints(comments)\n\n if hint != None:\n hints.append((hint, file, l))\n\n doc = self._testMethodDoc\n # print("doc", doc)\n if doc is not None:\n hint = get_hints(self._testMethodDoc)\n if hint is not None:\n hints = [(hint, None, self.cache_id()[1])] + hints\n\n return hints\n\n def _feedErrorsToResult(self, result, errors):\n """ Use this to show hints on test failure.\n It feeds error to the result -- so if there are errors, they will crop up here\n """\n self._error_fed_during_run = errors.copy() # import to copy the error list.\n\n # result._test._error_fed_during_run = errors.copy()\n\n if not isinstance(result, UTextResult):\n er = [e for e, v in errors if v != None]\n # print("Errors are", errors)\n if len(er) > 0:\n hints = []\n key = (self.cache_id(), \'coverage\')\n if self._cache_contains(key):\n CC = self._cache_get(key)\n cl, m = self.cache_id()\n # Insert newline to get better formatting.\n gprint(f"\\n> An error occured during the test: {cl}.{m}. The following files/methods has code in them you are supposed to edit and may therefore be the cause of the problem:")\n for file in CC:\n rec = CC[file]\n gprint(f"> * {file}")\n for l in rec:\n _, comments = CC[file][l]\n hint = get_hints(comments)\n\n if hint != None:\n hints.append((hint, file, l) )\n gprint(f"> - {l}")\n\n er = er[0]\n\n doc = er._testMethodDoc\n # print("doc", doc)\n if doc is not None:\n hint = get_hints(er._testMethodDoc)\n if hint is not None:\n hints = [(hint, None, self.cache_id()[1] )] + hints\n if len(hints) > 0:\n # print(hints)\n for hint, file, method in hints:\n s = (f"\'{method.strip()}\'" if method is not None else "")\n if method is not None and file is not None:\n s += " in "\n try:\n s += (file.strip() if file is not None else "")\n gprint(">")\n gprint("> Hints (from " + s + ")")\n gprint(textwrap.indent(hint, "> "))\n except Exception as e:\n print("Bad stuff in hints. ")\n print(hints)\n # result._last_errors = errors\n super()._feedErrorsToResult(result, errors)\n b = 234\n\n def startTestRun(self):\n super().startTestRun()\n\nclass Required:\n pass\n\nclass ParticipationTest(UTestCase,Required):\n max_group_size = None\n students_in_group = None\n workload_assignment = {\'Question 1\': [1, 0, 0]}\n\n def test_students(self):\n pass\n\n def test_workload(self):\n pass\n\n# 817, 705\nclass NotebookTestCase(UTestCase):\n notebook = None\n _nb = None\n @classmethod\n def setUpClass(cls) -> None:\n with Capturing():\n cls._nb = importnb.Notebook.load(cls.notebook)\n\n @property\n def nb(self):\n return self.__class__._nb\n # 870.\n\nimport hashlib\nimport io\nimport tokenize\nimport numpy as np\nfrom tabulate import tabulate\nfrom datetime import datetime\nimport pyfiglet\nimport unittest\nimport inspect\nimport os\nimport argparse\nimport time\n\nparser = argparse.ArgumentParser(description=\'Evaluate your report.\', epilog="""Example: \nTo run all tests in a report: \n\n> python assignment1_dp.py\n\nTo run only question 2 or question 2.1\n\n> python assignment1_dp.py -q 2\n> python assignment1_dp.py -q 2.1\n\nNote this scripts does not grade your report. To grade your report, use:\n\n> python report1_grade.py\n\nFinally, note that if your report is part of a module (package), and the report script requires part of that package, the -m option for python may be useful.\nFor instance, if the report file is in Documents/course_package/report3_complete.py, and `course_package` is a python package, then change directory to \'Documents/` and run:\n\n> python -m course_package.report1\n\nsee https://docs.python.org/3.9/using/cmdline.html\n""", formatter_class=argparse.RawTextHelpFormatter)\nparser.add_argument(\'-q\', nargs=\'?\', type=str, default=None, help=\'Only evaluate this question (e.g.: -q 2)\')\nparser.add_argument(\'--showexpected\', action="store_true", help=\'Show the expected/desired result\')\nparser.add_argument(\'--showcomputed\', action="store_true", help=\'Show the answer your code computes\')\nparser.add_argument(\'--unmute\', action="store_true", help=\'Show result of print(...) commands in code\')\nparser.add_argument(\'--passall\', action="store_true", help=\'Automatically pass all tests. Useful when debugging.\')\nparser.add_argument(\'--noprogress\', action="store_true", help=\'Disable progress bars.\')\n\ndef evaluate_report_student(report, question=None, qitem=None, unmute=None, passall=None, ignore_missing_file=False,\n show_tol_err=False, show_privisional=True, noprogress=None,\n generate_artifacts=True):\n args = parser.parse_args()\n if noprogress is None:\n noprogress = args.noprogress\n\n if question is None and args.q is not None:\n question = args.q\n if "." in question:\n question, qitem = [int(v) for v in question.split(".")]\n else:\n question = int(question)\n\n if hasattr(report, "computed_answer_file") and not os.path.isfile(report.computed_answers_file) and not ignore_missing_file:\n raise Exception("> Error: The pre-computed answer file", os.path.abspath(report.computed_answers_file), "does not exist. Check your package installation")\n\n if unmute is None:\n unmute = args.unmute\n if passall is None:\n passall = args.passall\n\n results, table_data = evaluate_report(report, question=question, show_progress_bar=not unmute and not noprogress, qitem=qitem,\n verbose=False, passall=passall, show_expected=args.showexpected, show_computed=args.showcomputed,unmute=unmute,\n show_tol_err=show_tol_err,\n generate_artifacts=generate_artifacts)\n\n\n if question is None and show_privisional:\n print("Provisional evaluation")\n tabulate(table_data)\n table = table_data\n print(tabulate(table))\n print(" ")\n\n fr = inspect.getouterframes(inspect.currentframe())[1].filename\n gfile = os.path.basename(fr)[:-3] + "_grade.py"\n if os.path.exists(gfile):\n print("Note your results have not yet been registered. \\nTo register your results, please run the file:")\n print(">>>", gfile)\n print("In the same manner as you ran this file.")\n\n\n return results\n\n\ndef upack(q):\n # h = zip([(i[\'w\'], i[\'possible\'], i[\'obtained\']) for i in q.values()])\n h =[(i[\'w\'], i[\'possible\'], i[\'obtained\']) for i in q.values()]\n h = np.asarray(h)\n return h[:,0], h[:,1], h[:,2],\n\nclass SequentialTestLoader(unittest.TestLoader):\n def getTestCaseNames(self, testCaseClass):\n test_names = super().getTestCaseNames(testCaseClass)\n # testcase_methods = list(testCaseClass.__dict__.keys())\n ls = []\n for C in testCaseClass.mro():\n if issubclass(C, unittest.TestCase):\n ls = list(C.__dict__.keys()) + ls\n testcase_methods = ls\n test_names.sort(key=testcase_methods.index)\n return test_names\n\ndef evaluate_report(report, question=None, qitem=None, passall=False, verbose=False, show_expected=False, show_computed=False,unmute=False, show_help_flag=True, silent=False,\n show_progress_bar=True,\n show_tol_err=False,\n generate_artifacts=True, # Generate the artifact .json files. These are exclusively used by the dashboard.\n big_header=True):\n\n now = datetime.now()\n if big_header:\n ascii_banner = pyfiglet.figlet_format("UnitGrade", font="doom")\n b = "\\n".join( [l for l in ascii_banner.splitlines() if len(l.strip()) > 0] )\n else:\n b = "Unitgrade"\n dt_string = now.strftime("%d/%m/%Y %H:%M:%S")\n print(b + " v" + __version__ + ", started: " + dt_string+ "\\n")\n # print("Started: " + dt_string)\n report._check_remote_versions() # Check (if report.url is present) that remote files exist and are in sync.\n s = report.title\n if hasattr(report, "version") and report.version is not None:\n s += f" version {report.version}"\n print(s, "(use --help for options)" if show_help_flag else "")\n # print(f"Loaded answers from: ", report.computed_answers_file, "\\n")\n table_data = []\n t_start = time.time()\n score = {}\n loader = SequentialTestLoader()\n\n for n, (q, w) in enumerate(report.questions):\n q._generate_artifacts = generate_artifacts # Set whether artifact .json files will be generated.\n if question is not None and n+1 != question:\n continue\n suite = loader.loadTestsFromTestCase(q)\n qtitle = q.question_title() if hasattr(q, \'question_title\') else q.__qualname__\n if not report.abbreviate_questions:\n q_title_print = "Question %i: %s"%(n+1, qtitle)\n else:\n q_title_print = "q%i) %s" % (n + 1, qtitle)\n\n print(q_title_print, end="")\n q.possible = 0\n q.obtained = 0\n # q_ = {} # Gather score in this class.\n UTextResult.q_title_print = q_title_print # Hacky\n UTextResult.show_progress_bar = show_progress_bar # Hacky.\n UTextResult.number = n\n UTextResult.nL = report.nL\n UTextResult.unmute = unmute # Hacky as well.\n UTextResult.setUpClass_time = q._cache.get(((q.__name__, \'setUpClass\'), \'time\'), 3) if hasattr(q, \'_cache\') and q._cache is not None else 3\n\n\n res = UTextTestRunner(verbosity=2, resultclass=UTextResult).run(suite)\n details = {}\n for s, msg in res.successes + res.failures + res.errors:\n # from unittest.suite import _ErrorHolder\n # from unittest import _Err\n # if isinstance(s, _ErrorHolder)\n if hasattr(s, \'_testMethodName\'):\n key = (q.__name__, s._testMethodName)\n else:\n # In case s is an _ErrorHolder (unittest.suite)\n key = (q.__name__, s.id())\n # key = (q.__name__, s._testMethodName) # cannot use the cache_id method bc. it is not compatible with plain unittest.\n\n detail = {}\n if (s,msg) in res.successes:\n detail[\'status\'] = "pass"\n elif (s,msg) in res.failures:\n detail[\'status\'] = \'fail\'\n elif (s,msg) in res.errors:\n detail[\'status\'] = \'error\'\n else:\n raise Exception("Status not known.")\n\n nice_title = s.title\n detail = {**detail, **msg, \'nice_title\': nice_title}#[\'message\'] = msg\n details[key] = detail\n\n # q_[s._testMethodName] = ("pass", None)\n # for (s,msg) in res.failures:\n # q_[s._testMethodName] = ("fail", msg)\n # for (s,msg) in res.errors:\n # q_[s._testMethodName] = ("error", msg)\n # res.successes[0]._get_outcome()\n\n possible = res.testsRun\n obtained = len(res.successes)\n\n # assert len(res.successes) + len(res.errors) + len(res.failures) == res.testsRun\n\n obtained = int(w * obtained * 1.0 / possible ) if possible > 0 else 0\n score[n] = {\'w\': w, \'possible\': w, \'obtained\': obtained, \'items\': details, \'title\': qtitle, \'name\': q.__name__,\n }\n q.obtained = obtained\n q.possible = possible\n # print(q._cache)\n # print(q._covcache)\n s1 = f" * q{n+1}) Total"\n s2 = f" {q.obtained}/{w}"\n print(s1 + ("."* (report.nL-len(s1)-len(s2) )) + s2 )\n print(" ")\n table_data.append([f"q{n+1}) Total", f"{q.obtained}/{w}"])\n\n ws, possible, obtained = upack(score)\n possible = int( msum(possible) )\n obtained = int( msum(obtained) ) # Cast to python int\n report.possible = possible\n report.obtained = obtained\n now = datetime.now()\n dt_string = now.strftime("%H:%M:%S")\n\n dt = int(time.time()-t_start)\n minutes = dt//60\n seconds = dt - minutes*60\n plrl = lambda i, s: str(i) + " " + s + ("s" if i != 1 else "")\n\n dprint(first = "Total points at "+ dt_string + " (" + plrl(minutes, "minute") + ", "+ plrl(seconds, "second") +")",\n last=""+str(report.obtained)+"/"+str(report.possible), nL = report.nL)\n\n # print(f"Completed at "+ dt_string + " (" + plrl(minutes, "minute") + ", "+ plrl(seconds, "second") +"). Total")\n\n table_data.append(["Total", ""+str(report.obtained)+"/"+str(report.possible) ])\n results = {\'total\': (obtained, possible), \'details\': score}\n return results, table_data\n\n\ndef python_code_str_id(python_code, strip_comments_and_docstring=True):\n s = python_code\n\n if strip_comments_and_docstring:\n try:\n s = remove_comments_and_docstrings(s)\n except Exception as e:\n print("--"*10)\n print(python_code)\n print(e)\n\n s = "".join([c.strip() for c in s.split()])\n hash_object = hashlib.blake2b(s.encode())\n return hash_object.hexdigest()\n\n\ndef file_id(file, strip_comments_and_docstring=True):\n with open(file, \'r\') as f:\n # s = f.read()\n return python_code_str_id(f.read())\n\n\ndef remove_comments_and_docstrings(source):\n """\n Returns \'source\' minus comments and docstrings.\n """\n io_obj = io.StringIO(source)\n out = ""\n prev_toktype = tokenize.INDENT\n last_lineno = -1\n last_col = 0\n for tok in tokenize.generate_tokens(io_obj.readline):\n token_type = tok[0]\n token_string = tok[1]\n start_line, start_col = tok[2]\n end_line, end_col = tok[3]\n ltext = tok[4]\n # The following two conditionals preserve indentation.\n # This is necessary because we\'re not using tokenize.untokenize()\n # (because it spits out code with copious amounts of oddly-placed\n # whitespace).\n if start_line > last_lineno:\n last_col = 0\n if start_col > last_col:\n out += (" " * (start_col - last_col))\n # Remove comments:\n if token_type == tokenize.COMMENT:\n pass\n # This series of conditionals removes docstrings:\n elif token_type == tokenize.STRING:\n if prev_toktype != tokenize.INDENT:\n # This is likely a docstring; double-check we\'re not inside an operator:\n if prev_toktype != tokenize.NEWLINE:\n # Note regarding NEWLINE vs NL: The tokenize module\n # differentiates between newlines that start a new statement\n # and newlines inside of operators such as parens, brackes,\n # and curly braces. Newlines inside of operators are\n # NEWLINE and newlines that start new code are NL.\n # Catch whole-module docstrings:\n if start_col > 0:\n # Unlabelled indentation means we\'re inside an operator\n out += token_string\n # Note regarding the INDENT token: The tokenize module does\n # not label indentation inside of an operator (parens,\n # brackets, and curly braces) as actual indentation.\n # For example:\n # def foo():\n # "The spaces before this docstring are tokenize.INDENT"\n # test = [\n # "The spaces before this string do not get a token"\n # ]\n else:\n out += token_string\n prev_toktype = token_type\n last_col = end_col\n last_lineno = end_line\n return out\n\nimport textwrap\nimport bz2\nimport pickle\nimport os\nimport zipfile\nimport io\n\ndef bzwrite(json_str, token): # to get around obfuscation issues\n with getattr(bz2, \'open\')(token, "wt") as f:\n f.write(json_str)\n\ndef gather_imports(imp):\n resources = {}\n m = imp\n f = m.__file__\n if hasattr(m, \'__file__\') and not hasattr(m, \'__path__\'):\n top_package = os.path.dirname(m.__file__)\n module_import = True\n else:\n im = __import__(m.__name__.split(\'.\')[0])\n if isinstance(im, list):\n print("im is a list")\n print(im)\n # the __path__ attribute *may* be a string in some cases. I had to fix this.\n print("path.:", __import__(m.__name__.split(\'.\')[0]).__path__)\n # top_package = __import__(m.__name__.split(\'.\')[0]).__path__._path[0]\n top_package = __import__(m.__name__.split(\'.\')[0]).__path__[0]\n module_import = False\n\n found_hashes = {}\n # pycode = {}\n resources[\'pycode\'] = {}\n zip_buffer = io.BytesIO()\n with zipfile.ZipFile(zip_buffer, \'w\') as zip:\n for root, dirs, files in os.walk(top_package):\n for file in files:\n if file.endswith(".py"):\n fpath = os.path.join(root, file)\n v = os.path.relpath(fpath, os.path.dirname(top_package) if not module_import else top_package)\n zip.write(fpath, v)\n if not fpath.endswith("_grade.py"): # Exclude grade files.\n with open(fpath, \'r\') as f:\n s = f.read()\n found_hashes[v] = python_code_str_id(s)\n resources[\'pycode\'][v] = s\n\n resources[\'zipfile\'] = zip_buffer.getvalue()\n resources[\'top_package\'] = top_package\n resources[\'module_import\'] = module_import\n resources[\'blake2b_file_hashes\'] = found_hashes\n return resources, top_package\n\n\nimport argparse\nparser = argparse.ArgumentParser(description=\'Evaluate your report.\', epilog="""Use this script to get the score of your report. Example:\n\n> python report1_grade.py\n\nFinally, note that if your report is part of a module (package), and the report script requires part of that package, the -m option for python may be useful.\nFor instance, if the report file is in Documents/course_package/report3_complete.py, and `course_package` is a python package, then change directory to \'Documents/` and run:\n\n> python -m course_package.report1\n\nsee https://docs.python.org/3.9/using/cmdline.html\n""", formatter_class=argparse.RawTextHelpFormatter)\nparser.add_argument(\'--noprogress\', action="store_true", help=\'Disable progress bars\')\nparser.add_argument(\'--autolab\', action="store_true", help=\'Show Autolab results\')\n\ndef gather_report_source_include(report):\n sources = {}\n # print("")\n # if not args.autolab:\n if len(report.individual_imports) > 0:\n print("By uploading the .token file, you verify the files:")\n for m in report.individual_imports:\n print(">", m.__file__)\n print("Are created/modified individually by you in agreement with DTUs exam rules")\n report.pack_imports += report.individual_imports\n\n if len(report.pack_imports) > 0:\n print("Including files in upload...")\n for k, m in enumerate(report.pack_imports):\n nimp, top_package = gather_imports(m)\n _, report_relative_location, module_import = report._import_base_relative()\n\n nimp[\'report_relative_location\'] = report_relative_location\n nimp[\'report_module_specification\'] = module_import\n nimp[\'name\'] = m.__name__\n sources[k] = nimp\n print(f" * {m.__name__}")\n return sources\n\ndef gather_upload_to_campusnet(report, output_dir=None, token_include_plaintext_source=False):\n # n = report.nL\n args = parser.parse_args()\n results, table_data = evaluate_report(report, show_help_flag=False, show_expected=False, show_computed=False, silent=True,\n show_progress_bar=not args.noprogress,\n big_header=not args.autolab,\n generate_artifacts=False,\n )\n print("")\n sources = {}\n if not args.autolab:\n results[\'sources\'] = sources = gather_report_source_include(report)\n\n token_plain = """\n# This file contains your results. Do not edit its content. Simply upload it as it is. """\n\n s_include = [token_plain]\n known_hashes = []\n cov_files = []\n use_coverage = True\n if report._config is not None:\n known_hashes = report._config[\'blake2b_file_hashes\']\n for Q, _ in report.questions:\n use_coverage = use_coverage and isinstance(Q, UTestCase)\n for key in Q._cache:\n if len(key) >= 2 and key[1] == "coverage":\n for f in Q._cache[key]:\n cov_files.append(f)\n\n for s in sources.values():\n for f_rel, hash in s[\'blake2b_file_hashes\'].items():\n if hash in known_hashes and f_rel not in cov_files and use_coverage:\n print("Skipping", f_rel)\n else:\n if token_include_plaintext_source:\n s_include.append("#"*3 +" Content of " + f_rel +" " + "#"*3)\n s_include.append("")\n s_include.append(s[\'pycode\'][f_rel])\n s_include.append("")\n\n if output_dir is None:\n output_dir = os.getcwd()\n\n payload_out_base = report.__class__.__name__ + "_handin"\n\n obtain, possible = results[\'total\']\n vstring = f"_v{report.version}" if report.version is not None else ""\n token = "%s_%i_of_%i%s.token"%(payload_out_base, obtain, possible,vstring)\n token = os.path.normpath(os.path.join(output_dir, token))\n\n save_token(results, "\\n".join(s_include), token)\n\n if not args.autolab:\n print("> Testing token file integrity...", sep="")\n load_token(token)\n print("Done!")\n print(" ")\n print("To get credit for your results, please upload the single unmodified file: ")\n print(">", token)\n\n\ndef save_token(dictionary, plain_text, file_out):\n if plain_text is None:\n plain_text = ""\n if len(plain_text) == 0:\n plain_text = "Start token file"\n plain_text = plain_text.strip()\n b, b_hash = dict2picklestring(dictionary)\n b_l1 = len(b)\n b = "."+b+"."\n b = "\\n".join( textwrap.wrap(b, 180))\n\n out = [plain_text, token_sep, f"{b_hash} {b_l1}", token_sep, b]\n with open(file_out, \'w\') as f:\n f.write("\\n".join(out))\n\n\n\n\ndef source_instantiate(name, report1_source, payload):\n # print("Executing sources", report1_source)\n eval("exec")(report1_source, globals())\n # print("Loaind gpayload..")\n pl = pickle.loads(bytes.fromhex(payload))\n report = eval(name)(payload=pl, strict=True)\n return report\n\n\n__version__ = "0.1.29.0"\n\nfrom cs108.homework1 import add, reverse_list, linear_regression_weights, linear_predict, foo\nimport time\nimport numpy as np\nimport pickle\nimport os\n# from unitgrade.framework import dash\n\ndef mk_bad():\n with open(os.path.dirname(__file__)+"/db.pkl", \'wb\') as f:\n d = {\'x1\': 100, \'x2\': 300}\n pickle.dump(d, f)\n\ndef mk_ok():\n with open(os.path.dirname(__file__)+"/db.pkl", \'wb\') as f:\n d = {\'x1\': 1, \'x2\': 2}\n pickle.dump(d, f)\n\nclass Numpy(UTestCase):\n z = 234\n\n # def __getattr__(self, item):\n # print("hi there ", item)\n # return super().__getattr__(item)\n #\n # def __getattribute__(self, item):\n # print("oh hello sexy. ", item)\n # return super().__getattribute__(item)\n\n @classmethod_dashboard\n def setUpClass(cls) -> None:\n print("Dum di dai, I am running some setup code here.")\n for i in range(10):\n print("Hello world", i)\n print("Set up.") # must be handled seperately.\n # assert False\n\n # @cache\n # def make_primes(self, n):\n # return primes(n)\n\n # def setUp(self) -> None:\n # print("We are doing the setup thing.")\n\n def test_bad(self):\n """\n Hints:\n * Remember to properly de-indent your code.\n * Do more stuff which works.\n """\n # raise Exception("This ended poorly")\n # print("Here we go")\n # return\n # self.assertEqual(1, 1)\n with open(os.path.dirname(__file__)+"/db.pkl", \'rb\') as f:\n d = pickle.load(f)\n # print(d)\n # assert False\n # for i in range(10):\n from tqdm import tqdm\n for i in tqdm(range(100)):\n # print("The current number is", i)\n time.sleep(.01)\n self.assertEqual(1, d[\'x1\'])\n for b in range(10):\n self.assertEqualC(add(3, b))\n\n\n def test_weights(self):\n """\n Hints:\n * Try harder!\n * Check the chapter on linear regression.\n """\n n = 3\n m = 2\n np.random.seed(5)\n # from numpy import asdfaskdfj\n # X = np.random.randn(n, m)\n # y = np.random.randn(n)\n foo()\n # assert 2 == 3\n # raise Exception("Bad exit")\n # self.assertEqual(2, np.random.randint(1000))\n # self.assertEqual(2, np.random.randint(1000))\n # self.assertL2(linear_regression_weights(X, y), msg="the message")\n self.assertEqual(1, 1)\n # self.assertEqual(1,2)\n return "THE RESULT OF THE TEST"\n\n\nclass AnotherTest(UTestCase):\n def test_more(self):\n self.assertEqual(2,2)\n\n def test_even_more(self):\n self.assertEqual(2,2)\n\nimport cs108\nclass Report2(Report):\n title = "CS 101 Report 2"\n questions = [\n (Numpy, 10), (AnotherTest, 20)\n ]\n pack_imports = [cs108]' +report1_payload = '8004954f040000000000007d94288c054e756d7079947d942868018c0a7365745570436c6173739486948c0474696d65948694473f3bf0000000000068018c08746573745f6261649486948c057469746c6594869468076801680786948c066173736572749486947d94284b004b034b014b044b024b054b034b064b044b074b054b084b064b094b074b0a4b084b0b4b094b0c7568016807869468058694473ff08790000000006801680786948c08636f7665726167659486947d948c1263733130382f686f6d65776f726b312e7079947d948c0e6465662061646428612c62293a20944b128ca12020202022222220476976656e2074776f206e756d626572732060616020616e642060626020746869732066756e6374696f6e2073686f756c642073696d706c792072657475726e2074686569722073756d3a0a202020203e2061646428612c6229203d20612b620a2020202048696e74733a0a20202020202020202a2052656d656d6265722062617369632061726974686d6574696373210a20202020222222948694737368018c0c746573745f7765696768747394869468098694681a6801681a8694680c86947d946801681a869468058694473f407400000000006801681a8694681286947d948c1263733130382f686f6d65776f726b312e7079947d94288c0b64656620666f6f28293a20944b168c162020202022222220436f6d6d656e742e2020202222229486948c0b6465662062617228293a20944b198c009486947573758c0b416e6f7468657254657374947d9428682d6803869468058694473f22700000000000682d8c09746573745f6d6f7265948694680c86947d94682d6831869468058694473f21200000000000682d8c0e746573745f6576656e5f6d6f7265948694680c86947d94682d6837869468058694473f1a700000000000758c06636f6e666967947d948c13626c616b6532625f66696c655f686173686573945d94288c806362363363336235383635306636313037643763663138646136303635666135373835666261626564643135316639653761633335313139323635623039393838623266653335373632303961333932616133656236633134636131316439646335393937343831633531373863313533393665656662313539653163373536948c803434656331613338643134373639626433653234323663386232366539303830356336313361386161653266333966663665633433363133666562363465303739373435323062306536353134353063303637623763633637636631366134313835653736346334383331373763333335303063626563626362336234646466948c803638306336353638323633623832303737313365616434306539323663643265363835336130613936353861386338343738393564363633643730643262343666616163333336396133636564366239623964303436346563316366656465326235306265376432626636313432313638383936663332306338353232313066946573752e' name="Report2" report = source_instantiate(name, report1_source, report1_payload) diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/AnotherTest-test_even_more.json b/devel/example_devel/instructor/cs108/unitgrade_data/AnotherTest-test_even_more.json deleted file mode 100644 index e01b626..0000000 --- a/devel/example_devel/instructor/cs108/unitgrade_data/AnotherTest-test_even_more.json +++ /dev/null @@ -1 +0,0 @@ -{"state": "pass", "run_id": 863304, "coverage_files_changed": null, "stdout": [[0, "Dashboard> Evaluation completed."]]} \ No newline at end of file diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/AnotherTest-test_even_more.json.lock b/devel/example_devel/instructor/cs108/unitgrade_data/AnotherTest-test_even_more.json.lock deleted file mode 100755 index e69de29..0000000 diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/AnotherTest-test_more.json b/devel/example_devel/instructor/cs108/unitgrade_data/AnotherTest-test_more.json deleted file mode 100644 index 710d65e..0000000 --- a/devel/example_devel/instructor/cs108/unitgrade_data/AnotherTest-test_more.json +++ /dev/null @@ -1 +0,0 @@ -{"state": "pass", "run_id": 282722, "coverage_files_changed": null, "stdout": [[0, "Dashboard> Evaluation completed."]]} \ No newline at end of file diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/AnotherTest-test_more.json.lock b/devel/example_devel/instructor/cs108/unitgrade_data/AnotherTest-test_more.json.lock deleted file mode 100755 index e69de29..0000000 diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/AnotherTest.pkl b/devel/example_devel/instructor/cs108/unitgrade_data/AnotherTest.pkl index 1a07e6f6b3f4993e4e5fe85ae58a067520f4eade..43a2319d3d9445b0cf1259dfc350ec87a82aecba 100644 GIT binary patch delta 29 icmbQsIG1sP8Kcrfb3JBzMTLnO+FbTh1q>iiss{jUUIsY; delta 29 hcmbQsIG1sP8KcNVb3JCe>kJb!w7KlAGJrv;9sqZu2MPcH diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-setUpClass.json b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-setUpClass.json deleted file mode 100644 index a46f0a4..0000000 --- a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-setUpClass.json +++ /dev/null @@ -1 +0,0 @@ -{"run_id": 188727, "coverage_files_changed": null, "stdout": [[0, "Dum di dai, I am running some setup code here.\nHello world 0\nHello world 1\nHello world 2\nHello world 3\nHello world 4\nHello world 5\nHello world 6\nHello world 7\nHello world 8\nHello world 9\nSet up.\nDashboard> Evaluation completed."]], "state": "pass"} \ No newline at end of file diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-setUpClass.json.lock b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-setUpClass.json.lock deleted file mode 100755 index e69de29..0000000 diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_bad.json b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_bad.json deleted file mode 100644 index 4ecb597..0000000 --- a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_bad.json +++ /dev/null @@ -1 +0,0 @@ -{"state": "fail", "run_id": 1789, "coverage_files_changed": null, "stdout": [[0, "\u001b[31m\r 0%| | 0/100 [00:00<?, ?it/s]\u001b[37m"], [1, "\u001b[31m\r 10%|# | 10/100 [00:00<00:00, 97.25it/s]\u001b[37m\u001b[31m\r 20%|## | 20/100 [00:00<00:00, 97.27it/s]\u001b[37m"], [2, "\u001b[31m\r 30%|### | 30/100 [00:00<00:00, 95.97it/s]\u001b[37m"], [3, "\u001b[31m\r 40%|#### | 40/100 [00:00<00:00, 95.72it/s]\u001b[37m\u001b[31m\r 50%|##### | 50/100 [00:00<00:00, 93.34it/s]\u001b[37m"], [4, "\u001b[31m\r 60%|###### | 60/100 [00:00<00:00, 91.76it/s]\u001b[37m\u001b[31m\r 70%|####### | 70/100 [00:00<00:00, 93.45it/s]\u001b[37m"], [5, "\u001b[31m\r 80%|######## | 80/100 [00:00<00:00, 94.95it/s]\u001b[37m\u001b[31m\r 90%|######### | 90/100 [00:00<00:00, 95.62it/s]\u001b[37m"], [6, "\u001b[31m\r100%|##########| 100/100 [00:01<00:00, 95.82it/s]\u001b[37m\u001b[31m\u001b[37m\u001b[31m\r100%|##########| 100/100 [00:01<00:00, 94.89it/s]\u001b[37m\u001b[31m\n\u001b[37m\u001b[92m>\n\u001b[92m> Hints (from 'test_bad')\n\u001b[92m> * Remember to properly de-indent your code.\n> * Do more stuff which works.\n\u001b[31mTraceback (most recent call last):\n File \"/usr/lib/python3.10/unittest/case.py\", line 59, in testPartExecutor\n yield\n File \"/usr/lib/python3.10/unittest/case.py\", line 591, in run\n self._callTestMethod(testMethod)\n File \"/home/tuhe/Documents/unitgrade/src/unitgrade/framework.py\", line 534, in _callTestMethod\n res = testMethod()\n File \"/home/tuhe/Documents/unitgrade_private/devel/example_devel/instructor/cs108/report_devel.py\", line 67, in test_bad\n self.assertEqual(1, d['x1'])\nAssertionError: 1 != 100\n\u001b[37mDashboard> Evaluation completed."]], "wz_stacktrace": "<div class=\"traceback\">\n <h3>Traceback <em>(most recent call last)</em>:</h3>\n <ul><li><div class=\"frame\" id=\"frame-140582372419264\">\n <h4>File <cite class=\"filename\">\"/usr/lib/python3.10/unittest/case.py\"</cite>,\n line <em class=\"line\">59</em>,\n in <code class=\"function\">testPartExecutor</code></h4>\n <div class=\"source library\"><pre class=\"line before\"><span class=\"ws\"> </span>@contextlib.contextmanager</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>def testPartExecutor(self, test_case, isTest=False):</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>old_success = self.success</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>self.success = True</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>try:</pre>\n<pre class=\"line current\"><span class=\"ws\"> </span>yield</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>except KeyboardInterrupt:</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>raise</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>except SkipTest as e:</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>self.success = False</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>self.skipped.append((test_case, str(e)))</pre></div>\n</div>\n\n<li><div class=\"frame\" id=\"frame-140582372597696\">\n <h4>File <cite class=\"filename\">\"/usr/lib/python3.10/unittest/case.py\"</cite>,\n line <em class=\"line\">591</em>,\n in <code class=\"function\">run</code></h4>\n <div class=\"source library\"><pre class=\"line before\"><span class=\"ws\"> </span>with outcome.testPartExecutor(self):</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>self._callSetUp()</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>if outcome.success:</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>outcome.expecting_failure = expecting_failure</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>with outcome.testPartExecutor(self, isTest=True):</pre>\n<pre class=\"line current\"><span class=\"ws\"> </span>self._callTestMethod(testMethod)</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>outcome.expecting_failure = False</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>with outcome.testPartExecutor(self):</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>self._callTearDown()</pre>\n<pre class=\"line after\"><span class=\"ws\"></span> </pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>self.doCleanups()</pre></div>\n</div>\n\n<li><div class=\"frame\" id=\"frame-140582372597808\">\n <h4>File <cite class=\"filename\">\"/home/tuhe/Documents/unitgrade/src/unitgrade/framework.py\"</cite>,\n line <em class=\"line\">534</em>,\n in <code class=\"function\">_callTestMethod</code></h4>\n <div class=\"source \"><pre class=\"line before\"><span class=\"ws\"> </span>self._ensure_cache_exists() # Make sure cache is there.</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>if self._testMethodDoc is not None:</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>self._cache_put((self.cache_id(), 'title'), self.shortDescriptionStandard())</pre>\n<pre class=\"line before\"><span class=\"ws\"></span> </pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>self._cache2[(self.cache_id(), 'assert')] = {}</pre>\n<pre class=\"line current\"><span class=\"ws\"> </span>res = testMethod()</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>elapsed = time.time() - t</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>self._get_outcome()[ (self.cache_id(), "return") ] = res</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>self._cache_put((self.cache_id(), "time"), elapsed)</pre>\n<pre class=\"line after\"><span class=\"ws\"></span> </pre>\n<pre class=\"line after\"><span class=\"ws\"></span> </pre></div>\n</div>\n\n<li><div class=\"frame\" id=\"frame-140582372597920\">\n <h4>File <cite class=\"filename\">\"/home/tuhe/Documents/unitgrade_private/devel/example_devel/instructor/cs108/report_devel.py\"</cite>,\n line <em class=\"line\">67</em>,\n in <code class=\"function\">test_bad</code></h4>\n <div class=\"source \"><pre class=\"line before\"><span class=\"ws\"> </span># for i in range(10):</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>from tqdm import tqdm</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>for i in tqdm(range(100)):</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span># print("The current number is", i)</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>time.sleep(.01)</pre>\n<pre class=\"line current\"><span class=\"ws\"> </span>self.assertEqual(1, d['x1'])</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>for b in range(10):</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>self.assertEqualC(add(3, b))</pre>\n<pre class=\"line after\"><span class=\"ws\"></span> </pre>\n<pre class=\"line after\"><span class=\"ws\"></span> </pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>def test_weights(self):</pre></div>\n</div>\n</ul>\n <blockquote>AssertionError: 1 != 100\n</blockquote>\n</div>\n"} \ No newline at end of file diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_bad.json.lock b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_bad.json.lock deleted file mode 100755 index e69de29..0000000 diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_weights.json b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_weights.json deleted file mode 100644 index 6b4397c..0000000 --- a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_weights.json +++ /dev/null @@ -1 +0,0 @@ -{"state": "pass", "run_id": 766225, "coverage_files_changed": null, "stdout": [[0, "Dashboard> Evaluation completed."]]} \ No newline at end of file diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_weights.json.lock b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_weights.json.lock deleted file mode 100755 index e69de29..0000000 diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy.pkl b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy.pkl index a8b4258ab07e08e10c14cb8ff117771a4e3d33e0..5ab625a286557ef448281e4a58d090bbd744fc8c 100644 GIT binary patch literal 553 zcmaJ;Jxc>Y5KUqdV<LhngfudyiXTMNNyHDZP&lv<tj*rt#$C+bhTYi&3&Bd8EiBF- z;6L;yIC~c?T{!OD+c)!O-o4eoc59X5yygSnxN~a$lKZyh_%OeoF>CYp+}BYmPT=P9 z?5EI*s&65;_!zS^2pTAH78If>s-j6ulS;WkqNs0FiPoZORF7&=gHV&uI-wS!4MJ@~ z9YUL~EEe@(_4Dx=yjJ~MiC%ESo`T71?z@Q{otz#Et@u)#=h2X+CVDI7Q($R2U`O%( z81ks=zkyD_--jEy;2GdjL*`V>jfDxD0LxOC#1lXvElgdOpwt;yq1`M6E7g4V0*0eA zR4TYMV4WJbL(hvpBRB_k7#HNN%+QwiL3052T=C!rF|#rOW+V#5Q6_e;D4@w`BWo$q z%_7kymrnvi?4m_akM{4;yA#%4;Ub<ZT>XjSdi$SL2EG$wnQA>KGTHGvVYZOTwN{E} VI4nQOvy{fnl+vEB5b3NN{sO-sv55cx delta 84 zcmZ3<a)*(%fo1ANhKa1&OcpOD8f7`;#4!Lt24fFTNosLPe0geSdPYg{l(s1u96&6P d!I&Wcq`BLs)J}o2S=y$!+x>C?X=5nW0|2g^8`}T? diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/cache.db b/devel/example_devel/instructor/cs108/unitgrade_data/cache.db index eba28aab5e607cfee36521a00079738cc07361f5..a50dc2f94a42cb35bee1de7c32d1bd2eb5b48c86 100644 GIT binary patch literal 45056 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 delta 640 zcmaiy!Hd&C7{!x(Z8PaM^QCCF>5A+Ygo?`=7WJxK#G~NRTMW}^$4xS|Gg(|cY%ktj zauD<n@T&IaL9lnhli*3wy;$&|;8jez)+!>-VVL)L%s0QsjNTigk#YZ688l5J`)e0; zczW#xjvL(%c+$>2hfXy7f*<iYKE?+)!aiQcvv?eVexmQ_8~Tb)##_!qdeL&e>x zC{2V0>2oDbnja*;6A~Mmgo!j!ZVk@qJubKX0cYD$=nsZM2_~YlyOf>wT5c6KEqN>A zD)i-U!umszh*UPMEbv^NWOu!lmI<EmGr|Ja&x84hHLX#}o_NbA%FxzX8byA@W3H<8 z60}Q2;81sSs19Urz0ReY0jop>H+-J-!rQhEZ6Z~mxKy0U>I{CWp^4A%6xQ)I{YIzs zJHEt6cn@Q|id*;<4|4bk&8Fu&d+tBLYRj;BNF4LTzs_^!;yFu&{Woa!C~VB_I1UE0 ztkr}wbMvN1he;6mvFL@Q0p8r-TU25Gc6E>{;qL@2P2&pvqnUCc7sZs0z`7ppaHhCO u{DFu#+XWj|^<NDcHngopL&f*doMM};@k;jq_pjKMqB+eO*SnwpFMj~ZpSv*t diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/cache.db-shm b/devel/example_devel/instructor/cs108/unitgrade_data/cache.db-shm deleted file mode 100644 index 43bd309ec02e932fe394e3429744ffee7e9391ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI)XK)o&7{>AclL926B-B9YEws===)IR<2gTm7VOPKgs1yMeJJ@>%I~ME}P_b7m zs93Q<><#ejV8%O6_;49=ak$T!Irr|K-92Z|?|pY??uUDFR~HbK$)O6eM~9EBuswe9 zim^*d#>^{ORFGdhXJO8`yrMM|S1m2fjr07d@2EumZj33LTL1k_4V&qqRtU9XsFgx3 zkG8ab`Yo^T|IQ)WFP+cU+Kybx>;6mo^*lD^V7xKgUSbpPyyHDTybeeSHTJrUupe)G zd3(p+Us>;?*yFL=Wz8cq%rAPkmc9?RWIy)W(3Oh?EMNf(Sik}nuz&?DU;ztQzycPq zfCVgI0Sj2b0v51<1uS3z3s}Gc7O;Q?EMNf(Sik}nuz&?DU;ztQzycPqfCVgI0Sj2b z0v51<1uS5J?L=Tbsmf4M^k;1s3nZDq2GW$NuFn1>i@T>A2`H>JltnhRs7HMo(wL?+ zrzNduLp$2jk<Jve13R$`yD^qM*pt24m;E`AgPF?doXHGkF`K!Z!#vLCLM~=L_i;ZD zvV=!?j3;=CXLycfJkN_%Q;uq=mg=ak^3*_$)I`nHLT&s6Iwcb<kVgX=(S&BSAfMK> zrGO4}Vl+FlGrKZ|-5JMT?8AN>z(Gvo45l-avzWu#oXdG!z(ribVjkcj9_CRV=SiOC zS(dV#7syt1)l_Zes-Ehrp&F~HnyaNVJ;8DIaAgUkgf*BdlS6F^=|)fb(4RpJWd!3n zoFh4g<C(}woXP^O;A*bpMsDGD?%-}#@G5WcHY@pnk66v;e8o3>PYFNqD}S(&bY&@D zZPh`Y)y)}SRsh_#4Fyue>WWpUP93^%2$%8_?VP3CFdiN{kpgLCP?Z|wQbc!p(U$=X zW*8$mlp{EbV>y9IoXlxl#+6*d_1wg*EaXn^;bmUqP2S->KICIQ<qOvEEo=FKpZSeH z!;gvzs;pKjP)8N2yLzj?25Y!R`T0s@h&|S31=7i+8a1g)S9;K!ehg#?!x_b4OyFpa z<HWEQ@F`5;a<1Z9Zs2BaV-a_8FR$=AZ}Begvx-mnj4%0`?^wr={KD`2g;Y^h)LQM; zNnO-KeKbHrG(yGB{>{e22NEfeqDrdjY)mA%$NJw0^i*FB)KHC7v>LmM1(Hdim-=at zhB@<-Dd?W71uS3z3s}Gc7O;Q?EMNf(Sik}nuz&?DU;ztQzycPqfCVgIfus@m8?1G^ AJOBUy diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/cache.db-wal b/devel/example_devel/instructor/cs108/unitgrade_data/cache.db-wal deleted file mode 100644 index 8d0e796d83831dcce78b83a22f959c52a9520a52..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1371992 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 E4@p|NKL7v# diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.artifacts.pkl b/devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.artifacts.pkl index 0889adfe5ecf9f47930b690d68842f07548476b6..93e73b00832ec7e5b3272af33d44b4c9612033a1 100644 GIT binary patch delta 476 zcmaJ-O-n*S6fI3C1tAxPkaTlVuMaGtMMx>uBEd~m`<&++nL+1`%tzuvY?X^<_a<uF zPv{Tz6Z#F!^Fa_q0|Rr<IrpA>=6Urqn|WM$xy^iJ&Qi->)Jk2?OnDz}FvHBp0(>NC z50IdWFsx`#Zd+66$=dPh5#bDvxfs-{qlq>70QUg}frk$Ly*k*r#bk48$*#Lzr(oW} zol2zwSM&jgpvD|R9a~7~qAs8?fUe&KMP^h&Uxz(Kc?gnlJq$onHX2SqV5NoYC7243 zn$@%M(8iA~oC7-W7hs!)N+x`)`8Uy`!L4SWtdu%>O})*kbBp5j|I9kIp2@{=`<#1m z)=tqBW0yGIC1(tWs+yE;WFqyENTemR<UlR8)+W~qxeFuiuvFGfCF`U~3huE%nr2No k^qz5nojxHiqCvW`)in79h21~?=G&xXP9mP(iN4b1Z&szB&Hw-a delta 105 zcmcb|)y=`$z%rGAWh1K%(_|;60M^6|nb;|l`<Z9jsJBl6b4p7~d$=9*@=G#Oi$YS1 yOQzIL(Z~?YFvt)BF${V*OMrs$x%ow@Q`(?%#*_E4*fAPR{>viCXgOJ&)g1t4P$W43 diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.artifacts.pkl.lock b/devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.artifacts.pkl.lock deleted file mode 100755 index e69de29..0000000 diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.json b/devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.json deleted file mode 100644 index 098b70e..0000000 --- a/devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.json +++ /dev/null @@ -1 +0,0 @@ -{"encoding_scheme": " from unitgrade_private.hidden_gather_upload import dict2picklestring, picklestring2dict;", "questions": "/Td6WFoAAATm1rRGAgAhARYAAAB0L+Wj4ALJAW9dAEABDnncV35phHsyxOZ/WAdcsRcnyJl1OO/vI8mjmhFI6lWS7SfFyihoIfXWjVmlOSPIYt5RtCJvS/3j4bxa5pi+3PPpcnS2VzmHCG1Ro9va9QyFawpcqgSSGVWVFndTK1xzGnFnOEsQAgiJ0VB9ATsnpaY1K5Z1aravch16BLCzLWocn3K1egojbjfRrL6HkB7XP21nDmqPeoHjVZNY6QM2BV9RrnccWViu+u9PVaH/q5YyjX36FQhwsGiMGmIM/LaZzWCyCJt7bbYjq1UXgqmMRrvYwHAXCeoFH1McQxAWLW4P2GU1rZqLMKc/OoQjEvMZdHxWkQBqE2wS4++OffV1YnQ7I0xOljxcxIxhVlPVxUFyj/D04h0CF/ekMP1FxoZsff7QPOLT6apxSDa9jtn1P+u4E9eo30YIdFqZt6hmAmAOyVtNVJxeV+gOFhQOLe8suClVF+MoeXta30NnnqCdXgsjq0/69kvgmi1jackAAPVvsIGknWNrAAGLA8oFAACfhrnvscRn+wIAAAAABFla", "root_dir": "/home/tuhe/Documents/unitgrade_private/devel/example_devel/instructor", "relative_path": "cs108/report_devel.py", "modules": ["cs108", "report_devel"], "token_stub": "cs108/Report2_handin"} \ No newline at end of file diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.json.lock b/devel/example_devel/instructor/cs108/unitgrade_data/main_config_report_devel.json.lock deleted file mode 100755 index e69de29..0000000 diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/report_devel.json.lock b/devel/example_devel/instructor/cs108/unitgrade_data/report_devel.json.lock deleted file mode 100755 index e69de29..0000000 diff --git a/docs/README.jinja.md b/docs/README.jinja.md index 6fdc56d..b2e2184 100644 --- a/docs/README.jinja.md +++ b/docs/README.jinja.md @@ -15,6 +15,7 @@ Unitgrade is an automatic report and exam evaluation framework that enables inst - Instructors can automatically verify the students solution using a Docker VM and run hidden tests - Automatic Moss anti-plagiarism detection - CMU Autolab integration (Experimental) + - A live dashboard which shows the outcome of the tests ### Install Simply use `pip` @@ -30,6 +31,7 @@ The figure shows an overview of the workflow. - You write exercises and a suite of unittests. - They are then compiled to a version of the exercises without solutions. - The students solve the exercises using the tests and when they are happy, they run an automatically generated `_grade.py`-script to produce a `.token`-file with the number of points they obtain. This file is then uploaded for further verification/evaluation. + - The students can see their progress and review hints using the dashboard (see below) ### Videos Videos where I try to talk and code my way through the examples can be found on youtube: @@ -114,6 +116,32 @@ This runs an identical set of tests and produces the file `Report1_handin_10_of_ - You can easily use the framework to include output of functions. - See below for how to validate the students results + +### Viewing the results using the dashboard +I recommend to monitor and run the tests from the IDE, as this allows you to use the debugger in conjunction with your tests. +However, unitgrade comes with a dashboard that allows students to see the outcome of individual tests + and what is currently recorded in the `token`-file. To start the dashboard, they should simply run the command +``` +unitgrade +``` +from a directory that contains a test (the directory will be searched recursively for test files). + The command will start a small background service and open a webpage: + + + +Features supported in the current version: + - Shows which files need to be edited to solve the problem + - Collect hints given in the homework files and display them for the relevant tests + - fully responsive -- the UI, including the terminal, will update while the test is running regardless of where you launch the test + - Allows students to re-run tests from the UI + - Shows current test status and results captured in `.token`-file + - Tested on Windows/Linux + - Frontend is pure javascript and the backend only depends on python packages. + +The frontend is automatically enabled the moment your classes inherits from the `UTestCase`-class; no configuration files required, and there are no known bugs. + +Note the frontend is currently not provided in the pypi `unitgrade` package, but only through the gitlab repository (install using `git clone` and then `pip install -e ./`) -- it seems ready, but I want to test it on mac and a few more systems before publishing it. + ## How safe is Unitgrade? There are three principal ways of cheating: - Break the framework and submit a `.token` file that 'lies' about the true number of points diff --git a/docs/snips/0_homework1.py b/docs/snips/0_homework1.py index 6722399..39004d7 100644 --- a/docs/snips/0_homework1.py +++ b/docs/snips/0_homework1.py @@ -1,4 +1,4 @@ -# example_moss/tmp/submissions/s1003/0_homework1.py +# example_moss/tmp/submissions/s1002/0_homework1.py def reverse_list(mylist): #!f """ Given a list 'mylist' returns a list consisting of the same elements in reverse order. E.g. @@ -13,8 +13,8 @@ def reverse_list(mylist): #!f def add(a,b): #!f """ Given two numbers `a` and `b` this function should simply return their sum: > add(a,b) = a+b """ - sum2 = a + b - return sum2 + sum = a + b + return sum if __name__ == "__main__": # Example usage: diff --git a/docs/snips/deploy.txt b/docs/snips/deploy.txt index bad9a43..9c9f5f6 100644 --- a/docs/snips/deploy.txt +++ b/docs/snips/deploy.txt @@ -3,27 +3,71 @@ | | | |_ __ _| |_| | \/_ __ __ _ __| | ___ | | | | '_ \| | __| | __| '__/ _` |/ _` |/ _ \ | |_| | | | | | |_| |_\ \ | | (_| | (_| | __/ - \___/|_| |_|_|\__|\____/_| \__,_|\__,_|\___| v0.1.22, started: 15/06/2022 09:18:15 + \___/|_| |_|_|\__|\____/_| \__,_|\__,_|\___| v0.1.27, started: 16/09/2022 14:30:15 CS 102 Report 2 Question 1: Week1 - * q1.1) test_add...................................................................................................PASS - * q1.2) test_reverse...............................................................................................PASS + * q1.1) test_add.................................................................................................FAILED + * q1.2) test_reverse.............................................................................................FAILED * q1.3) test_output_capture........................................................................................PASS - * q1) Total.................................................................................................... 10/10 +====================================================================== +FAIL: test_add (__main__.Week1) +test_add +---------------------------------------------------------------------- +Traceback (most recent call last): + File "<string>", line 882, in _callTestMethod + File "<string>", line 1699, in test_add + File "<string>", line 987, in assertEqualC + File "<string>", line 975, in wrap_assert +AssertionError: 4 != 'Key 0 not found in cache; framework files missing. Please run deploy()' + +====================================================================== +FAIL: test_reverse (__main__.Week1) +test_reverse +---------------------------------------------------------------------- +Traceback (most recent call last): + File "<string>", line 882, in _callTestMethod + File "<string>", line 1703, in test_reverse + File "<string>", line 987, in assertEqualC + File "<string>", line 975, in wrap_assert +AssertionError: [3, 2, 1] != 'Key 0 not found in cache; framework files missing. Please run deploy()' + + * q1) Total..................................................................................................... 3/10 Question 2: The same problem as before with nicer titles - * q2.1) Test the addition method add(a,b)..........................................................................PASS - * q2.2) Checking if reverse_list([1, 2, 3]) = [3, 2, 1]............................................................PASS - * q2) Total...................................................................................................... 6/6 + * q2.1) Test the addition method add(a,b)........................................................................FAILED + * q2.2) test_reverse.............................................................................................FAILED +====================================================================== +FAIL: test_add (__main__.Week1Titles) +Test the addition method add(a,b) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "<string>", line 882, in _callTestMethod + File "<string>", line 1715, in test_add + File "<string>", line 987, in assertEqualC + File "<string>", line 975, in wrap_assert +AssertionError: 4 != 'Key 0 not found in cache; framework files missing. Please run deploy()' + +====================================================================== +FAIL: test_reverse (__main__.Week1Titles) +test_reverse +---------------------------------------------------------------------- +Traceback (most recent call last): + File "<string>", line 882, in _callTestMethod + File "<string>", line 1723, in test_reverse + File "<string>", line 987, in assertEqualC + File "<string>", line 975, in wrap_assert +AssertionError: [3, 2, 1] != 'Key 0 not found in cache; framework files missing. Please run deploy()' + + * q2) Total...................................................................................................... 0/6 -Total points at 09:18:16 (0 minutes, 0 seconds)....................................................................16/16 +Total points at 14:30:15 (0 minutes, 0 seconds).....................................................................3/16 Including files in upload... -path.: _NamespacePath(['C:\\Users\\tuhe\\Documents\\unitgrade_private\\examples\\example_framework\\instructor\\cs102', 'C:\\Users\\tuhe\\Documents\\unitgrade_private\\examples\\example_framework\\instructor\\cs102']) +path.: _NamespacePath(['/home/tuhe/Documents/unitgrade_private/examples/example_framework/instructor/cs102', '/home/tuhe/Documents/unitgrade_private/examples/example_framework/instructor/cs102']) * cs102 > Testing token file integrity... Done! To get credit for your results, please upload the single unmodified file: -> C:\Users\tuhe\Documents\unitgrade_private\examples\example_framework\instructor\cs102\Report2_handin_16_of_16.token +> /home/tuhe/Documents/unitgrade_private/examples/example_framework/instructor/cs102/Report2_handin_3_of_16.token diff --git a/docs/snips/deploy_autolab_a.py b/docs/snips/deploy_autolab_a.py index 3abb333..7c490a7 100644 --- a/docs/snips/deploy_autolab_a.py +++ b/docs/snips/deploy_autolab_a.py @@ -1,6 +1,6 @@ -# autolab_token_upload/deploy_autolab.py +# autolab_example_py_upload/instructor/cs102_autolab/deploy_autolab.py # Step 1: Download and compile docker grading image. You only need to do this once. - download_docker_images("./docker") # Download docker images from gitlab (only do this once. - dockerfile = f"./docker/docker_tango_python/Dockerfile" - autograde_image = 'tango_python_tue' - compile_docker_image(Dockerfile=dockerfile, tag=autograde_image) # Compile docker image. \ No newline at end of file + download_docker_images("../docker") # Download docker images from gitlab (only do this once). + dockerfile = f"../docker/docker_tango_python/Dockerfile" + autograde_image = 'tango_python_tue2' # Tag given to the image in case you have multiple images. + compile_docker_image(Dockerfile=dockerfile, tag=autograde_image, no_cache=False) # Compile docker image. \ No newline at end of file diff --git a/docs/snips/deploy_autolab_b.py b/docs/snips/deploy_autolab_b.py index db9a499..51c186d 100644 --- a/docs/snips/deploy_autolab_b.py +++ b/docs/snips/deploy_autolab_b.py @@ -1,10 +1,14 @@ -# autolab_token_upload/deploy_autolab.py +# autolab_example_py_upload/instructor/cs102_autolab/deploy_autolab.py # Step 2: Create the cs102.tar file from the grade scripts. - instructor_base = f"../example_framework/instructor" - student_base = f"../example_framework/students" - output_tar = deploy_assignment("cs102", # Autolab name of assignment (and name of .tar file) + instructor_base = f"." + student_base = f"../../students/cs102_autolab" + + from report2_test import Report2 + # INSTRUCTOR_GRADE_FILE = + output_tar = new_deploy_assignment("cs105h", # Autolab name of assignment (and name of .tar file) INSTRUCTOR_BASE=instructor_base, - INSTRUCTOR_GRADE_FILE=f"{instructor_base}/cs102/report2_grade.py", + INSTRUCTOR_GRADE_FILE=f"{instructor_base}/report2_test_grade.py", STUDENT_BASE=student_base, - STUDENT_GRADE_FILE=f"{student_base}/cs102/report2_grade.py", - autograde_image_tag=autograde_image) \ No newline at end of file + STUDENT_GRADE_FILE=f"{instructor_base}/report2_test.py", + autograde_image_tag=autograde_image, + homework_file="homework1.py") \ No newline at end of file diff --git a/docs/snips/homework1.py b/docs/snips/homework1.py index 00d6c1f..54fe19e 100644 --- a/docs/snips/homework1.py +++ b/docs/snips/homework1.py @@ -1,4 +1,4 @@ -# example_simplest/instructor/cs101/homework1.py +# autolab_example_py_upload/instructor/cs102_autolab/homework1.py def reverse_list(mylist): #!f """ Given a list 'mylist' returns a list consisting of the same elements in reverse order. E.g. @@ -9,9 +9,8 @@ def reverse_list(mylist): #!f def add(a,b): #!f """ Given two numbers `a` and `b` this function should simply return their sum: > add(a,b) = a+b """ - return a+b + return a+b*2 -if __name__ == "__main__": - # Example usage: +if __name__ == "__main__": # Example usage: print(f"Your result of 2 + 2 = {add(2,2)}") print(f"Reversing a small list", reverse_list([2,3,5,7])) \ No newline at end of file diff --git a/docs/snips/report1_all.py b/docs/snips/report1_all.py index 67c1bf3..2c9e51b 100644 --- a/docs/snips/report1_all.py +++ b/docs/snips/report1_all.py @@ -18,4 +18,9 @@ class Report1(Report): pack_imports = [cs101] # Include all .py files in this folder if __name__ == "__main__": - evaluate_report_student(Report1()) \ No newline at end of file + # from HtmlTestRunner import HTMLTestRunner + import HtmlTestRunner + unittest.main(testRunner=HtmlTestRunner.HTMLTestRunner(output='example_dir')) + + + # evaluate_report_student(Report1()) \ No newline at end of file diff --git a/docs/snips/report2.py b/docs/snips/report2.py index e7aa0ed..ab18c20 100644 --- a/docs/snips/report2.py +++ b/docs/snips/report2.py @@ -1,10 +1,16 @@ # example_framework/instructor/cs102/report2.py from unitgrade import UTestCase, cache + + class Week1(UTestCase): + @classmethod + def setUpClass(cls) -> None: + a = 234 + def test_add(self): self.assertEqualC(add(2,2)) self.assertEqualC(add(-100, 5)) - def test_reverse(self): - self.assertEqualC(reverse_list([1, 2, 3])) \ No newline at end of file + # def test_reverse(self): + # self.assertEqualC(reverse_list([1, 2, 3])) \ No newline at end of file diff --git a/docs/snips/report2_b.py b/docs/snips/report2_b.py index 5de6d0b..cd07d0f 100644 --- a/docs/snips/report2_b.py +++ b/docs/snips/report2_b.py @@ -1,16 +1,16 @@ # example_framework/instructor/cs102/report2.py -class Week1Titles(UTestCase): - """ The same problem as before with nicer titles """ - def test_add(self): - """ Test the addition method add(a,b) """ - self.assertEqualC(add(2,2)) - print("output generated by test") - self.assertEqualC(add(-100, 5)) - # self.assertEqual(2,3, msg="This test automatically fails.") - - def test_reverse(self): - ls = [1, 2, 3] - reverse = reverse_list(ls) - self.assertEqualC(reverse) - # Although the title is set after the test potentially fails, it will *always* show correctly for the student. - self.title = f"Checking if reverse_list({ls}) = {reverse}" # Programmatically set the title \ No newline at end of file +# class Week1Titles(UTestCase): +# """ The same problem as before with nicer titles """ +# def test_add(self): +# """ Test the addition method add(a,b) """ +# self.assertEqualC(add(2,2)) +# print("output generated by test") +# self.assertEqualC(add(-100, 5)) +# # self.assertEqual(2,3, msg="This test automatically fails.") +# +# def test_reverse(self): +# ls = [1, 2, 3] +# reverse = reverse_list(ls) +# self.assertEqualC(reverse) +# # Although the title is set after the test potentially fails, it will *always* show correctly for the student. +# self.title = f"Checking if reverse_list({ls}) = {reverse}" # Programmatically set the title \ No newline at end of file diff --git a/docs/snips/report2_c.py b/docs/snips/report2_c.py index aa444a6..65c06b3 100644 --- a/docs/snips/report2_c.py +++ b/docs/snips/report2_c.py @@ -1,16 +1,16 @@ # example_framework/instructor/cs102/report2.py -class Question2(UTestCase): - @cache - def my_reversal(self, ls): - # The '@cache' decorator ensures the function is not run on the *students* computer - # Instead the code is run on the teachers computer and the result is passed on with the - # other pre-computed results -- i.e. this function will run regardless of how the student happens to have - # implemented reverse_list. - return reverse_list(ls) - - def test_reverse_tricky(self): - ls = (2,4,8) - ls2 = self.my_reversal(tuple(ls)) # This will always produce the right result, [8, 4, 2] - print("The correct answer is supposed to be", ls2) # Show students the correct answer - self.assertEqualC(reverse_list(ls)) # This will actually test the students code. - return "Buy world!" # This value will be stored in the .token file \ No newline at end of file +# class Question2(UTestCase): +# @cache +# def my_reversal(self, ls): +# # The '@cache' decorator ensures the function is not run on the *students* computer +# # Instead the code is run on the teachers computer and the result is passed on with the +# # other pre-computed results -- i.e. this function will run regardless of how the student happens to have +# # implemented reverse_list. +# return reverse_list(ls) +# +# def test_reverse_tricky(self): +# ls = (2,4,8) +# ls2 = self.my_reversal(tuple(ls)) # This will always produce the right result, [8, 4, 2] +# print("The correct answer is supposed to be", ls2) # Show students the correct answer +# self.assertEqualC(reverse_list(ls)) # This will actually test the students code. +# return "Buy world!" # This value will be stored in the .token file \ No newline at end of file diff --git a/docs/unitgrade_devel.bib b/docs/unitgrade_devel.bib index 2de0c48..b0682ad 100644 --- a/docs/unitgrade_devel.bib +++ b/docs/unitgrade_devel.bib @@ -1,7 +1,7 @@ @online{unitgrade_devel, - title={Unitgrade-devel (0.1.39): \texttt{pip install unitgrade-devel}}, + title={Unitgrade-devel (0.1.42): \texttt{pip install unitgrade-devel}}, url={https://lab.compute.dtu.dk/tuhe/unitgrade_private}, - urldate = {2022-06-15}, + urldate = {2022-09-16}, month={9}, publisher={Technical University of Denmark (DTU)}, author={Tue Herlau}, diff --git a/devel/example_devel/instructor/cache.db-shm b/examples/02631/instructor/week5/unitgrade_data/cache.db similarity index 69% rename from devel/example_devel/instructor/cache.db-shm rename to examples/02631/instructor/week5/unitgrade_data/cache.db index e7762b285bdaf5d8ee34b76d323fd18c34594666..4922ef2db92a2060c5a27c35bf00ec2ce8ec921c 100644 GIT binary patch literal 40960 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 literal 32768 zcmeI)*G^SI6oBC$6zqT;%TX*8MURLL>>`2%P(kciz=j@BY#`VbEO0MxVnX6$a6`Q3 z-WM=2Fxwd8jfp0b9m&6vmD!o>tTpq^{Ys9P6Ge5%49FV_>+F#2-6O+eAB$4WU#h+i z6!(8ld>9{1cYgbn{LBACsqYcho%khwZ2w$lgs+()r*h2-^|M3H3K`uo>h-64Q++n; z|Nia6GZ_jfpnw7jD4>7>3Mim}0tzUgfC36Apnw7jD4>7>3Mim}0tzUgfC36Apnw7j zD4>7>3Mim}0tzUgfC36Apnw7jD4>7>3Mim}0tzUgfC36Apnw9?Mj#{1|H%rofaWld z1>~}j1dCb9GFDN-dN#0;O>AKsmF!>_d)UW*4p7S>j!@50j?+Xlr#QnoTDZt%+PKPf z+POssx9Q>@59sC*eLUeAFL*^iZ+OSxv{CdT|EEA^n6{N2reMuwJ~`x(&mxwPBt;qJ zY-TGJY-cCC*-KTJ4pzfK4pT=1$7tjPCppbo&U1lFv~q=O+~6i@I=RDL?(>iydU?!K zp7WB|4DgosL{q>F1>zQ1&T7`geF*z25Q{(og{+OGvrQF<TVMr6l*WAs`zsKOz)Ff) z7fWZGDxiP@3Mim}0tzUgfC36Apnw7jD4>7>3Mim}0tzUgfC36Apnw7jD4>7>3Min! HzY>@P3UE-- diff --git a/examples/example_framework/instructor/cs102/Report2_handin_3_of_16.token b/examples/example_framework/instructor/cs102/Report2_handin_3_of_16.token new file mode 100644 index 0000000..a233c64 --- /dev/null +++ b/examples/example_framework/instructor/cs102/Report2_handin_3_of_16.token @@ -0,0 +1,188 @@ +# This file contains your results. Do not edit its content. Simply upload it as it is. +---------------------------------------------------------------------- ..ooO0Ooo.. ---------------------------------------------------------------------- +c3a65f79b5cf3f4dc9d011d60c14f19d064fc2ce5791e50ee371fde1eb3a641e873496c89e0f28e246b08a583b601223350fff3c09a36853ad603d454f604a49 33016 +---------------------------------------------------------------------- ..ooO0Ooo.. ---------------------------------------------------------------------- +.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. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2a45e4b..ec68e02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ mosspy -jinja2 +jinja2 # Used to create the _grade.py script. unitgrade # setuptools==57 # This is because of pyminifier (mumble, grumble) # pyminifier # No longer needed; bundled. diff --git a/setup.py b/setup.py index fea73ea..5b6bfea 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ setuptools.setup( packages=setuptools.find_packages(where="src"), include_package_data=True, python_requires=">=3.8", - install_requires=['numpy', "codesnipper", 'tabulate', 'tqdm', "pyfiglet", + install_requires=['unitgrade', 'numpy', "codesnipper", 'tabulate', 'tqdm', "pyfiglet", 'jinja2' "colorama", "coverage", # 'pyminifier', cannot use pyminifier because 2to3 issue. bundled. will that work? 'mosspy'], ) diff --git a/src/unitgrade_private/hidden_create_files.py b/src/unitgrade_private/hidden_create_files.py index 2924e22..b0d0aff 100644 --- a/src/unitgrade_private/hidden_create_files.py +++ b/src/unitgrade_private/hidden_create_files.py @@ -5,10 +5,10 @@ import inspect import time import os from unitgrade_private import hidden_gather_upload -import sys +# import sys import os import glob -from pupdb.core import PupDB +# from pupdb.core import PupDB data = """ {{head}} @@ -101,16 +101,16 @@ def setup_grade_file_report(ReportClass, execute=False, obfuscate=False, minify= 'coverage_files': cf } a = 34 - s, _ = dict2picklestring(artifacts['questions']) + # s, _ = dict2picklestring(artifacts['questions']) db['questions'] = artifacts['questions'] # ('questions', s) with open(report._artifact_file(), 'wb') as f: pickle.dump(db, f) for f in glob.glob(os.path.dirname(report._artifact_file()) + "/*.json") + glob.glob(os.path.dirname(report._artifact_file()) + "/cache.db*"): # blow old artifact files. should probably also blow the test cache. - if os.path.basename(f).startswith("main_config"): - continue - else: - os.remove(f) + # if os.path.basename(f).startswith("main_config"): + # continue + # else: + os.remove(f) from unitgrade_private.hidden_gather_upload import gather_report_source_include sources = gather_report_source_include(report) -- GitLab