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:
+
+![The dashboard](https://gitlab.compute.dtu.dk/tuhe/unitgrade/-/raw/master/docs/dashboard.png)
+
+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(), &#39;title&#39;), 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(), &#39;assert&#39;)] = {}</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(), &#34;return&#34;) ] = res</pre>\n<pre class=\"line after\"><span class=\"ws\">        </span>self._cache_put((self.cache_id(), &#34;time&#34;), 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(&#34;The current number is&#34;, 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[&#39;x1&#39;])</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&AMPe>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:
+
+![The dashboard](https://gitlab.compute.dtu.dk/tuhe/unitgrade/-/raw/master/docs/dashboard.png)
+
+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