diff --git a/devel/example_devel/instructor/cs108/Report2_handin_10_of_10.token b/devel/example_devel/instructor/cs108/Report2_handin_10_of_10.token new file mode 100644 index 0000000000000000000000000000000000000000..8ff976542514ba1b21e85bb39fee4496db182ed2 --- /dev/null +++ b/devel/example_devel/instructor/cs108/Report2_handin_10_of_10.token @@ -0,0 +1,314 @@ +# This file contains your results. Do not edit its content. Simply upload it as it is. +---------------------------------------------------------------------- ..ooO0Ooo.. ---------------------------------------------------------------------- +827e0833f0b9d63e1a7aa2c0fedf6e9c6ca44d4006be322b9b1877819d08c958c8d9c606040d36134177ebdf00517e91f684ba72cfd26949a5e45e94933cd522 55680 +---------------------------------------------------------------------- ..ooO0Ooo.. ---------------------------------------------------------------------- +.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. \ No newline at end of file diff --git a/devel/example_devel/instructor/cs108/Report2_handin_38_of_38.token b/devel/example_devel/instructor/cs108/Report2_handin_38_of_38.token deleted file mode 100644 index d0e33b2e8a9f4d0c5c3b5b6bcd1aacb5a9dad0a1..0000000000000000000000000000000000000000 --- a/devel/example_devel/instructor/cs108/Report2_handin_38_of_38.token +++ /dev/null @@ -1,249 +0,0 @@ -# This file contains your results. Do not edit its content. Simply upload it as it is. -### Content of cs102\deploy.py ### - -from cs102.report2 import Report2 -from unitgrade_private.hidden_create_files import setup_grade_file_report -from snipper.snip_dir import snip_dir - -if __name__ == "__main__": - setup_grade_file_report(Report2) - snip_dir("./", "../../students/cs102", clean_destination_dir=True, exclude=['*.token', 'deploy.py']) - - -### Content of cs102\homework1.py ### - -def reverse_list(mylist): #!f #!s;keeptags - """ - Given a list 'mylist' returns a list consisting of the same elements in reverse order. E.g. - reverse_list([1,2,3]) should return [3,2,1] (as a list). - """ - return list(reversed(mylist)) - -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 - -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])) #!s - - -### Content of cs102\report2.py ### - -from unitgrade.framework import Report, cache -from unitgrade.evaluate import evaluate_report_student -from cs102.homework1 import add, reverse_list -from unitgrade import UTestCase #!s - -class Week1(UTestCase): - 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])) #!s - - def test_output_capture(self): - with self.capture() as out: - print("hello world 42") # Genereate some output (i.e. in a homework script) - self.assertEqual(out.numbers[0], 42) # out.numbers is a list of all numbers generated - self.assertEqual(out.output, "hello world 42") # you can also access the raw output. - -class Week1Titles(UTestCase): #!s=b - """ The same problem as before with nicer titles """ - def test_add(self): - """ Test the addition method add(a,b) """ - self.assertEqualC(add(2,2)) - self.assertEqualC(add(-100, 5)) - - 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 #!s - - def ex_test_output_capture(self): - with self.capture() as out: - print("hello world 42") # Genereate some output (i.e. in a homework script) - self.assertEqual(out.numbers[0], 42) # out.numbers is a list of all numbers generated - self.assertEqual(out.output, "hello world 42") # you can also access the raw output. - - -class Question2(UTestCase): #!s=c - @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 #!s=c - - -import cs102 -class Report2(Report): - title = "CS 101 Report 2" - questions = [(Week1, 10), (Week1Titles, 8)] - pack_imports = [cs102] - -if __name__ == "__main__": - evaluate_report_student(Report2(), unmute=True) ----------------------------------------------------------------------- ..ooO0Ooo.. ---------------------------------------------------------------------- -41cf41393a14ea9a555597ad42fa7a922dbd6f3e1cf303d2ce3b3e56481b389490846ff847743732c3b3664b91565acc0471175bd2952c0f992541833eaafe73 26440 ----------------------------------------------------------------------- ..ooO0Ooo.. ---------------------------------------------------------------------- -.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. \ No newline at end of file diff --git a/devel/example_devel/instructor/cs108/db.pkl b/devel/example_devel/instructor/cs108/db.pkl index d5e7f185526f6f1e94e0a707668bef186d84fad8..4d91d3931e3377cfb0a6de8c39b187956cec0252 100644 Binary files a/devel/example_devel/instructor/cs108/db.pkl and b/devel/example_devel/instructor/cs108/db.pkl differ diff --git a/devel/example_devel/instructor/cs108/deploy.py b/devel/example_devel/instructor/cs108/deploy.py index 81ba32b57cd5c440582c966c47cce96241939350..86535e034b9fb635c961404822a7b6ebda121160 100644 --- a/devel/example_devel/instructor/cs108/deploy.py +++ b/devel/example_devel/instructor/cs108/deploy.py @@ -4,6 +4,7 @@ from snipper.snip_dir import snip_dir def main(with_coverage=True): mk_ok() + setup_grade_file_report(Report2, with_coverage=with_coverage, minify=False, obfuscate=False,bzip=False) @@ -21,5 +22,5 @@ if __name__ == "__main__": # print(data['details'][1]['items'] ) # None of that coverage shit. - # snip_dir("./", "../../students/cs108", clean_destination_dir=True, exclude=['*.token', 'deploy.py']) - main() + snip_dir("./", "../../students/cs108", clean_destination_dir=True, exclude=['*.token', 'deploy.py']) + main(with_coverage=False) diff --git a/devel/example_devel/instructor/cs108/homework1.py b/devel/example_devel/instructor/cs108/homework1.py index 9991f74fcb101ab47151b5b83148f9c936d1d314..8c7c0f9d9d78ed8cd7868e871eab1fa911fa041c 100644 --- a/devel/example_devel/instructor/cs108/homework1.py +++ b/devel/example_devel/instructor/cs108/homework1.py @@ -1,7 +1,6 @@ import numpy as np - def reverse_list(mylist): #!f """ Given a list 'mylist' returns a list consisting of the same elements in reverse order. E.g. @@ -19,7 +18,6 @@ def add(a,b): #!f x = 234 return a+b - def foo(): #!f """ Comment. """ bar() @@ -35,7 +33,6 @@ def linear_predict(X, w): #!f y = X @ w return y - if __name__ == "__main__": print(f"Your result of 2 + 2 = {add(2,2)}") print(f"Reversing a small list", reverse_list([2,3,5,7])) diff --git a/devel/example_devel/instructor/cs108/report_devel.py b/devel/example_devel/instructor/cs108/report_devel.py index 9da58ef01e05a8a0e531eaf4a4e2fbbbc64ac6fc..a551fc8b0b020c768895f5139828e276840dddcb 100644 --- a/devel/example_devel/instructor/cs108/report_devel.py +++ b/devel/example_devel/instructor/cs108/report_devel.py @@ -6,7 +6,7 @@ import time import numpy as np import pickle import os - +# from unitgrade.framework import dash def mk_bad(): with open(os.path.dirname(__file__)+"/db.pkl", 'wb') as f: @@ -18,12 +18,38 @@ def mk_ok(): d = {'x1': 1, 'x2': 2} pickle.dump(d, f) + +def formatHeader(fn): + from functools import wraps + @wraps(fn) + def wrapper(*args, **kw): + return fn(*args, **kw) + return wrapper + + class Numpy(UTestCase): + z = 234 + + def __getattr__(self, item): + print("hi there ", item) + return super().__getattr__(item) + + def __getattribute__(self, item): + print("oh hello sexy. ", item) + return super().__getattribute__(item) + @classmethod + # @dash def setUpClass(cls) -> None: + print("Dum di dai, I am running some setup code here.") + for i in range(10): + print("Hello world", i) print("Set up.") # must be handled seperately. - # raise Exception("bad set up class") + # assert False + @cache + def make_primes(self, n): + return primes(n) def test_bad(self): """ @@ -36,18 +62,18 @@ class Numpy(UTestCase): # return # self.assertEqual(1, 1) with open(os.path.dirname(__file__)+"/db.pkl", 'rb') as f: - # d = {'x1': 1, 'x2': 2} - # pickle.dump(d, f) d = pickle.load(f) # print(d) # assert False - for i in range(10): - print("The current number is", i) - # time.sleep(1) + # for i in range(10): + from tqdm import tqdm + for i in tqdm(range(100)): + # print("The current number is", i) + time.sleep(.01) self.assertEqual(1, d['x1']) + for b in range(10): + self.assertEqualC(add(3, b)) - # assert False - pass def test_weights(self): """ @@ -83,6 +109,6 @@ class Report2(Report): if __name__ == "__main__": # import texttestrunner import unittest - unittest.main(exit=False) + unittest.main() # evaluate_report_student(Report2()) diff --git a/devel/example_devel/instructor/cs108/report_devel_grade.py b/devel/example_devel/instructor/cs108/report_devel_grade.py index ba74cb097701d32063d19a8563095cc034d5a5ec..07ef022dcaa70d6bc25d363a7a98aaa03cad64d8 100644 --- a/devel/example_devel/instructor/cs108/report_devel_grade.py +++ b/devel/example_devel/instructor/cs108/report_devel_grade.py @@ -41,7 +41,9 @@ parser.add_argument('--unmute', action="store_true", help='Show result of prin parser.add_argument('--passall', action="store_true", help='Automatically pass all tests. Useful when debugging.') parser.add_argument('--noprogress', action="store_true", help='Disable progress bars.') -def evaluate_report_student(report, question=None, qitem=None, unmute=None, passall=None, ignore_missing_file=False, show_tol_err=False, show_privisional=True, noprogress=None): +def evaluate_report_student(report, question=None, qitem=None, unmute=None, passall=None, ignore_missing_file=False, + show_tol_err=False, show_privisional=True, noprogress=None, + generate_artifacts=True): args = parser.parse_args() if noprogress is None: noprogress = args.noprogress @@ -63,7 +65,8 @@ def evaluate_report_student(report, question=None, qitem=None, unmute=None, pass results, table_data = evaluate_report(report, question=question, show_progress_bar=not unmute and not noprogress, qitem=qitem, verbose=False, passall=passall, show_expected=args.showexpected, show_computed=args.showcomputed,unmute=unmute, - show_tol_err=show_tol_err) + show_tol_err=show_tol_err, + generate_artifacts=generate_artifacts) if question is None and show_privisional: @@ -105,6 +108,7 @@ class SequentialTestLoader(unittest.TestLoader): def 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, show_progress_bar=True, show_tol_err=False, + generate_artifacts=True, # Generate the artifact .json files. These are exclusively used by the dashboard. big_header=True): now = datetime.now() @@ -128,6 +132,7 @@ def evaluate_report(report, question=None, qitem=None, passall=False, verbose=Fa loader = SequentialTestLoader() for n, (q, w) in enumerate(report.questions): + q._generate_artifacts = generate_artifacts # Set whether artifact .json files will be generated. if question is not None and n+1 != question: continue suite = loader.loadTestsFromTestCase(q) @@ -399,23 +404,13 @@ def gather_report_source_include(report): print(f" * {m.__name__}") return sources -# def report_script_relative_location(report): -# """ -# Given the grade script corresponding to the 'report', work out it's relative location either compared to the -# package it is in or directory. -# """ -# if len(report.individual_imports) == 0: -# return "./" -# else: -# -# pass - def gather_upload_to_campusnet(report, output_dir=None, token_include_plaintext_source=False): # n = report.nL args = parser.parse_args() results, table_data = evaluate_report(report, show_help_flag=False, show_expected=False, show_computed=False, silent=True, show_progress_bar=not args.noprogress, big_header=not args.autolab, + generate_artifacts=False, ) print("") sources = {} @@ -510,9 +505,7 @@ def load_token(file_in): hash, l1 = info.split(" ") data = "".join( data.strip()[1:-1].splitlines() ) l1 = int(l1) - dictionary, b_hash = picklestring2dict(data) - assert len(data) == l1 assert b_hash == hash.strip() return dictionary, plain_text @@ -528,8 +521,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\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\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\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\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\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]) + ".json")\n\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\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\n evaluate_report_student(self, unmute=verbose, noprogress=not verbose)\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 # 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 # print("Sanity checking time using snapshot", snapshot_file)\n # print("and using self-identified file", self._file())\n\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 # print("Is this file run in local mode?", self._is_run_in_grade_mode())\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 #\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 #\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 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 = qq._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 _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\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 from unittest.case import TestCase\n from pupdb.core import PupDB\n db = PupDB(self._artifact_file())\n\n db.set(\'run_id\', np.random.randint(1000*1000))\n db.set("state", "running")\n db.set(\'coverage_files_changed\', None)\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\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\n # exi = result_._excinfo[0]._excinfo\n # tb = result_._excinfo[0]._excinfo[-1]\n # DebugTraceback(tb)\n # ex = exi[1]\n exi.__traceback__ = tb\n # tbe = _process_traceback(ex)\n dbt = DebugTraceback(exi)\n # dbt.render_traceback_text()\n sys.stderr.write(dbt.render_traceback_text())\n html = dbt.render_traceback_html(include_title="hello world")\n # print(HEADER)\n\n # from unittest.case import As\n db.set(\'wz_stacktrace\', html)\n db.set(\'state\', \'fail\')\n\n\n # print("> Set state of test to:", "fail", exi, tb)\n\n else:\n print("> Set state of test to:", "pass")\n db.set(\'state\', \'pass\')\n\n\n except Exception as e:\n print("-----------------.///////////////////////////////////////////////////////////////")\n # print(e)\n import traceback\n traceback.print_exc()\n raise e\n\n finally:\n\n sys.stdout = _stdout\n sys.stderr = _stderr\n\n std_capture.close()\n\n # stderr_capture.close()\n # if len(actual_errors)\n\n # print(result._test._error_fed_during_run)\n # print(self._error_fed_during_run)\n # print( result.errors[0][0]._error_fed_during_run )\n #\n # result_.errors[0][0]._error_fed_during_run\n\n # result_._excinfo[0].errisinstance(Exception)\n # import AssertionError\n from werkzeug.debug.tbtools import HEADER\n # from pupdb.core import PupDB\n # db = PupDB(self._artifact_file())\n\n # actual_errors\n\n\n return result_\n\n\n @classmethod\n def before_setup_called(cls):\n print("hi")\n # print("I am called before the fucking class is fucking made. setUpClass has been broken!")\n pass\n\n setUpClass_not_overwritten = False\n @classmethod\n def setUpClass(cls) -> None:\n cls.setUpClass_not_overwritten = True\n pass\n\n @classmethod\n def __new__(cls, *args, **kwargs):\n old_setup = cls.setUpClass\n def new_setup():\n cls.before_setup_called()\n try:\n old_setup()\n except Exception as e:\n raise e\n finally:\n pass\n\n cls.setUpClass = new_setup\n return super().__new__(cls)\n\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 inspect\n\n # file = inspect.getfile(self.__class__)\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\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 def _cache_file(self):\n return os.path.dirname(inspect.getabsfile(type(self))) + "/unitgrade_data/" + self.__class__.__name__ + ".pkl"\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._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._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._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, None)\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\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, show_tol_err=False, show_privisional=True, noprogress=None):\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\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 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 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 lzma\nimport base64\nimport textwrap\nimport hashlib\nimport bz2\nimport pickle\nimport os\nimport zipfile\nimport io\n\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\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\n# def report_script_relative_location(report):\n# """\n# Given the grade script corresponding to the \'report\', work out it\'s relative location either compared to the\n# package it is in or directory.\n# """\n# if len(report.individual_imports) == 0:\n# return "./"\n# else:\n#\n# pass\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 )\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\n\ndef dict2picklestring(dd):\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 b = base64.b64decode(picklestr)\n hash = hashlib.blake2b(b).hexdigest()\n dictionary = pickle.loads(lzma.decompress(b))\n return dictionary, hash\n\n\ntoken_sep = "-"*70 + " ..ooO0Ooo.. " + "-"*70\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\ndef load_token(file_in):\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\n dictionary, b_hash = picklestring2dict(data)\n\n assert len(data) == l1\n assert b_hash == hash.strip()\n return dictionary, plain_text\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.27"\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\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 @classmethod\n def setUpClass(cls) -> None:\n print("Set up.") # must be handled seperately.\n # raise Exception("bad set up class")\n\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 = {\'x1\': 1, \'x2\': 2}\n # pickle.dump(d, f)\n d = pickle.load(f)\n # print(d)\n # assert False\n for i in range(10):\n print("The current number is", i)\n # time.sleep(1)\n self.assertEqual(1, d[\'x1\'])\n\n # assert False\n pass\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\nimport cs108\nclass Report2(Report):\n title = "CS 101 Report 2"\n questions = [\n (Numpy, 10),\n ]\n pack_imports = [cs108]' -report1_payload = '800495c9020000000000007d94288c054e756d7079947d942868018c0a7365745570436c6173739486948c0474696d65948694473f7b6b400000000068018c08746573745f6261649486948c057469746c6594869468076801680786948c066173736572749486947d9468016807869468058694473f7300c00000000068018c0c746573745f77656967687473948694680986946811680168118694680c86947d9468016811869468058694473f44cc00000000006801681186948c08636f7665726167659486947d948c1263733130382f686f6d65776f726b312e7079947d94288c0b64656620666f6f28293a20944b188c142020202022222220436f6d6d656e742e202222229486948c0b6465662062617228293a20944b1b8c009486947573758c06636f6e666967947d948c13626c616b6532625f66696c655f686173686573945d94288c806533626432393138326330346430393339383337663665656532383132353463633933316664663433633765663532623139303636636161653463623836343739636131303266323234623536353565313732336462306264383035323931303538313161336561626364396234616366663139366435396332386532666261948c803233663637396166656366346137373462366164636464633136383562323264343236373031336630386166623764663366376530373037363633363065633339336531653936613437336131653534613235373636313263393162386563666462393833343366626165366634353039373830623462366337383634396231948c806233306634613464633032346462353734643565656263616663636638383934393336643761303363393930633531323334663363626234643435333830643063366534653462653234326534333733363763363261613837316431373961643236626365633233383865653232656363333536383264393035363362386335946573752e' +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\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\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\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\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# def dash(func):\n# if isinstance(func, classmethod):\n# raise Exception("the @dash-decorator was used in the wrong order. The right order is: @dash\\n@classmethod\\ndef setUpClass(cls):")\n#\n# def wrapper(*args, **kwargs):\n# print("Something is happening before the function is called.")\n# func(*args, **kwargs)\n# print("Something is happening after the function is called.")\n# return wrapper\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]) + ".json")\n\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\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 # 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 # print("Sanity checking time using snapshot", snapshot_file)\n # print("and using self-identified file", self._file())\n\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 # print("Is this file run in local mode?", self._is_run_in_grade_mode())\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 #\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 #\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 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 = qq._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 # def __call__(self, *args, **kwargs):\n # a = \'234\'\n # pass\n\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 if not self._generate_artifacts:\n return super().run(result)\n from unittest.case import TestCase\n from pupdb.core import PupDB\n\n db = PupDB(self._artifact_file())\n db.set(\'run_id\', np.random.randint(1000*1000))\n db.set("state", "running")\n db.set(\'coverage_files_changed\', None)\n\n # print("Re-running test")\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\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 else:\n db.set(\'state\', \'pass\')\n except Exception as e:\n print("-----------------.///////////////////////////////////////////////////////////////")\n # print(e)\n import traceback\n traceback.print_exc()\n raise e\n finally:\n sys.stdout = _stdout\n sys.stderr = _stderr\n std_capture.close()\n return result_\n\n @classmethod\n def before_setup_called(cls):\n print("hi")\n # print("I am called before the fucking class is fucking made. setUpClass has been broken!")\n pass\n\n _setUpClass_not_overwritten = False\n @classmethod\n def setUpClass(cls) -> None:\n cls._setUpClass_not_overwritten = True\n\n @classmethod\n def __new__(cls, *args, **kwargs):\n old_setup = cls.setUpClass\n def new_setup():\n raise Exception("Bad")\n cls.before_setup_called()\n if cls.setUpClass == UTestCase.setUpClass:\n print("Setup class not overwritten")\n else:\n print("Setup class is overwritten")\n\n try:\n old_setup()\n except Exception as e:\n raise e\n finally:\n pass\n\n # cls.setUpClass = new_setup\n ci = super().__new__(cls)\n ci.setUpClass = new_setup\n return ci\n\n # def inheritors(klass):\n # import new\n # z.q = new.instancemethod(method, z, None)\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\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 def _cache_file(self):\n return os.path.dirname(inspect.getabsfile(type(self))) + "/unitgrade_data/" + self.__class__.__name__ + ".pkl"\n\n @classmethod\n def _artifact_file_for_setUpClass(cls):\n cf = os.path.dirname(inspect.getabsfile(cls)) + "/unitgrade_data/" + cls.__name__\n return os.path.join(os.path.dirname(self._cache_file()), "-setUpClass.json")\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._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._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._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, None)\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\n# subclasses = set()\n# work = [UTestCase]\n# while work:\n# parent = work.pop()\n# for child in parent.__subclasses__():\n# if child not in subclasses:\n# subclasses.add(child)\n# work.append(child)\n# return subclasses\n# import builtins\n# ga = builtins.getattr\n# def my_funky_getatt(a,b,c=None):\n# print("ga", a, b, c)\n# return ga(a,b,c)\n# builtins.getattr = my_funky_getatt\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\n# import __builtin__\n# all subclasses which are known at this point.\n# def get_all_subclasses(cls):\n# all_subclasses = []\n#\n# for subclass in cls.__subclasses__():\n# all_subclasses.append(subclass)\n# all_subclasses.extend(get_all_subclasses(subclass))\n#\n# return all_subclasses\n#\n# a = 234\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 lzma\nimport base64\nimport textwrap\nimport hashlib\nimport bz2\nimport pickle\nimport os\nimport zipfile\nimport io\n\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\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\n\ndef dict2picklestring(dd):\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 b = base64.b64decode(picklestr)\n hash = hashlib.blake2b(b).hexdigest()\n dictionary = pickle.loads(lzma.decompress(b))\n return dictionary, hash\n\n\ntoken_sep = "-"*70 + " ..ooO0Ooo.. " + "-"*70\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\ndef load_token(file_in):\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\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.1"\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\n\ndef formatHeader(fn):\n from functools import wraps\n @wraps(fn)\n def wrapper(*args, **kw):\n return fn(*args, **kw)\n return wrapper\n\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\n # @dash\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 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\n # assert False\n pass\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\nimport cs108\nclass Report2(Report):\n title = "CS 101 Report 2"\n questions = [\n (Numpy, 10),\n ]\n pack_imports = [cs108]' +report1_payload = '8004955c020000000000007d94288c054e756d7079947d942868018c0a7365745570436c6173739486948c0474696d65948694473f5a7c000000000068018c08746573745f6261649486948c057469746c6594869468076801680786948c066173736572749486947d9468016807869468058694473ff041eac000000068018c0c746573745f77656967687473948694680986946811680168118694680c86947d9468016811869468058694473f4d120000000000758c06636f6e666967947d948c13626c616b6532625f66696c655f686173686573945d94288c806533626432393138326330346430393339383337663665656532383132353463633933316664663433633765663532623139303636636161653463623836343739636131303266323234623536353565313732336462306264383035323931303538313161336561626364396234616366663139366435396332386532666261948c803765633535633764313137383538356537343162346564653165353764353030393433646539303935326361636331666662643036633435636232613163666561663962613636383032383562643235323062343166623933373061646231643330633531386261383737363935373031333239653562383534663934373536948c803862366232646531396233353632346166633863653935343566303639643361383765313131343766346562623461666163333734313435626639656663636334336165346637623535383164633166393133363264653030636332633134366162393631633736363931356331636663356231363662356536383333353831946573752e' name="Report2" report = source_instantiate(name, report1_source, report1_payload) 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 index 6a2c931f4f02157c4fba8597795ca0dd91144e5c..58055dc04e49b6d6197753fb67fe7ff011f4762a 100644 --- a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_bad.json +++ b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_bad.json @@ -1 +1 @@ -{"run_id": 345491, "state": "running", "coverage_files_changed": null} \ No newline at end of file +{"run_id": 458734, "state": "fail", "coverage_files_changed": null, "stdout": [[0, "oh hello sexy. _testMethodName\noh hello sexy. test_bad\noh hello sexy. __class__\noh hello sexy. __unittest_expecting_failure__\nhi there __unittest_expecting_failure__\noh hello sexy. _callSetUp\noh hello sexy. _with_coverage\noh hello sexy. setUp\noh hello sexy. _callTestMethod\noh hello sexy. _ensure_cache_exists\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. _testMethodDoc\noh hello sexy. _cache_put\noh hello sexy. cache_id\noh hello sexy. __class__\noh hello sexy. _testMethodName\noh hello sexy. shortDescriptionStandard\noh hello sexy. _testMethodDoc\noh hello sexy. _testMethodName\noh hello sexy. _ensure_cache_exists\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. _cache2\noh hello sexy. cache_id\noh hello sexy. __class__\noh hello sexy. _testMethodName\n\u001b[31m\r 0%| | 0/100 [00:00<?, ?it/s]\u001b[37m"], [1, "\u001b[31m\r 10%|# | 10/100 [00:00<00:00, 98.47it/s]\u001b[37m"], [2, "\u001b[31m\r 20%|## | 20/100 [00:00<00:00, 96.03it/s]\u001b[37m\u001b[31m\r 30%|### | 30/100 [00:00<00:00, 96.12it/s]\u001b[37m"], [3, "\u001b[31m\r 40%|#### | 40/100 [00:00<00:00, 96.49it/s]\u001b[37m"], [4, "\u001b[31m\r 50%|##### | 50/100 [00:00<00:00, 97.36it/s]\u001b[37m\u001b[31m\r 60%|###### | 60/100 [00:00<00:00, 97.71it/s]\u001b[37m"], [5, "\u001b[31m\r 70%|####### | 70/100 [00:00<00:00, 96.98it/s]\u001b[37m"], [6, "\u001b[31m\r 80%|######## | 80/100 [00:00<00:00, 97.46it/s]\u001b[37m\u001b[31m\r 90%|######### | 90/100 [00:00<00:00, 97.87it/s]\u001b[37m"], [7, "\u001b[31m\r100%|##########| 100/100 [00:01<00:00, 98.15it/s]\u001b[37m\u001b[31m\u001b[37m\u001b[31m\r100%|##########| 100/100 [00:01<00:00, 97.46it/s]\u001b[37m\u001b[31m\n\u001b[37moh hello sexy. assertEqual\noh hello sexy. _getAssertEqualityFunc\noh hello sexy. _type_equality_funcs\noh hello sexy. _baseAssertEqual\noh hello sexy. assertEqualC\noh hello sexy. wrap_assert\noh hello sexy. assertEqual\noh hello sexy. cache_id\noh hello sexy. __class__\noh hello sexy. _testMethodName\noh hello sexy. _cache_contains\noh hello sexy. _ensure_cache_exists\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. _cache_get\noh hello sexy. _ensure_cache_exists\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. _assert_cache_index\nWarning, framework missing cache index (('Numpy', 'test_bad'), 'assert') id = 0 - The test will be skipped for now.\noh hello sexy. _setup_answers_mode\noh hello sexy. _cache_put\noh hello sexy. _ensure_cache_exists\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. _assert_cache_index\noh hello sexy. _setup_answers_mode\noh hello sexy. _getAssertEqualityFunc\noh hello sexy. _baseAssertEqual\noh hello sexy. _formatMessage\noh hello sexy. longMessage\noh hello sexy. failureException\noh hello sexy. _callTearDown\noh hello sexy. tearDown\noh hello sexy. _with_coverage\noh hello sexy. doCleanups\noh hello sexy. _outcome\noh hello sexy. _cleanups\noh hello sexy. _feedErrorsToResult\noh hello sexy. cache_id\noh hello sexy. __class__\noh hello sexy. _testMethodName\noh hello sexy. _cache_contains\noh hello sexy. _ensure_cache_exists\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. _testMethodDoc\noh hello sexy. _testMethodDoc\noh hello sexy. cache_id\noh hello sexy. __class__\noh hello sexy. _testMethodName\n\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.\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. failureException\noh hello sexy. failureException\noh hello sexy. __class__\noh hello sexy. _error_fed_during_run\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 516, in _callTestMethod\n res = testMethod()\n File \"/home/tuhe/Documents/unitgrade_private/devel/example_devel/instructor/cs108/report_devel.py\", line 75, in test_bad\n self.assertEqualC(add(3, b))\n File \"/home/tuhe/Documents/unitgrade/src/unitgrade/framework.py\", line 613, in assertEqualC\n self.wrap_assert(self.assertEqual, first, msg)\n File \"/home/tuhe/Documents/unitgrade/src/unitgrade/framework.py\", line 601, in wrap_assert\n assert_fun(first, _expected, *args, **kwargs)\nAssertionError: 3 != 'Key 0 not found in cache; framework files missing. Please run deploy()'\n\u001b[37m"]], "wz_stacktrace": "<div class=\"traceback\">\n <h3>Traceback <em>(most recent call last)</em>:</h3>\n <ul><li><div class=\"frame\" id=\"frame-140591462072832\">\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-140591462169200\">\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-140591462169312\">\n <h4>File <cite class=\"filename\">\"/home/tuhe/Documents/unitgrade/src/unitgrade/framework.py\"</cite>,\n line <em class=\"line\">516</em>,\n in <code class=\"function\">_callTestMethod</code></h4>\n <div class=\"source \"><pre class=\"line before\"><span class=\"ws\"> </span>self._ensure_cache_exists() # Make sure cache is there.</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>if self._testMethodDoc is not None:</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>self._cache_put((self.cache_id(), 'title'), self.shortDescriptionStandard())</pre>\n<pre class=\"line before\"><span class=\"ws\"></span> </pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>self._cache2[(self.cache_id(), 'assert')] = {}</pre>\n<pre class=\"line current\"><span class=\"ws\"> </span>res = testMethod()</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>elapsed = time.time() - t</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>self._get_outcome()[ (self.cache_id(), "return") ] = res</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>self._cache_put((self.cache_id(), "time"), elapsed)</pre>\n<pre class=\"line after\"><span class=\"ws\"></span> </pre>\n<pre class=\"line after\"><span class=\"ws\"></span> </pre></div>\n</div>\n\n<li><div class=\"frame\" id=\"frame-140591462169424\">\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\">75</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 tqdm(range(100)):</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span># print("The current number is", i)</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>time.sleep(.01)</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>self.assertEqual(1, d['x1'])</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>for b in range(10):</pre>\n<pre class=\"line current\"><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>\n<pre class=\"line after\"><span class=\"ws\"> </span>"""</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>Hints:</pre></div>\n</div>\n\n<li><div class=\"frame\" id=\"frame-140591462169536\">\n <h4>File <cite class=\"filename\">\"/home/tuhe/Documents/unitgrade/src/unitgrade/framework.py\"</cite>,\n line <em class=\"line\">613</em>,\n in <code class=\"function\">assertEqualC</code></h4>\n <div class=\"source \"><pre class=\"line before\"><span class=\"ws\"> </span>print("> expected", _expected)</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>print(e)</pre>\n<pre class=\"line before\"><span class=\"ws\"></span> </pre>\n<pre class=\"line before\"><span class=\"ws\"></span> </pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>def assertEqualC(self, first, msg=None):</pre>\n<pre class=\"line current\"><span class=\"ws\"> </span>self.wrap_assert(self.assertEqual, first, msg)</pre>\n<pre class=\"line after\"><span class=\"ws\"></span> </pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>def _shape_equal(self, first, second):</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>a1 = np.asarray(first).squeeze()</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>a2 = np.asarray(second).squeeze()</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>msg = None</pre></div>\n</div>\n\n<li><div class=\"frame\" id=\"frame-140591462169648\">\n <h4>File <cite class=\"filename\">\"/home/tuhe/Documents/unitgrade/src/unitgrade/framework.py\"</cite>,\n line <em class=\"line\">601</em>,\n in <code class=\"function\">wrap_assert</code></h4>\n <div class=\"source \"><pre class=\"line before\"><span class=\"ws\"> </span># The order of these calls is important. If the method assert fails, we should still store the correct result in cache.</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>cache[id] = first</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>self._cache_put(key, cache)</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>self._assert_cache_index += 1</pre>\n<pre class=\"line before\"><span class=\"ws\"> </span>if not self._setup_answers_mode:</pre>\n<pre class=\"line current\"><span class=\"ws\"> </span>assert_fun(first, _expected, *args, **kwargs)</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>else:</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>try:</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>assert_fun(first, _expected, *args, **kwargs)</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>except Exception as e:</pre>\n<pre class=\"line after\"><span class=\"ws\"> </span>print("Mumble grumble. Cache function failed during class setup. Most likely due to old cache. Re-run deploy to check it pass.", id)</pre></div>\n</div>\n</ul>\n <blockquote>AssertionError: 3 != 'Key 0 not found in cache; framework files missing. Please run deploy()'\n</blockquote>\n</div>\n"} \ No newline at end of file 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 new file mode 100644 index 0000000000000000000000000000000000000000..78741a0bca6e3880db82497c26b49c6a251ccaa7 --- /dev/null +++ b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy-test_weights.json @@ -0,0 +1 @@ +{"coverage_files_changed": null, "run_id": 613968, "state": "pass", "stdout": [[0, "oh hello sexy. _testMethodName\noh hello sexy. test_weights\noh hello sexy. __class__\noh hello sexy. __unittest_expecting_failure__\nhi there __unittest_expecting_failure__\noh hello sexy. _callSetUp\noh hello sexy. _with_coverage\noh hello sexy. setUp\noh hello sexy. _callTestMethod\noh hello sexy. _ensure_cache_exists\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. _testMethodDoc\noh hello sexy. _cache_put\noh hello sexy. cache_id\noh hello sexy. __class__\noh hello sexy. _testMethodName\noh hello sexy. shortDescriptionStandard\noh hello sexy. _testMethodDoc\noh hello sexy. _testMethodName\noh hello sexy. _ensure_cache_exists\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. _cache2\noh hello sexy. cache_id\noh hello sexy. __class__\noh hello sexy. _testMethodName\noh hello sexy. assertEqual\noh hello sexy. _getAssertEqualityFunc\noh hello sexy. _type_equality_funcs\noh hello sexy. _baseAssertEqual\noh hello sexy. _get_outcome\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. cache_id\noh hello sexy. __class__\noh hello sexy. _testMethodName\noh hello sexy. _cache_put\noh hello sexy. cache_id\noh hello sexy. __class__\noh hello sexy. _testMethodName\noh hello sexy. _ensure_cache_exists\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. _callTearDown\noh hello sexy. tearDown\noh hello sexy. _with_coverage\noh hello sexy. doCleanups\noh hello sexy. _outcome\noh hello sexy. _cleanups\noh hello sexy. _feedErrorsToResult\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. __class__\noh hello sexy. _error_fed_during_run\n"]]} \ No newline at end of file diff --git a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy.pkl b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy.pkl index 52c4f5cbb565b14edbf6db2bbb3b3c6ed7cfb186..9dfd8b4c668a3122831f4ba8f5a16227110cb4fa 100644 Binary files a/devel/example_devel/instructor/cs108/unitgrade_data/Numpy.pkl and b/devel/example_devel/instructor/cs108/unitgrade_data/Numpy.pkl differ 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 index a6a737ade51bb699e3f46626e29ca5d66584b3ae..6fade8d417fee2bb12951531ebd0d7354f278d43 100644 --- 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 @@ -1 +1 @@ -{"encoding_scheme": " from unitgrade_private.hidden_gather_upload import dict2picklestring, picklestring2dict;", "questions": "/Td6WFoAAATm1rRGAgAhARYAAAB0L+Wj4AHyAVFdAEABDnx/coDphHtjJz/Hf6BMJ8jsKcX6D2GlIpCTdQefBtKe4zVRUqE7IM4RD5R3T2C+GesRy0Q5CgrFjodW6xsauaOPVgMwAO1n3axAyU1UhpvS1V4sPs0g7xaNtsCv8oRoe1AhKnl3IFDf6Gg6nXO36ces1MgE7xDz9CSsQ5T2chCmCFLNziwvyXiZKmi6MvcRQ49bpAWpgL4hLMkYc3stfxkRNFCND+MKghupeHwxC4fWNFnP648dKpkQg5xXbkFyD+544w0PH+PJ5pebdXG1+e6LAMSZhOnTHNgUV/SOoiYRLohCowLRTz82ihjKzZH+EqvquWg5r0Yx3Ja1gRz3xz+q4ucPm5sFnELtxqjQdRQYpfjlaDlfNe0GiwzrpgOXv1Vdggdv/bafsf2KXpOkHIRXexotRNAJX9b9f1h2y/P3pOsllmmzbQXfJYsgvXoAAAAAHE5f2fQPWZMAAe0C8wMAAPGI2oWxxGf7AgAAAAAEWVo=", "root_dir": "/home/tuhe/Documents/unitgrade_private/devel/example_devel/instructor", "relative_path": "cs108/report_devel.py", "modules": ["cs108", "report_devel"]} \ No newline at end of file +{"encoding_scheme": " from unitgrade_private.hidden_gather_upload import dict2picklestring, picklestring2dict;", "questions": "/Td6WFoAAATm1rRGAgAhARYAAAB0L+Wj4AHyAVFdAEABDnx/coDphHtjJz/Hf6BMJ8jsKcX6D2GlIpCTdQefBtKe4zVRUqE7IM4RD5R3T2C+GesRy0Q5CgrFjodW6xsauaOPVgMwAO1n3axAyU1UhpvS1V4sPs0g7xaNtsCv8oRoe1AhKnl3IFDf6Gg6nXO36ces1MgE7xDz9CSsQ5T2chCmCFLNziwvyXiZKmi6MvcRQ49bpAWpgL4hLMkYc3stfxkRNFCND+MKghupeHwxC4fWNFnP648dKpkQg5xXbkFyD+544w0PH+PJ5pebdXG1+e6LAMSZhOnTHNgUV/SOoiYRLohCowLRTz82ihjKzZH+EqvquWg5r0Yx3Ja1gRz3xz+q4ucPm5sFnELtxqjQdRQYpfjlaDlfNe0GiwzrpgOXv1Vdggdv/bafsYvcXpOkHIRXexotRNAJX9b9f1h2y/P3pMnYlmmzbQXfJYsgvXoAAAAAyhvQuqp9FQ4AAe0C8wMAAPGI2oWxxGf7AgAAAAAEWVo=", "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/setup.py b/setup.py index 62b6cf74ddbb64d6fd5e8634043e12f5a449042c..e891c68d311d34fcc259323ea428b50aad827912 100644 --- a/setup.py +++ b/setup.py @@ -31,5 +31,6 @@ setuptools.setup( include_package_data=True, python_requires=">=3.8", install_requires=['numpy', "unitgrade", "codesnipper", 'tabulate', 'tqdm', "pyfiglet", - "colorama", "coverage", 'mosspy', 'pyminifier', 'mosspy'], + "colorama", "coverage", 'mosspy', # 'pyminifier', cannot use pyminifier because 2to3 issue. + 'mosspy'], ) diff --git a/src/unitgrade_devel.egg-info/PKG-INFO b/src/unitgrade_devel.egg-info/PKG-INFO index cb94e5ab4f9610d60babc3c71a7cb8ac5eba8f87..3825a47257c27e369284a5234b345a6fd04d9ada 100644 --- a/src/unitgrade_devel.egg-info/PKG-INFO +++ b/src/unitgrade_devel.egg-info/PKG-INFO @@ -1,12 +1,13 @@ Metadata-Version: 2.1 Name: unitgrade-devel -Version: 0.1.39 +Version: 0.1.41 Summary: A set of tools to develop unitgrade tests and reports and later evaluate them Home-page: https://lab.compute.dtu.dk/tuhe/unitgrade_private Author: Tue Herlau Author-email: tuhe@dtu.dk License: MIT Project-URL: Bug Tracker, https://lab.compute.dtu.dk/tuhe/unitgrade_private/issues +Platform: UNKNOWN Classifier: Programming Language :: Python :: 3 Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent @@ -17,20 +18,8 @@ License-File: LICENSE # Unitgrade-devel **Note: This is the development version of unitgrade. If you are a student, please see http://gitlab.compute.dtu.dk/tuhe/unitgrade.** -Unitgrade is an automatic report and exam evaluation framework that enables instructors to offer automatically evaluated programming assignments. It is currently used in 02465: https://gitlab.gbar.dtu.dk/02465material/02465students - -## Why not (alternative online automatic evaluation framework)? -I think the most important thing to ask from an automatic evaluation framework is that it helps the students become better programmers. -Automatic evaluation frameworks are fundamentally about testing code. We know testing works, but we also know that not all ways of doing tests are equally good. Online testing has several clear disadvantages: - - - You need to upload code and often press to see output - - No debugger - - Often encourage/requires coding in the browser, i.e. without IDE support - - They are often constraining, meaning problems must be phrased in a less than optimal way to suit the evaluation framework - -For these reasons, we would never accept an online evaluation tool as a testing tool -- so why should we ask that of students? - -Unitgrade is different because it is build on pythons `unittest` framework. This means tests are specified simply as unittests and will integrate with any modern IDE. What it offers beyond `unittest` is the ability to collect tests in reports (for automatic evaluation) and an easy and 100% safe mechanism for verifying the students results and creating additional, hidden tests. A cache system allows instructors to automatically create test-answers based on a working solution. +Unitgrade is an automatic report and exam evaluation framework that enables instructors to offer automatically evaluated programming assignments. + Unitgrade is build on pythons `unittest` framework so that the tests can be specified in a familiar syntax and will integrate with any modern IDE. What it offers beyond `unittest` is the ability to collect tests in reports (for automatic evaluation) and an easy and 100% safe mechanism for verifying the students results and creating additional, hidden tests. A powerful cache system allows instructors to automatically create test-answers based on a working solution. - 100% Python `unittest` compatible - No configuration files @@ -51,6 +40,14 @@ pip install unitgrade-devel ``` This will install `unitgrade-devel` (this package) and all dependencies to get you started. +## Overview + + +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. + ### Videos Videos where I try to talk and code my way through the examples can be found on youtube: @@ -62,7 +59,7 @@ Videos where I try to talk and code my way through the examples can be found on - Autolab: https://youtu.be/h5mqR8iNMwM # Instructions and examples of use -The examples can be found in the `/examples` directory: https://gitlab.compute.dtu.dk/tuhe/unitgrade_private/-/tree/master/examples . +The examples can be found in the `/examples` directory: https://gitlab.compute.dtu.dk/tuhe/unitgrade_private/-/tree/master/examples ## A simple example Unitgrade makes the following assumptions: @@ -256,7 +253,7 @@ When this is run, the titles are shown as follows: | | | |_ __ _| |_| | \/_ __ __ _ __| | ___ | | | | '_ \| | __| | __| '__/ _` |/ _` |/ _ \ | |_| | | | | | |_| |_\ \ | | (_| | (_| | __/ - \___/|_| |_|_|\__|\____/_| \__,_|\__,_|\___| v0.1.17, started: 21/09/2021 11:57:05 + \___/|_| |_|_|\__|\____/_| \__,_|\__,_|\___| v0.1.17, started: 19/05/2022 15:14:09 CS 102 Report 2 Question 1: Week1 @@ -270,7 +267,7 @@ 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 11:57:05 (0 minutes, 0 seconds)....................................................................16/16 +Total points at 15:14:09 (0 minutes, 0 seconds)....................................................................16/16 Including files in upload... * cs102 @@ -523,7 +520,7 @@ 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_example/deploy_autolab.py +# autolab_token_upload/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" @@ -533,7 +530,7 @@ Concretely, the following code will download and build the image (note this code 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_example/deploy_autolab.py +# autolab_token_upload/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" @@ -568,12 +565,13 @@ 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.35): \texttt{pip install unitgrade-devel}}, + title={Unitgrade-devel (0.1.39): \texttt{pip install unitgrade-devel}}, url={https://lab.compute.dtu.dk/tuhe/unitgrade_private}, - urldate = {2022-05-19}, + urldate = {2022-06-15}, month={9}, publisher={Technical University of Denmark (DTU)}, author={Tue Herlau}, year={2022}, } ``` + diff --git a/src/unitgrade_devel.egg-info/requires.txt b/src/unitgrade_devel.egg-info/requires.txt index d067d6ef7575d321adbc2bc4308f24755138dfd4..c27729819985e85184b07e311121ddc4c95a6f6a 100644 --- a/src/unitgrade_devel.egg-info/requires.txt +++ b/src/unitgrade_devel.egg-info/requires.txt @@ -1,11 +1,10 @@ -numpy -unitgrade codesnipper -tabulate -tqdm -pyfiglet colorama coverage mosspy -pyminifier mosspy +numpy +pyfiglet +tabulate +tqdm +unitgrade diff --git a/src/unitgrade_private/hidden_create_files.py b/src/unitgrade_private/hidden_create_files.py index c6543f811a98108b810d7d6c649613f1ff5ab4b8..dde1be0d4924a44e16795451a21ba88b82a77cbb 100644 --- a/src/unitgrade_private/hidden_create_files.py +++ b/src/unitgrade_private/hidden_create_files.py @@ -61,6 +61,7 @@ def setup_grade_file_report(ReportClass, execute=False, obfuscate=False, minify= db.set('root_dir', root_dir) db.set('relative_path', relative_path) db.set('modules', modules) + db.set('token_stub', os.path.dirname(relative_path) +"/" + ReportClass.__name__ + "_handin") # Set up the artifact file. Do this by looping over all tests in the report. Assumes that all are of the form UTestCase. from unitgrade.evaluate import SequentialTestLoader diff --git a/src/unitgrade_private/hidden_gather_upload.py b/src/unitgrade_private/hidden_gather_upload.py index 0764caa1009c3a45e6dd1a035dfaa1b89593aab9..97f8a76704f2c71c5d6f6eb6be631067bca294dd 100644 --- a/src/unitgrade_private/hidden_gather_upload.py +++ b/src/unitgrade_private/hidden_gather_upload.py @@ -96,23 +96,13 @@ def gather_report_source_include(report): print(f" * {m.__name__}") return sources -# def report_script_relative_location(report): -# """ -# Given the grade script corresponding to the 'report', work out it's relative location either compared to the -# package it is in or directory. -# """ -# if len(report.individual_imports) == 0: -# return "./" -# else: -# -# pass - def gather_upload_to_campusnet(report, output_dir=None, token_include_plaintext_source=False): # n = report.nL args = parser.parse_args() results, table_data = evaluate_report(report, show_help_flag=False, show_expected=False, show_computed=False, silent=True, show_progress_bar=not args.noprogress, big_header=not args.autolab, + generate_artifacts=False, ) print("") sources = {} @@ -208,9 +198,7 @@ def load_token(file_in): hash, l1 = info.split(" ") data = "".join( data.strip()[1:-1].splitlines() ) l1 = int(l1) - dictionary, b_hash = picklestring2dict(data) - assert len(data) == l1 assert b_hash == hash.strip() return dictionary, plain_text diff --git a/src/unitgrade_private/version.py b/src/unitgrade_private/version.py index b87f46d7d3bd8b95f680f7c94b8da9e9d4beb659..6cb6f94cdd97cf290997d276d7d3c6a2dd9cf5ee 100644 --- a/src/unitgrade_private/version.py +++ b/src/unitgrade_private/version.py @@ -1,2 +1,2 @@ -__version__ = "0.1.40" +__version__ = "0.1.41" diff --git a/vue_flask_stuff/server/None.lock b/vue_flask_stuff/server/None.lock deleted file mode 100755 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/vue_flask_stuff/server/app.py b/vue_flask_stuff/server/app.py deleted file mode 100644 index ad705639a3a8942cf6ec94e8f0c5e6be82b1ec54..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/app.py +++ /dev/null @@ -1,317 +0,0 @@ -#!/usr/bin/env python3 -import fnmatch -from queue import Queue -from threading import Lock -from server.watcher import Watcher -import argparse -import datetime -import subprocess -from flask import Flask, render_template -from flask_socketio import SocketIO -import pty -import os -import subprocess -import select -import termios -import struct -import fcntl -import shlex -import logging -import sys -import glob -from pupdb.core import PupDB -from unitgrade_private.hidden_gather_upload import picklestring2dict -from unitgrade_private.hidden_gather_upload import dict2picklestring, picklestring2dict -from pathlib import Path -from app_helpers import get_available_reports -from server.file_change_handler import FileChangeHandler - -logging.getLogger("werkzeug").setLevel(logging.ERROR) -__version__ = "0.0.1" -from werkzeug.debug import DebuggedApplication - -def mkapp(base_dir="./"): - app = Flask(__name__, template_folder="templates", static_folder="static", static_url_path="/static") - x = {'watcher': None, 'handler': None} # super scope dictionary for program state. - - app.config["SECRET_KEY"] = "secret!" - app.config["fd"] = None - app.config["TEMPLATES_AUTO_RELOAD"] = True - app.config["child_pid"] = None - socketio = SocketIO(app) - # DebuggedApplication(app, evalex=True, pin_security=False) - - available_reports = get_available_reports(jobfolder=base_dir) - current_report = {} - watched_files_lock = Lock() - watched_files_dictionary = {} - - - def do_something(file_pattern): - """ - Oh crap, `file` has changed on disk. We need to open it, look at it, and then do stuff based on what is in it. - That is, we push all chnages in the file to clients. - - We don't know what are on the clients, so perhaps push everything and let the browser resolve it. - """ - with watched_files_lock: - file = watched_files_dictionary[file_pattern]['file'] - type = watched_files_dictionary[file_pattern]['type'] - lrc = watched_files_dictionary[file_pattern]['last_recorded_change'] - - if type == 'question_json': # file.endswith(".json"): - if file is None: - return # There is nothing to do, the file does not exist. - db = PupDB(file) - if "state" not in db.keys(): # Test has not really been run yet. There is no reason to submit this change to the UI. - return - state = db.get('state') - key = os.path.basename(file)[:-5] - wz = db.get('wz_stacktrace') if 'wz_stacktrace' in db.keys() else None - if wz is not None: - print(wz) - wz = wz.replace('<div class="traceback">', f'<div class="traceback"><div class="{key}-traceback">') - wz += "</div>" - - - coverage_files_changed = db.get('coverage_files_changed') if 'coverage_files_changed' in db.keys() else None - socketio.emit('testupdate', {"id": key, 'state': state, 'stacktrace': wz, 'stdout': db.get('stdout'), 'run_id': db.get('run_id'), - 'coverage_files_changed': coverage_files_changed}, namespace="/status") - elif type =='coverage': - if lrc is None: # Program startup. We don't care about this. - return - db = get_report_database() - for q in db['questions']: - for i in db['questions'][q]['tests']: - # key = '-'.join(i) - test_invalidated = False - - for f in db['questions'][q]['tests'][i]['coverage_files']: - - # fnmatch.fnmatch(f, file_pattern) - if fnmatch.fnmatch(file, "**/" + f): - # This file has been matched. The question is now invalid. - test_invalidated = True - break - - if test_invalidated: - # Why not simply write this bitch into the db? - dbf = current_report['root_dir'] + "/" + current_report['questions'][q]['tests'][i]['artifact_file'] - db = PupDB(dbf) - db.set('coverage_files_changed', [file]) - # print("dbf", dbf) - # print("marking a test as invalidated: ", db) - - - print(file, type) - else: - import subprocess - from cs108 import deploy - - subprocess.run(["python", "report_devel_grade.py"], cwd = os.path.dirname( deploy.__file__ )) - - - print(file, type) - - - def get_json_base(jobfolder): - return current_report['json'] - - def get_report_database(): - dbjson = get_json_base(base_dir) - db = PupDB(dbjson) - from unitgrade_private.hidden_gather_upload import picklestring2dict - rs = {} - for k in db.keys(): - if k == 'questions': - qenc, _ = picklestring2dict(db.get("questions")) - rs['questions'] = qenc # This feels like a good place to find the test-file stuff. - else: - rs[k] = db.get(k) - - lpath_full = Path(os.path.normpath(os.path.dirname(dbjson) + "/../" + os.path.basename(dbjson)[12:-5] + ".py")) - rpath = Path(db.get('relative_path')) - base = lpath_full.parts[:-len(rpath.parts)] - - rs['local_base_dir_for_test_module'] = str(Path(*base)) - rs['test_module'] = ".".join(db.get('modules')) - return rs - - def select_report_file(json): - current_report.clear() - for k, v in available_reports[json].items(): - current_report[k] = v - - def mkempty(pattern, type): - import fnmatch - fls = glob.glob(current_report['root_dir'] + pattern) - f = None if len(fls) == 0 else fls[0] # Bootstrap with the given best matched file. - return {'type': type, 'last_recorded_change': None, 'last_handled_change': None, 'file': f} - with watched_files_lock: - watched_files_dictionary.clear() - db = PupDB(json) - dct = picklestring2dict(db.get('questions'))[0] - for q in dct.values(): - for i in q['tests'].values(): - file = "*/"+i['artifact_file'] - watched_files_dictionary[file] = mkempty(file, 'question_json') # when the file was last changed and when that change was last handled. - for c in i['coverage_files']: - file = "*/"+c - watched_files_dictionary[file] = mkempty(file, "coverage") - - tdir = "*/"+os.path.dirname(current_report['relative_path_token']) + "/" + os.path.basename(current_report['relative_path'])[:-3] + "*.token" - watched_files_dictionary[tdir] = mkempty(file, 'token') - - for l in ['watcher', 'handler']: - if x[l] is not None: x[l].close() - - x['watcher'] = Watcher(current_report['root_dir'], watched_files_dictionary, watched_files_lock) - x['watcher'].run() - - x['handler'] = FileChangeHandler(watched_files_dictionary, watched_files_lock, do_something) - x['handler'].start() - - for k, v in get_report_database().items(): - current_report[k] = v - - select_report_file(list(available_reports.keys()).pop()) - - - @app.route("/app.js") - def appjs(): - return render_template("app.js") - - @socketio.on("ping", namespace="/status") - def ping(): - json = get_json_base(jobfolder=base_dir)[0] - socketio.emit("pong", {'base_json': json}) - - - @app.route("/") - def index(): - rs = get_report_database() - qenc = rs['questions'] - x = {} - for k, v in current_report.items(): - x[k] = v - x['questions'] = {} - - for q in qenc: - items = {} - for it_key, it_value in qenc[q]['tests'].items(): - it_key_js = "-".join(it_key) - # do a quick formatting of the hints. Split into list by breaking at *. - hints = it_value['hints'] - for k in range(len(hints)): - ahints = [] - for h in hints[k][0].split("\n"): - if h.strip().startswith("*"): - ahints.append('') - h = h.strip()[1:] - ahints[-1] += "\n" + h - hints[k] = (ahints, hints[k][1], hints[k][2]) - - items[it_key_js] = {'title': it_value['title'], 'hints': hints} - x['questions'][q] = {'title': qenc[q]['title'], 'tests': items} - - run_cmd_grade = '.'.join(x['modules']) + "_grade" - x['grade_script'] = x['modules'][-1] + "_grade.py" - x['run_cmd_grade'] = f"python -m {run_cmd_grade}" - # x['root_dir'] - return render_template("index3.html", **x) - - @socketio.on("rerun", namespace="/status") - def rerun(data): - """write to the child pty. The pty sees this as if you are typing in a real - terminal. - """ - db = get_report_database() - targs = ".".join( data['test'].split("-") ) - m = '.'.join(db['modules']) - cmd = f"python -m {m} {targs}" - out = subprocess.run(cmd, cwd=db['local_base_dir_for_test_module'], shell=True, check=True, capture_output=True, text=True) - for q in db['questions']: - for i in db['questions'][q]['tests']: - if "-".join(i) == data['test']: - with watched_files_lock: - watched_files_dictionary["*/"+db['questions'][q]['tests'][i]['artifact_file']]['last_recorded_change'] = datetime.datetime.now() - - @socketio.on("pty-input", namespace="/pty") - def pty_input(data): - """write to the child pty. The pty sees this as if you are typing in a real - terminal. - """ - if app.config["fd"]: - logging.debug("received input from browser: %s" % data["input"]) - os.write(app.config["fd"], data["input"].encode()) - - @app.route("/crash") - def navbar(): - assert False - - @app.route('/wz') - def wz(): - return render_template('wz.html') - - @socketio.on("reconnected", namespace="/status") - def client_reconnected(data): - """write to the child pty. The pty sees this as if you are typing in a real - terminal. - """ - print("Client recoonnected...", data) - with watched_files_lock: - for k in watched_files_dictionary: - watched_files_dictionary[k]['last_handled_change'] = None - closeables = [x['watcher'], x['handler']] - return app, socketio, closeables - - -def main(): - parser = argparse.ArgumentParser( - description=( - "A fully functional terminal in your browser. " - "https://github.com/cs01/pyxterm.js" - ), - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - parser.add_argument("-p", "--port", default=5000, help="port to run server on") - parser.add_argument("--host",default="127.0.0.1", help="host to run server on (use 0.0.0.0 to allow access from other hosts)",) - parser.add_argument("--debug", action="store_true", help="debug the server") - parser.add_argument("--version", action="store_true", help="print version and exit") - # parser.add_argument("--command", default="bash", help="Command to run in the terminal") - # parser.add_argument("--cmd-args",default="", help="arguments to pass to command (i.e. --cmd-args='arg1 arg2 --flag')",) - args = parser.parse_args() - if args.version: - print(__version__) - exit(0) - from cs108 import deploy - deploy.main(with_coverage=True) - import subprocess - # subprocess.run("python ", cwd="") - - - from cs108.report_devel import mk_bad - mk_bad() - bdir = os.path.dirname(deploy.__file__) - - app, socketio, closeables = mkapp(base_dir=bdir) - # app.config["cmd"] = [args.command] + shlex.split(args.cmd_args) - green = "\033[92m" - end = "\033[0m" - log_format = green + "pyxtermjs > " + end + "%(levelname)s (%(funcName)s:%(lineno)s) %(message)s" - logging.basicConfig( - format=log_format, - stream=sys.stdout, - level=logging.DEBUG if args.debug else logging.INFO, - ) - logging.info(f"serving on http://{args.host}:{args.port}") - debug = args.debug - debug = False - os.environ["WERKZEUG_DEBUG_PIN"] = "off" - socketio.run(app, debug=debug, port=args.port, host=args.host, allow_unsafe_werkzeug=True ) - for c in closeables: - c.close() - sys.exit() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/vue_flask_stuff/server/app_helpers.py b/vue_flask_stuff/server/app_helpers.py deleted file mode 100644 index d3bcd067f454a057b642b8dd45cce862ccfb0d67..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/app_helpers.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -from queue import Queue -from threading import Lock -from server.watcher import Watcher -import argparse -import datetime -import subprocess -from flask import Flask, render_template -from flask_socketio import SocketIO -import pty -import os -import subprocess -import select -import termios -import struct -import fcntl -import shlex -import logging -import sys -import glob -from pupdb.core import PupDB -from unitgrade_private.hidden_gather_upload import picklestring2dict -from unitgrade_private.hidden_gather_upload import dict2picklestring, picklestring2dict -from pathlib import Path - -def get_available_reports(jobfolder): - bdir = os.path.abspath(jobfolder) - available_reports = {} - if os.path.isdir(bdir): - fls = glob.glob(bdir + "/**/main_config_*.json", recursive=True) - elif os.path.isfile(bdir): - fls = glob.glob(os.path.dirname(bdir) + "/**/main_config_*.json", recursive=True) - else: - raise Exception( - "No report files found in the given directory. Start the dashboard in a folder which contains a report test file.") - - for f in fls: - db = PupDB(f) - - report_py = db.get('relative_path') - lpath_full = Path(os.path.normpath(os.path.dirname(f) + f"/../{os.path.basename(report_py)}")) - # rpath = - base = lpath_full.parts[:-len(Path(report_py).parts)] - - # rs['local_base_dir_for_test_module'] = str(Path(*base)) - root_dir = str(Path(*base)) - token = report_py[:-3] + "_grade.py" - available_reports[f] = {'json': f, - 'relative_path': report_py, - 'root_dir': root_dir, - 'title': db.get('title'), - 'relative_path_token': None if not os.path.isfile(root_dir + "/" + token) else token - } - return available_reports \ No newline at end of file diff --git a/vue_flask_stuff/server/db.pkl b/vue_flask_stuff/server/db.pkl deleted file mode 100644 index d5e7f185526f6f1e94e0a707668bef186d84fad8..0000000000000000000000000000000000000000 Binary files a/vue_flask_stuff/server/db.pkl and /dev/null differ diff --git a/vue_flask_stuff/server/file_change_handler.py b/vue_flask_stuff/server/file_change_handler.py deleted file mode 100644 index cbc3cb6d99586b0e329c40cf2e50db8d485c17f0..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/file_change_handler.py +++ /dev/null @@ -1,67 +0,0 @@ -from threading import Thread -from queue import Queue, Empty -import threading -import datetime -import time - - -class FileChangeHandler(Thread): - def __init__(self, watched_files_dictionary, watched_files_lock, do_something): - super().__init__() - self.watched_files_dictionary = watched_files_dictionary - self.watched_files_lock = watched_files_lock - self.do_something = do_something - self.stoprequest = threading.Event() - - - def run(self): - # As long as we weren't asked to stop, try to take new tasks from the - # queue. The tasks are taken with a blocking 'get', so no CPU - # cycles are wasted while waiting. - # Also, 'get' is given a timeout, so stoprequest is always checked, - # even if there's nothing in the queue. - while not self.stoprequest.is_set(): - ct = datetime.datetime.now() - # try: - - file_to_handle = None - with self.watched_files_lock: - for k, v in self.watched_files_dictionary.items(): - if v['last_handled_change'] is None: - file_to_handle = k - break - else: - # This file has been handled recently. Check last change to the file. - if v['last_recorded_change'] is not None: - from datetime import timedelta - if (v['last_recorded_change'] - v['last_handled_change'] ) > timedelta(seconds=0): - file_to_handle = k - break - - if file_to_handle is not None: - # Handle the changes made to this exact file. - self.do_something(file_to_handle) - - with self.watched_files_lock: - self.watched_files_dictionary[file_to_handle]['last_handled_change'] = datetime.datetime.now() - - time.sleep(min(0.1, (datetime.datetime.now()-ct).seconds ) ) - - - def join(self, timeout=None): - # print("Got a stop") - self.stoprequest.set() - super().join(timeout) - - def close(self): - print("Closing change handler..") - self.join() - print("Closed.") - - # - # q = Queue() - # try: - # e = q.get_nowait() - # except Empty as e: - # print("empty queue. ") - # # menial tasks if required. diff --git a/vue_flask_stuff/server/static/favicon.ico b/vue_flask_stuff/server/static/favicon.ico deleted file mode 100644 index 67f19c1cabac6f3a8e297d22eb118acf00d3813b..0000000000000000000000000000000000000000 Binary files a/vue_flask_stuff/server/static/favicon.ico and /dev/null differ diff --git a/vue_flask_stuff/server/static/sidebars.css b/vue_flask_stuff/server/static/sidebars.css deleted file mode 100644 index 60226dbe38799fe490a5844f932a28ad526038ae..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/static/sidebars.css +++ /dev/null @@ -1,106 +0,0 @@ -body { - min-height: 100vh; - min-height: -webkit-fill-available; - height: 100%; -} -html { - height: -webkit-fill-available; -} -.box { - -} - -main { - display: flex; - display: flex; - flex-wrap: nowrap; -/** height: -webkit-fill-available; **/ - - -min-height: 100vh; -/* - - */ - /* max-height: 100vh; - overflow-y: hidden; - */ - overflow-x: off; -} - -.b-example-divider { - /* flex-shrink: 0; - height: 100%; - - */ - width: calc(100% - 280px); - background-color: rgba(0, 0, 0, .1); - border: solid rgba(0, 0, 0, .15); - border-width: 1px 0; - box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15); - padding-left: 10px; - padding-top: 10px; - padding-right: 10px; - padding-bottom: 10px; -} - - -.bi { - vertical-align: -.125em; - pointer-events: none; - fill: currentColor; -} - -.dropdown-toggle { outline: 0; } - -.nav-flush .nav-link { - border-radius: 0; -} - -.btn-toggle { - display: inline-flex; - align-items: center; - padding: .25rem .5rem; - font-weight: 600; - color: rgba(0, 0, 0, .65); - background-color: transparent; - border: 0; -} -.btn-toggle:hover, -.btn-toggle:focus { - color: rgba(0, 0, 0, .85); - background-color: #d2f4ea; -} - -.btn-toggle::before { - width: 1.25em; - line-height: 0; - content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e"); - transition: transform .35s ease; - transform-origin: .5em 50%; -} - -.btn-toggle[aria-expanded="true"] { - color: rgba(0, 0, 0, .85); -} -.btn-toggle[aria-expanded="true"]::before { - transform: rotate(90deg); -} - -.btn-toggle-nav a { - display: inline-flex; - padding: .1875rem .5rem; - margin-top: .125rem; - margin-left: 1.25rem; - text-decoration: none; -} -.btn-toggle-nav a:hover, -.btn-toggle-nav a:focus { - background-color: #d2f4ea; -} - -.scrollarea { - overflow-y: auto; -} - -.fw-semibold { font-weight: 600; } -.lh-tight { line-height: 1.25; } diff --git a/vue_flask_stuff/server/static/sidebars.js b/vue_flask_stuff/server/static/sidebars.js deleted file mode 100644 index 68384c1633e88970593660b87763b1c937cb12a6..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/static/sidebars.js +++ /dev/null @@ -1,8 +0,0 @@ -/* global bootstrap: false */ -(function () { - 'use strict' - var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) - tooltipTriggerList.forEach(function (tooltipTriggerEl) { - new bootstrap.Tooltip(tooltipTriggerEl) - }) -})() diff --git a/vue_flask_stuff/server/static/unitgrade.css b/vue_flask_stuff/server/static/unitgrade.css deleted file mode 100644 index 36883b27fb0163f49c11888a34e5c0875d1eac99..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/static/unitgrade.css +++ /dev/null @@ -1,43 +0,0 @@ -.test-running{ - background-color: 'red'; - color: rgba(0, 0, .9, 0.75); -} -.icon-green { - color: green; -} - -.icon-red { - color: red; -} - - -.list-unstyled .btn{ - text-align: left;'' - background-color: rgba(0,0,0,0.05); -} -.list-unstyled .btn:hover{ - background-color: rgba(0,0,0,0.25); -} -.list-unstyled a{ - // width: 100%; - // width:100%; - // background-color: rgba(0,0,0,0.05); -} -.traceback{ - font-size: 12px; - line-height: .9em; -} - -.test-unknown{ - color: rgba(0, 0, 0.6, 0.5); -} -.test-success{ - color: rgba(0, 0.6, 0, 0.5); -} -.test-fail{ - color: rgba(0, 0.6, 0.6, 0.5); -} -.test-error{ - color: rgba(0.6, 0, 0, 0.5); -} - diff --git a/vue_flask_stuff/server/static/unitgrade.js b/vue_flask_stuff/server/static/unitgrade.js deleted file mode 100644 index 3c04f44818a12eb53f4faacb6d95488ff354a67c..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/static/unitgrade.js +++ /dev/null @@ -1,208 +0,0 @@ -const socket = io.connect("/status"); // Status of the tests. -socket.on("connect", () => { - $("#status-connected").show(); // className = "badge rounded-pill bg-success" - $("#status-connecting").hide(); // className = "badge rounded-pill bg-success" -}); -socket.on("disconnect", () => { - $("#status-connected").hide(); // className = "badge rounded-pill bg-success" - $("#status-connecting").show(); // className = "badge rounded-pill bg-success" -}); -function re_run_test(test){ - console.log(test); - socket.emit("rerun", {'test': test}); - ui_set_state(test, 'running', {'stacktrace': 'Test is running'}); - terminals[test][0].reset(); - terminals[test][0].writeln('Rerunning test...'); -} -function tests_and_state(){ - /** This function update the token/test results. **/ -} - -function ui_set_token_state(){ - /** React to a change in the .token file state **/ -} - -socket.on("token_update", function(data){ - console.log('token update'); - console.log(data); -}); - -function ui_set_state(test_id, state, data){ - /** Set the state of the test in the UI. Does not fire any events to the server. **/ - state_emojis = {'fail': "bi bi-emoji-frown", - 'pass': "bi bi-emoji-smile", - 'running': 'spinner-border text-primary spinner-border-sm', - } - state_classes = {'fail': 'text-danger', - 'pass': 'text-success', - 'running': 'text-warning', - } - td_classes = {'fail': 'table-danger', - 'pass': 'table-success', - 'running': 'table-warning', - } - - - $("#tbl-"+test_id+"-title").removeClass(); - $("#tbl-"+test_id+"-title").addClass(td_classes[state]); - - $("#tbl-"+test_id+"-unit").removeClass(); - $("#tbl-"+test_id+"-unit").addClass(td_classes[state]); - $("#tbl-"+test_id+"-title").innerHtml(state); - - for(const e of $("." + test_id + "-status")){ - var icon = e.querySelector("#" + test_id + "-icon") - if (icon != null){ - icon.setAttribute("class", state_emojis[state]); - } - var icon = e.querySelector("#" + test_id + "-status") - if (icon != null){ - nc = state_classes[state] - - if(data.coverage_files_changed != null){ - nc = nc + " text-decoration-line-through"; - } - icon.setAttribute("class", nc); - } - } - - if (state == 'pass'){ - $('#'+test_id+'-stacktrace').html('The test was successfull!') - } - if(state == 'fail'){ - $('#'+test_id+'-stacktrace').html(data.stacktrace +" <script> $('.traceback').on('load', function() { console.log('STUFF'); do_call_doc_ready(); } ); </script>").ready( - function(){ //alert('loaded'); - $('.traceback').ready( function() { - console.log('STUFF'); - - setTimeout(function(){ - do_call_doc_ready(test_id) - }, 500); - }); - }); - } -} -// const status = document.getElementById("status"); -/** - socket.of("/admin").on("state", function (data) { - console.log("new output received from server:", data.output); - term.write(data.output); - }); - - socket.on("update", function (data) { - console.log("new output received from server:", data.output); - term.write(data.output); - }); - - socket.on('test_update', function (data){ - console.log('test got some new stuff'); - }); - - function fitToscreen() { - //fit.fit(); - const dims = { cols: term.cols, rows: term.rows }; - console.log("sending new dimensions to server's pty", dims); - socket.emit("resize", dims); - } - **/ - - socket.on("testupdate", function(data){ - console.log('> ', data.state, ': updating test with with id', data.id); - ui_set_state(data.id, data.state, data); - const targetNode = document.getElementById(''+data.id+'-stacktrace'); - const callback = (mutationList, observer) => { - for (const mutation of mutationList) { - } - }; - console.log(data.stdout); - if(data.run_id != terminals[data.id][2]['run_id']){ - terminals[data.id][0].reset(); - terminals[data.id][0].writeln('> Waiting for data...'); - - terminals[data.id][2]['run_id'] = data.run_id; - terminals[data.id][2]['last_chunk_id'] = -1; - } - if(data.stdout != null){ - for (const o of data.stdout){ - if (o[0] > terminals[data.id][2]['last_chunk_id']){ - terminals[data.id][0].write(o[1]); - terminals[data.id][2]['last_chunk_id'] = o[0] - } - } - } - }); - - function debounce(func, wait_ms) { - let timeout; - return function (...args) { - const context = this; - clearTimeout(timeout); - timeout = setTimeout(() => func.apply(context, args), wait_ms); - }; - } - $("#status-connected").hide(); - function reconnect(){ - console.log("hi world"); - socket.emit("reconnected", {'hello': 'world'}); - // <span class="badge rounded-pill bg-success">Success</span> - // $('#status').innerHTML = '<span style="background-color: lightgreen;">connected tp tje server.</span>'; - $("#status-connected").show(); // className = "badge rounded-pill bg-success" - $("#status-connecting").hide(); // className = "badge rounded-pill bg-success" -// $("#status").text('Connected') - // console.log("changed html"); - /** - socket.on("connect", () => { - fitToscreen(); - status.innerHTML = - '<span style="background-color: lightgreen;">connected</span>'; - }); - **/ - } - const wait_ms = 50; - // window.onresize = debounce(fitToscreen, wait_ms); -//reconnect(); -window.onload = debounce(reconnect, wait_ms); - -/** This block of code is responsible for managing the terminals */ -//console.log(terminals); - -for (var key in terminals) { - const term = new Terminal({ - rows: 22, - cursorBlink: true, - macOptionIsMeta: true, - scrollback: true, - disableStdin: true, - convertEol: true, - }); - const fit = new FitAddon.FitAddon(); - term.loadAddon(fit); - - term.open(document.getElementById(key)); - fit.fit(); - term.writeln("Welcome back! Press the blue 'rerun' button above to run the test anew.") - terminals[key] = [term, fit, {'last_run_id': -1, 'last_chunk': 0}]; // Last item are the state details. -} - - function fitToscreen() { - // for(key in terminals){ - // terminals[key][1].fit(); - // } - mpt = $("#main_page_tabs")[0] - // console.log("trying to fit..."); - for(k in terminals){ - e = mpt.querySelector("#"+k + "-pane"); - // console.log("k", k, e.classList) - if ( e.classList.contains("active") ){ - console.log("Fitting the termianl given by ", k) - terminals[k][1].fit(); - } - } - } -window.onresize = debounce(fitToscreen, wait_ms); - -$('button[data-toggle="tab"]').on('shown.bs.tab', function (e) { - for(key in terminals){ - terminals[key][0].write(''); // This appears to refresh the terminal. - } -}); diff --git a/vue_flask_stuff/server/static/wz_js.js b/vue_flask_stuff/server/static/wz_js.js deleted file mode 100644 index c30fcf9644c2630107040c60e3e0e92a53446875..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/static/wz_js.js +++ /dev/null @@ -1,365 +0,0 @@ -function do_call_doc_ready(id){ - - docReady(() => { - if (!EVALEX_TRUSTED) { - initPinBox(); - } - // if we are in console mode, show the console. - if (CONSOLE_MODE && EVALEX) { - createInteractiveConsole(); - } - - const frames = document.querySelectorAll("div."+id +"-traceback div.frame"); - if (EVALEX) { - addConsoleIconToFrames(frames); - } - addEventListenersToElements(document.querySelectorAll("div.detail"), "click", () => - document.querySelector("div."+id+"-traceback").scrollIntoView(false) - ); - addToggleFrameTraceback(frames); - addToggleTraceTypesOnClick(document.querySelectorAll("h2.traceback")); - addInfoPrompt(document.querySelectorAll("span.nojavascript")); - console.log("Document is ready; setting traceback.") - }); -} - -function addToggleFrameTraceback(frames) { - frames.forEach((frame) => { -// console.log("Adding event listener...") - frame.addEventListener("click", () => { -// console.log("Now the element has been clicked. " + frame + " " + frame.getElementsByTagName("pre")[0].parentElement); - frame.getElementsByTagName("pre")[0].parentElement.classList.toggle("expanded"); - }); - }) -} - - -function wrapPlainTraceback() { - const plainTraceback = document.querySelector("div.plain textarea"); - const wrapper = document.createElement("pre"); - const textNode = document.createTextNode(plainTraceback.textContent); - wrapper.appendChild(textNode); - plainTraceback.replaceWith(wrapper); -} - -function initPinBox() { - document.querySelector(".pin-prompt form").addEventListener( - "submit", - function (event) { - event.preventDefault(); - const pin = encodeURIComponent(this.pin.value); - const encodedSecret = encodeURIComponent(SECRET); - const btn = this.btn; - btn.disabled = true; - - fetch( - `${document.location.pathname}?__debugger__=yes&cmd=pinauth&pin=${pin}&s=${encodedSecret}` - ) - .then((res) => res.json()) - .then(({auth, exhausted}) => { - if (auth) { - EVALEX_TRUSTED = true; - fadeOut(document.getElementsByClassName("pin-prompt")[0]); - } else { - alert( - `Error: ${ - exhausted - ? "too many attempts. Restart server to retry." - : "incorrect pin" - }` - ); - } - }) - .catch((err) => { - alert("Error: Could not verify PIN. Network error?"); - console.error(err); - }) - .finally(() => (btn.disabled = false)); - }, - false - ); -} - -function promptForPin() { - if (!EVALEX_TRUSTED) { - const encodedSecret = encodeURIComponent(SECRET); - fetch( - `${document.location.pathname}?__debugger__=yes&cmd=printpin&s=${encodedSecret}` - ); - const pinPrompt = document.getElementsByClassName("pin-prompt")[0]; - fadeIn(pinPrompt); - document.querySelector('.pin-prompt input[name="pin"]').focus(); - } -} - -/** - * Helper function for shell initialization - */ -function openShell(consoleNode, target, frameID) { - promptForPin(); - if (consoleNode) { - slideToggle(consoleNode); - return consoleNode; - } - let historyPos = 0; - const history = [""]; - const consoleElement = createConsole(); - const output = createConsoleOutput(); - const form = createConsoleInputForm(); - const command = createConsoleInput(); - - target.parentNode.appendChild(consoleElement); - consoleElement.append(output); - consoleElement.append(form); - form.append(command); - command.focus(); - slideToggle(consoleElement); - - form.addEventListener("submit", (e) => { - handleConsoleSubmit(e, command, frameID).then((consoleOutput) => { - output.append(consoleOutput); - command.focus(); - consoleElement.scrollTo(0, consoleElement.scrollHeight); - const old = history.pop(); - history.push(command.value); - if (typeof old !== "undefined") { - history.push(old); - } - historyPos = history.length - 1; - command.value = ""; - }); - }); - - command.addEventListener("keydown", (e) => { - if (e.key === "l" && e.ctrlKey) { - output.innerText = "--- screen cleared ---"; - } else if (e.key === "ArrowUp" || e.key === "ArrowDown") { - // Handle up arrow and down arrow. - if (e.key === "ArrowUp" && historyPos > 0) { - e.preventDefault(); - historyPos--; - } else if (e.key === "ArrowDown" && historyPos < history.length - 1) { - historyPos++; - } - command.value = history[historyPos]; - } - return false; - }); - - return consoleElement; -} - -function addEventListenersToElements(elements, event, listener) { - elements.forEach((el) => el.addEventListener(event, listener)); -} - -/** - * Add extra info - */ -function addInfoPrompt(elements) { - for (let i = 0; i < elements.length; i++) { - elements[i].innerHTML = - "<p>To switch between the interactive traceback and the plaintext " + - 'one, you can click on the "Traceback" headline. From the text ' + - "traceback you can also create a paste of it. " + - (!EVALEX - ? "" - : "For code execution mouse-over the frame you want to debug and " + - "click on the console icon on the right side." + - "<p>You can execute arbitrary Python code in the stack frames and " + - "there are some extra helpers available for introspection:" + - "<ul><li><code>dump()</code> shows all variables in the frame" + - "<li><code>dump(obj)</code> dumps all that's known about the object</ul>"); - elements[i].classList.remove("nojavascript"); - } -} - -function addConsoleIconToFrames(frames) { - for (let i = 0; i < frames.length; i++) { - let consoleNode = null; - const target = frames[i]; - const frameID = frames[i].id.substring(6); - - for (let j = 0; j < target.getElementsByTagName("pre").length; j++) { - const img = createIconForConsole(); - img.addEventListener("click", (e) => { - e.stopPropagation(); - consoleNode = openShell(consoleNode, target, frameID); - return false; - }); - target.getElementsByTagName("pre")[j].append(img); - } - } -} - -function slideToggle(target) { - target.classList.toggle("active"); -} - -/** - * toggle traceback types on click. - */ -function addToggleTraceTypesOnClick(elements) { - // logger.log("something..") - for (let i = 0; i < elements.length; i++) { - elements[i].addEventListener("click", () => { - document.querySelector("div.traceback").classList.toggle("hidden"); - document.querySelector("div.plain").classList.toggle("hidden"); - }); - elements[i].style.cursor = "pointer"; - document.querySelector("div.plain").classList.toggle("hidden"); - } -} - -function createConsole() { - const consoleNode = document.createElement("pre"); - consoleNode.classList.add("console"); - consoleNode.classList.add("active"); - return consoleNode; -} - -function createConsoleOutput() { - const output = document.createElement("div"); - output.classList.add("output"); - output.innerHTML = "[console ready]"; - return output; -} - -function createConsoleInputForm() { - const form = document.createElement("form"); - form.innerHTML = ">>> "; - return form; -} - -function createConsoleInput() { - const command = document.createElement("input"); - command.type = "text"; - command.setAttribute("autocomplete", "off"); - command.setAttribute("spellcheck", false); - command.setAttribute("autocapitalize", "off"); - command.setAttribute("autocorrect", "off"); - return command; -} - -function createIconForConsole() { - const img = document.createElement("img"); - img.setAttribute("src", "?__debugger__=yes&cmd=resource&f=console.png"); - img.setAttribute("title", "Open an interactive python shell in this frame"); - return img; -} - -function createExpansionButtonForConsole() { - const expansionButton = document.createElement("a"); - expansionButton.setAttribute("href", "#"); - expansionButton.setAttribute("class", "toggle"); - expansionButton.innerHTML = " "; - return expansionButton; -} - -function createInteractiveConsole() { - const target = document.querySelector("div.console div.inner"); - while (target.firstChild) { - target.removeChild(target.firstChild); - } - openShell(null, target, 0); -} - -function handleConsoleSubmit(e, command, frameID) { - // Prevent page from refreshing. - e.preventDefault(); - - return new Promise((resolve) => { - // Get input command. - const cmd = command.value; - - // Setup GET request. - const urlPath = ""; - const params = { - __debugger__: "yes", - cmd: cmd, - frm: frameID, - s: SECRET, - }; - const paramString = Object.keys(params) - .map((key) => { - return "&" + encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); - }) - .join(""); - - fetch(urlPath + "?" + paramString) - .then((res) => { - return res.text(); - }) - .then((data) => { - const tmp = document.createElement("div"); - tmp.innerHTML = data; - resolve(tmp); - - // Handle expandable span for long list outputs. - // Example to test: list(range(13)) - let wrapperAdded = false; - const wrapperSpan = document.createElement("span"); - const expansionButton = createExpansionButtonForConsole(); - - tmp.querySelectorAll("span.extended").forEach((spanToWrap) => { - const parentDiv = spanToWrap.parentNode; - if (!wrapperAdded) { - parentDiv.insertBefore(wrapperSpan, spanToWrap); - wrapperAdded = true; - } - parentDiv.removeChild(spanToWrap); - wrapperSpan.append(spanToWrap); - spanToWrap.hidden = true; - - expansionButton.addEventListener("click", () => { - spanToWrap.hidden = !spanToWrap.hidden; - expansionButton.classList.toggle("open"); - return false; - }); - }); - - // Add expansion button at end of wrapper. - if (wrapperAdded) { - wrapperSpan.append(expansionButton); - } - }) - .catch((err) => { - console.error(err); - }); - return false; - }); -} - -function fadeOut(element) { - element.style.opacity = 1; - - (function fade() { - element.style.opacity -= 0.1; - if (element.style.opacity < 0) { - element.style.display = "none"; - } else { - requestAnimationFrame(fade); - } - })(); -} - -function fadeIn(element, display) { - element.style.opacity = 0; - element.style.display = display || "block"; - - (function fade() { - let val = parseFloat(element.style.opacity) + 0.1; - if (val <= 1) { - element.style.opacity = val; - requestAnimationFrame(fade); - } - })(); -} - -function docReady(fn) { - if (document.readyState === "complete" || document.readyState === "interactive") { - setTimeout(fn, 1); - } else { - document.addEventListener("DOMContentLoaded", fn); - } -} diff --git a/vue_flask_stuff/server/static/wz_style.css b/vue_flask_stuff/server/static/wz_style.css deleted file mode 100644 index e9397ca0a1b6c26f30cb28fc81510a48fc46ede9..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/static/wz_style.css +++ /dev/null @@ -1,150 +0,0 @@ -body, input { font-family: sans-serif; color: #000; text-align: center; - margin: 1em; padding: 0; font-size: 15px; } -h1, h2, h3 { font-weight: normal; } - -input { background-color: #fff; margin: 0; text-align: left; - outline: none !important; } -input[type="submit"] { padding: 3px 6px; } -a { color: #11557C; } -a:hover { color: #177199; } -pre, code, -textarea { font-family: monospace; font-size: 14px; } - -div.debugger { text-align: left; padding: 12px; margin: auto; - background-color: white; } -h1 { font-size: 36px; margin: 0 0 0.3em 0; } -div.detail { cursor: pointer; } -div.detail p { margin: 0 0 8px 13px; font-size: 14px; white-space: pre-wrap; - font-family: monospace; } -div.explanation { margin: 20px 13px; font-size: 15px; color: #555; } -div.footer { font-size: 13px; text-align: right; margin: 30px 0; - color: #86989B; } - -h2 { font-size: 16px; margin: 1.3em 0 0.0 0; padding: 9px; - background-color: #11557C; color: white; } -h2 em, h3 em { font-style: normal; color: #A5D6D9; font-weight: normal; } - -div.traceback, div.plain { border: 1px solid #ddd; margin: 0 0 1em 0; padding: 10px; } -div.plain p { margin: 0; } -div.plain textarea, -div.plain pre { margin: 10px 0 0 0; padding: 4px; - background-color: #E8EFF0; border: 1px solid #D3E7E9; } -div.plain textarea { width: 99%; height: 300px; } -div.traceback h3 { font-size: 1em; margin: 0 0 0.8em 0; } -div.traceback ul { list-style: none; margin: 0; padding: 0 0 0 1em; } -div.traceback h4 { font-size: 13px; font-weight: normal; margin: 0.7em 0 0.1em 0; } -div.traceback pre { margin: 0; padding: 5px 0 3px 15px; - background-color: #E8EFF0; border: 1px solid #D3E7E9; } -div.traceback .library .current { background: white; color: #555; } -div.traceback .expanded .current { background: #E8EFF0; color: black; } -div.traceback pre:hover { background-color: #DDECEE; color: black; cursor: pointer; } -div.traceback div.source.expanded pre + pre { border-top: none; } - -div.traceback span.ws { display: none; } -div.traceback pre.before, div.traceback pre.after { display: none; background: white; } -div.traceback div.source.expanded pre.before, -div.traceback div.source.expanded pre.after { - display: block; -} - -div.traceback div.source.expanded span.ws { - display: inline; -} - -div.traceback blockquote { margin: 1em 0 0 0; padding: 0; white-space: pre-line; } -div.traceback img { float: right; padding: 2px; margin: -3px 2px 0 0; display: none; } -div.traceback img:hover { background-color: #ddd; cursor: pointer; - border-color: #BFDDE0; } -div.traceback pre:hover img { display: block; } -div.traceback cite.filename { font-style: normal; color: #3B666B; } - -pre.console { border: 1px solid #ccc; background: white!important; - color: black; padding: 5px!important; - margin: 3px 0 0 0!important; cursor: default!important; - max-height: 400px; overflow: auto; } -pre.console form { color: #555; } -pre.console input { background-color: transparent; color: #555; - width: 90%; font-family: monospace; font-size: 14px; - border: none!important; } - -span.string { color: #30799B; } -span.number { color: #9C1A1C; } -span.help { color: #3A7734; } -span.object { color: #485F6E; } -span.extended { opacity: 0.5; } -span.extended:hover { opacity: 1; } -a.toggle { text-decoration: none; background-repeat: no-repeat; - background-position: center center; - background-image: url(?__debugger__=yes&cmd=resource&f=more.png); } -a.toggle:hover { background-color: #444; } -a.open { background-image: url(?__debugger__=yes&cmd=resource&f=less.png); } - -pre.console div.traceback, -pre.console div.box { margin: 5px 10px; white-space: normal; - border: 1px solid #11557C; padding: 10px; - font-family: sans-serif; } -pre.console div.box h3, -pre.console div.traceback h3 { margin: -10px -10px 10px -10px; padding: 5px; - background: #11557C; color: white; } - -pre.console div.traceback pre:hover { cursor: default; background: #E8EFF0; } -pre.console div.traceback pre.syntaxerror { background: inherit; border: none; - margin: 20px -10px -10px -10px; - padding: 10px; border-top: 1px solid #BFDDE0; - background: #E8EFF0; } -pre.console div.noframe-traceback pre.syntaxerror { margin-top: -10px; border: none; } - -pre.console div.box pre.repr { padding: 0; margin: 0; background-color: white; border: none; } -pre.console div.box table { margin-top: 6px; } -pre.console div.box pre { border: none; } -pre.console div.box pre.help { background-color: white; } -pre.console div.box pre.help:hover { cursor: default; } -pre.console table tr { vertical-align: top; } -div.console { border: 1px solid #ccc; padding: 4px; background-color: #fafafa; } - -div.traceback pre, div.console pre { - white-space: pre-wrap; /* css-3 should we be so lucky... */ - white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ - white-space: -pre-wrap; /* Opera 4-6 ?? */ - white-space: -o-pre-wrap; /* Opera 7 ?? */ - word-wrap: break-word; /* Internet Explorer 5.5+ */ - _white-space: pre; /* IE only hack to re-specify in - addition to word-wrap */ -} - - -div.pin-prompt { - position: absolute; - display: none; - top: 0; - bottom: 0; - left: 0; - right: 0; - background: rgba(255, 255, 255, 0.8); -} - -div.pin-prompt .inner { - background: #eee; - padding: 10px 50px; - width: 350px; - margin: 10% auto 0 auto; - border: 1px solid #ccc; - border-radius: 2px; -} - -div.exc-divider { - margin: 0.7em 0 0 -1em; - padding: 0.5em; - background: #11557C; - color: #ddd; - border: 1px solid #ddd; -} - -.console.active { - max-height: 0!important; - display: none; -} - -.hidden { - display: none; -} diff --git a/vue_flask_stuff/server/static/wz_style_modified.css b/vue_flask_stuff/server/static/wz_style_modified.css deleted file mode 100644 index 3518ad84384c4d154ef1afe386e49642110c9c2f..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/static/wz_style_modified.css +++ /dev/null @@ -1,152 +0,0 @@ -/** -body, input { font-family: sans-serif; color: #000; text-align: center; - margin: 1em; padding: 0; font-size: 15px; } - **/ -h1, h2, h3 { font-weight: normal; } - -input { background-color: #fff; margin: 0; text-align: left; - outline: none !important; } -input[type="submit"] { padding: 3px 6px; } -a { color: #11557C; } -a:hover { color: #177199; } -pre, code, -textarea { font-family: monospace; font-size: 14px; } - -div.debugger { text-align: left; padding: 12px; margin: auto; - background-color: white; } -h1 { font-size: 36px; margin: 0 0 0.3em 0; } -div.detail { cursor: pointer; } -div.detail p { margin: 0 0 8px 13px; font-size: 14px; white-space: pre-wrap; - font-family: monospace; } -div.explanation { margin: 20px 13px; font-size: 15px; color: #555; } -div.footer { font-size: 13px; text-align: right; margin: 30px 0; - color: #86989B; } - -h2 { font-size: 16px; margin: 1.3em 0 0.0 0; padding: 9px; - background-color: #11557C; color: white; } -h2 em, h3 em { font-style: normal; color: #A5D6D9; font-weight: normal; } - -div.traceback, div.plain { border: 1px solid #ddd; margin: 0 0 1em 0; padding: 10px; } -div.plain p { margin: 0; } -div.plain textarea, -div.plain pre { margin: 10px 0 0 0; padding: 4px; - background-color: #E8EFF0; border: 1px solid #D3E7E9; } -div.plain textarea { width: 99%; height: 300px; } -div.traceback h3 { font-size: 1em; margin: 0 0 0.8em 0; } -div.traceback ul { list-style: none; margin: 0; padding: 0 0 0 1em; } -div.traceback h4 { font-size: 13px; font-weight: normal; margin: 0.7em 0 0.1em 0; } -div.traceback pre { margin: 0; padding: 5px 0 3px 15px; - background-color: #E8EFF0; border: 1px solid #D3E7E9; } -div.traceback .library .current { background: white; color: #555; } -div.traceback .expanded .current { background: #E8EFF0; color: black; } -div.traceback pre:hover { background-color: #DDECEE; color: black; cursor: pointer; } -div.traceback div.source.expanded pre + pre { border-top: none; } - -div.traceback span.ws { display: none; } -div.traceback pre.before, div.traceback pre.after { display: none; background: white; } -div.traceback div.source.expanded pre.before, -div.traceback div.source.expanded pre.after { - display: block; -} - -div.traceback div.source.expanded span.ws { - display: inline; -} - -div.traceback blockquote { margin: 1em 0 0 0; padding: 0; white-space: pre-line; } -div.traceback img { float: right; padding: 2px; margin: -3px 2px 0 0; display: none; } -div.traceback img:hover { background-color: #ddd; cursor: pointer; - border-color: #BFDDE0; } -div.traceback pre:hover img { display: block; } -div.traceback cite.filename { font-style: normal; color: #3B666B; } - -pre.console { border: 1px solid #ccc; background: white!important; - color: black; padding: 5px!important; - margin: 3px 0 0 0!important; cursor: default!important; - max-height: 400px; overflow: auto; } -pre.console form { color: #555; } -pre.console input { background-color: transparent; color: #555; - width: 90%; font-family: monospace; font-size: 14px; - border: none!important; } - -span.string { color: #30799B; } -span.number { color: #9C1A1C; } -span.help { color: #3A7734; } -span.object { color: #485F6E; } -span.extended { opacity: 0.5; } -span.extended:hover { opacity: 1; } -a.toggle { text-decoration: none; background-repeat: no-repeat; - background-position: center center; - background-image: url(?__debugger__=yes&cmd=resource&f=more.png); } -a.toggle:hover { background-color: #444; } -a.open { background-image: url(?__debugger__=yes&cmd=resource&f=less.png); } - -pre.console div.traceback, -pre.console div.box { margin: 5px 10px; white-space: normal; - border: 1px solid #11557C; padding: 10px; - font-family: sans-serif; } -pre.console div.box h3, -pre.console div.traceback h3 { margin: -10px -10px 10px -10px; padding: 5px; - background: #11557C; color: white; } - -pre.console div.traceback pre:hover { cursor: default; background: #E8EFF0; } -pre.console div.traceback pre.syntaxerror { background: inherit; border: none; - margin: 20px -10px -10px -10px; - padding: 10px; border-top: 1px solid #BFDDE0; - background: #E8EFF0; } -pre.console div.noframe-traceback pre.syntaxerror { margin-top: -10px; border: none; } - -pre.console div.box pre.repr { padding: 0; margin: 0; background-color: white; border: none; } -pre.console div.box table { margin-top: 6px; } -pre.console div.box pre { border: none; } -pre.console div.box pre.help { background-color: white; } -pre.console div.box pre.help:hover { cursor: default; } -pre.console table tr { vertical-align: top; } -div.console { border: 1px solid #ccc; padding: 4px; background-color: #fafafa; } - -div.traceback pre, div.console pre { - white-space: pre-wrap; /* css-3 should we be so lucky... */ - white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ - white-space: -pre-wrap; /* Opera 4-6 ?? */ - white-space: -o-pre-wrap; /* Opera 7 ?? */ - word-wrap: break-word; /* Internet Explorer 5.5+ */ - _white-space: pre; /* IE only hack to re-specify in - addition to word-wrap */ -} - - -div.pin-prompt { - position: absolute; - display: none; - top: 0; - bottom: 0; - left: 0; - right: 0; - background: rgba(255, 255, 255, 0.8); -} - -div.pin-prompt .inner { - background: #eee; - padding: 10px 50px; - width: 350px; - margin: 10% auto 0 auto; - border: 1px solid #ccc; - border-radius: 2px; -} - -div.exc-divider { - margin: 0.7em 0 0 -1em; - padding: 0.5em; - background: #11557C; - color: #ddd; - border: 1px solid #ddd; -} - -.console.active { - max-height: 0!important; - display: none; -} - -.hidden { - display: none; -} diff --git a/vue_flask_stuff/server/templates/app.js b/vue_flask_stuff/server/templates/app.js deleted file mode 100644 index c8ad07c54cdf0e426bd92e84ccbc5afc8ab86db4..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/templates/app.js +++ /dev/null @@ -1,43 +0,0 @@ -const app = Vue.createApp({ - data() { - return { - firstName: 'John', - lastName: 'Doe', - email: 'john@gmail.com', - gender: 'male', - picture: 'https://randomuser.me/api/portraits/men/10.jpg', - } - }, - methods: { - async getUser() { - const res = await fetch('https://randomuser.me/api') - const { results } = await res.json() - - // console.log(results) - // global scope. - - this.firstName = results[0].name.first - this.lastName = results[0].name.last - this.email = results[0].email - this.gender = results[0].gender - this.picture = results[0].picture.large - }, - }, - compilerOptions: { - delimiters: ["[[", "]]"] - }, -}) - -app.mount('#app') - -div id="app"> - <img v-bind:src="picture" - :alt="`${firstName} ${lastName}`" - :class="gender"/> - <h1>[[firstName]] [[lastName]]</h1> - <h3>Email: [[email]]</h3> - <button :class="gender" @click="getUser()">Get Random User</button> - [[ lastName ]] -</div> -<script src="https://unpkg.com/vue@next"></script> -<script src="/app.js"></script> \ No newline at end of file diff --git a/vue_flask_stuff/server/templates/base.html b/vue_flask_stuff/server/templates/base.html deleted file mode 100644 index 208709d6a61bcbadacfc33b309ddbb82d0c5e174..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/templates/base.html +++ /dev/null @@ -1,35 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <title>Unitgrade Dashboard</title> - - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous"> - <script src="https://unpkg.com/xterm@4.11.0/lib/xterm.js"></script> - <script src="https://unpkg.com/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script> - <script src="https://unpkg.com/xterm-addon-web-links@0.4.0/lib/xterm-addon-web-links.js"></script> - <script src="https://unpkg.com/xterm-addon-search@0.8.0/lib/xterm-addon-search.js"></script> - <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script> - <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script> - <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.3.1.min.js"></script> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css"> - </head> - - - <body> - {% block head %} {% endblock %} - - - -{% block content %} - - -{% endblock %} - - - - - </body> -</html> - diff --git a/vue_flask_stuff/server/templates/bootstrap.html b/vue_flask_stuff/server/templates/bootstrap.html deleted file mode 100644 index 566a1332a78a39259480e07b387a048b127fa638..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/templates/bootstrap.html +++ /dev/null @@ -1,70 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <title>Bootstrap demo</title> - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous"> - </head> - <body> - <h1>Hello, world!</h1> - <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script> - - - <div class="accordion" id="accordionPanelsStayOpenExample"> - <div class="accordion-item"> - <h2 class="accordion-header" id="panelsStayOpen-headingOne"> - <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#panelsStayOpen-collapseOne" aria-expanded="true" aria-controls="panelsStayOpen-collapseOne"> - Accordion Item #1 <span class="glyphicon glyphicon-star" aria-hidden="true"> das sdafsdf</span> Star - </button> - </h2> - <div id="panelsStayOpen-collapseOne" class="accordion-collapse collapse show" aria-labelledby="panelsStayOpen-headingOne"> - <div class="accordion-body"> - <strong>This is the first item's accordion body.</strong> It is shown by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow. - <div class="accordion" id="accordionPanelsStayOpenExample2"> - <div class="accordion-item"> - <h2 class="accordion-header" id="panelsStayOpen-headingOne2"> - <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#panelsStayOpen-collapseOne2" aria-expanded="true" aria-controls="panelsStayOpen-collapseOne"> - Accordion Item #1 - </button> - </h2> - <div id="panelsStayOpen-collapseOne2" class="accordion-collapse collapse show" aria-labelledby="panelsStayOpen-headingOne"> - <div class="accordion-body"> - <strong>This is the first item's accordion body.</strong> It is shown by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow. - </div> - </div> - </div> - </div> - - </div> - </div> - </div> - <div class="accordion-item"> - <h2 class="accordion-header" id="panelsStayOpen-headingTwo"> - <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#panelsStayOpen-collapseTwo" aria-expanded="false" aria-controls="panelsStayOpen-collapseTwo"> - Accordion Item #2 - </button> - </h2> - <div id="panelsStayOpen-collapseTwo" class="accordion-collapse collapse" aria-labelledby="panelsStayOpen-headingTwo"> - <div class="accordion-body"> - <strong>This is the second item's accordion body.</strong> It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow. - </div> - </div> - </div> - <div class="accordion-item"> - <h2 class="accordion-header" id="panelsStayOpen-headingThree"> - <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#panelsStayOpen-collapseThree" aria-expanded="false" aria-controls="panelsStayOpen-collapseThree"> - Accordion Item #3 - </button> - </h2> - <div id="panelsStayOpen-collapseThree" class="accordion-collapse collapse" aria-labelledby="panelsStayOpen-headingThree"> - <div class="accordion-body"> - <strong>This is the third item's accordion body.</strong> It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow. - </div> - </div> - </div> -</div> - - - </body> -</html> \ No newline at end of file diff --git a/vue_flask_stuff/server/templates/index.html b/vue_flask_stuff/server/templates/index.html deleted file mode 100644 index d3df6c74180688d0b61fedaad232164a5c973ace..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/templates/index.html +++ /dev/null @@ -1,141 +0,0 @@ -{% extends 'base.html' %} - - -{% macro build_question_body(hi) %} -{{hi}} -{% endmacro %} - -{% block head %} -<script language="javascript"> -const socket = io.connect("/status"); // Status of the tests. - -// const status = document.getElementById("status"); -/** - socket.of("/admin").on("state", function (data) { - console.log("new output received from server:", data.output); - term.write(data.output); - }); - - socket.on("update", function (data) { - console.log("new output received from server:", data.output); - term.write(data.output); - }); - - socket.on('test_update', function (data){ - console.log('test got some new stuff'); - }); - - socket.on("connect", () => { - fitToscreen(); - status.innerHTML = - '<span style="background-color: lightgreen;">connected</span>'; - }); - - socket.on("disconnect", () => { - status.innerHTML = - '<span style="background-color: #ff8383;">disconnected</span>'; - }); - - function fitToscreen() { - //fit.fit(); - const dims = { cols: term.cols, rows: term.rows }; - console.log("sending new dimensions to server's pty", dims); - socket.emit("resize", dims); - } - **/ - function debounce(func, wait_ms) { - let timeout; - return function (...args) { - const context = this; - clearTimeout(timeout); - timeout = setTimeout(() => func.apply(context, args), wait_ms); - }; - } - - function reconnect(){ - console.log("hi world"); - socket.emit("reconnected", {'hello': 'world'}); - $('#status').innerHTML = '<span style="background-color: lightgreen;">connected tp tje server.</span>'; - $("#status").css("background-color", "lightgreen"); - $("#status").text('Connected') - console.log("changed html"); - /** - socket.on("connect", () => { - fitToscreen(); - status.innerHTML = - '<span style="background-color: lightgreen;">connected</span>'; - }); - **/ - } - const wait_ms = 50; - // window.onresize = debounce(fitToscreen, wait_ms); -//reconnect(); -window.onload = debounce(reconnect, wait_ms); - -</script> -{% endblock %} - - - -{% block content %} -<nav class="navbar navbar-dark bg-dark"> - <!-- -<a class="navbar-brand" href="#">Navbar</a>--> -<span class="navbar-brand mb-0 h3">Unitgrade <i class="bi bi-emoji-heart-eyes-fill"></i></span> - <form class="form-inline"> - <div class="input-group"> - <div class="input-group-prepend"> - <span class="input-group-text" id="basic-addon1">@</span> - </div> - <input type="text" class="form-control" placeholder="Username" aria-label="Username" aria-describedby="basic-addon1" value="{{jobdir}}"> - </div> - </form> -<span class="navbar-brand mb-0 h2" id="status">Status</span> - -<h3> stuff here.</h3> - {% block title %}<h3>Test results</h3>{% endblock %} - <p>{{jobdir}}</p> - - <!-- Navbar content --> -</nav> - - -<div id="status"></div> - -<div class="accordion" id="questions"> - {% for qkey, qbody in questions.items() %} - <div class="accordion-item"> - <h2 class="accordion-header" id="{{qkey}}-header"> - <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#{{qkey}}-collapse" aria-expanded="true" aria-controls="{{qkey}}-collapse"> - {{qbody.title}}: Accordion Item #1 <span class="glyphicon glyphicon-star" aria-hidden="true"> das sdafsdf</span> Star - </button> - </h2> - </div> - <div id="{{qkey}}-collapse" class="accordion-collapse collapse show" aria-labelledby="{{qkey}}-header"> - <div class="accordion-body"> - <!-- Begin question body --> - <strong>This is the first item's accordion body.</strong> It is shown by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow. - <div class="accordion" id="{{qkey}}-tests"> - {% for ikey, ibody in qbody.tests.items() %} - <div class="accordion-item"> - <h2 class="accordion-header" id="{{ikey}}-header"> - <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#{{ikey}}-collapse" aria-expanded="true" aria-controls="{{ikey}}-collapse"> - {{ ibody.title }}: Accordion Item #1 <span class="glyphicon glyphicon-star" aria-hidden="true"> das sdafsdf</span> Star - </button> - </h2> - </div> - <div id="{{ikey}}-collapse" class="accordion-collapse collapse show" aria-labelledby="{{ikey}}-header"> - <div class="accordion-body"> - <!-- Begin item body--> - <strong>This is the first item's accordion body.</strong> It is shown by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow. - <!-- End item body --> - </div> - </div> - {% endfor %} - </div> - <!-- End question body --> - </div> - </div> -{% endfor %} -</div> -{% endblock %} diff --git a/vue_flask_stuff/server/templates/index2.html b/vue_flask_stuff/server/templates/index2.html deleted file mode 100644 index 94946cfde5e73c35616277107948e5ce46af4df5..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/templates/index2.html +++ /dev/null @@ -1,121 +0,0 @@ -<html lang="en"> - <head> - <meta charset="utf-8" /> - <title>pyxterm.js</title> - <style> - html { - font-family: arial; - } - </style> - <link - rel="stylesheet" - href="https://unpkg.com/xterm@4.11.0/css/xterm.css" - /> - </head> - <body> - <span style="font-size: 1.4em">pyxterm.js</span> - <span style="font-size: small" - >status: - <span style="font-size: small" id="status">connecting...</span></span - > - - <div style="width: 100%; height: calc(100% - 50px)" id="terminal"></div> - <div style="width: 50%; height: calc(20%)" id="term2">another terminal</div> - <p style="text-align: right; font-size: small"> - built by <a href="https://chadsmith.dev">Chad Smith</a> - <a href="https://github.com/cs01">GitHub</a> - </p> - <!-- xterm --> - <script src="https://unpkg.com/xterm@4.11.0/lib/xterm.js"></script> - <script src="https://unpkg.com/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script> - <script src="https://unpkg.com/xterm-addon-web-links@0.4.0/lib/xterm-addon-web-links.js"></script> - <script src="https://unpkg.com/xterm-addon-search@0.8.0/lib/xterm-addon-sear -ch.js"></script> - <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script> - - <script> - const term = new Terminal({ - cursorBlink: true, - macOptionIsMeta: true, - scrollback: 300, - }); - // https://github.com/xtermjs/xterm.js/issues/2941 - const fit = new FitAddon.FitAddon(); - term.loadAddon(fit); - term.loadAddon(new WebLinksAddon.WebLinksAddon()); - term.loadAddon(new SearchAddon.SearchAddon()); - - term.open(document.getElementById("terminal")); - fit.fit(); - term.resize(15, 50); - console.log(`size: ${term.cols} columns, ${term.rows} rows`); - fit.fit(); - term.writeln("Welcome to pyxterm.js!"); - term.writeln("https://github.com/cs01/pyxterm.js"); - term.onData((data) => { - console.log("key pressed in browser:", data); - socket.emit("pty-input", { input: data }); - }); - - const socket = io.connect("/pty"); - const status = document.getElementById("status"); - - socket.on("pty-output", function (data) { - console.log("new output received from server:", data.output); - term.write(data.output); - }); - - socket.on("connect", () => { - fitToscreen(); - status.innerHTML = - '<span style="background-color: lightgreen;">connected</span>'; - }); - - socket.on("disconnect", () => { - status.innerHTML = - '<span style="background-color: #ff8383;">disconnected</span>'; - }); - - function fitToscreen() { - fit.fit(); - const dims = { cols: term.cols, rows: term.rows }; - console.log("sending new dimensions to server's pty", dims); - socket.emit("resize", dims); - } - - function debounce(func, wait_ms) { - let timeout; - return function (...args) { - const context = this; - clearTimeout(timeout); - timeout = setTimeout(() => func.apply(context, args), wait_ms); - }; - } - - const wait_ms = 50; - window.onresize = debounce(fitToscreen, wait_ms); - </script> - <script> - const term2 = new Terminal({ - cursorBlink: true, - macOptionIsMeta: true, - scrollback: true, - }); - // https://github.com/xtermjs/xterm.js/issues/2941 - const fit2 = new FitAddon.FitAddon(); - term2.loadAddon(fit2); - term2.loadAddon(new WebLinksAddon.WebLinksAddon()); - term2.loadAddon(new SearchAddon.SearchAddon()); - - term2.open(document.getElementById("term2")); - fit2.fit(); - term2.resize(15, 50); - console.log(`size: ${term.cols} columns, ${term.rows} rows`); - fit2.fit(); - term2.writeln("Welcome to pyxterm.js!"); - term2.writeln("https://github.com/cs01/pyxterm.js"); - - </script> - - </body> -</html> \ No newline at end of file diff --git a/vue_flask_stuff/server/templates/index3.html b/vue_flask_stuff/server/templates/index3.html deleted file mode 100644 index e772db992e14b9841dde5abdb5785ba194765e2b..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/templates/index3.html +++ /dev/null @@ -1,182 +0,0 @@ -{% extends 'sidebar.html' %} -{% macro build_question_body(hi) %} -{{hi}} -{% endmacro %} -{% block head %} -{% endblock %} - -{% block content %} -<div class="tab-content" id="main_page_tabs"> -<script> -var terminals = {}; - </script> -{% set count=0 %} -{% for qkey, qbody in questions.items() %} - {% set outer_loop = loop %} - {% for ikey, ibody in qbody.tests.items() %} - <script>terminals["{{ikey}}"] = null; </script> -<div class="tab-pane fade {{ 'show active' if outer_loop.index == 1 and loop.index == 1 else ''}}" id="{{ikey}}-pane" role="tabpanel" aria-labelledby="{{ikey}}-pane-tab"> -<!-- begin tab card --> -<h1>{{qbody.title}}</h1> -<h4> - <span class="{{ikey}}-status"> - <span id="{{ikey}}-status"><i id="{{ikey}}-icon" class="bi bi-emoji-neutral"></i><span class="text-left">{{ibody.title}}</span></span> - </span> - <a onclick="re_run_test('{{ikey}}');" type="button" class="btn btn-primary">Rerun</a> -</h4> - -<div class="card shadow mb-3 bg-white rounded"> - <div class="card-header"> - Terminal Output - </div> - <div class="card-body"> - <p class="card-text"> - <div style="width: 100%; height: 20%" id="{{ikey}}"></div> - </p> - </div> -</div> - -<div class="row"> - <div class="col-sm-8"> - <div class="card shadow mb-5 bg-white rounded"> - <div class="card-header">Test outcome</div> - <div class="card-body"> - <div id="{{ikey}}-stacktrace">{{ibody.wz}}</div> - <!-- <a href="#" class="btn btn-primary">Go somewhere</a> --> - </div> - </div> - </div> - <div class="col-sm-4"> - <div class="card shadow mb-5 bg-white rounded"> - <div class="card-header"> Hints </div> - <div class="card-body"> - <dl> - {% for h in ibody.hints %} - <dt>{% if not h[1] %} Overall hints: {% else %} From the file <emph>{{ h[1] }}</emph> {% endif %}</dt> - <dd> - <ul> - {% for hitem in h[0] %} - <li>{{hitem}}</li> - {% endfor %} - </ul> - </dd> - {% endfor %} -</dl> - <!-- - <h5 class="card-title">Special title treatment</h5> - <p class="card-text">With supporting text below as a natural lead-in to additional content.</p> - <a href="#" class="btn btn-primary">Go somewhere</a> - --> - </div> - </div> - </div> -</div> -</div> - {% endfor %} - {% endfor %} -<div class="tab-pane fade" id="token-pane" role="tabpanel" aria-labelledby="token-pane-tab"> - -<div class="row"> - <div class="col-sm-2"> </div> - <div class="col-sm-8"> - <div class="card shadow mb-5 bg-white rounded"> - <div class="card-header">Your submission</div> - <div class="card-body"> - {% for qkey, qbody in questions.items() %} - <h6> {{qbody.title}}</h6> - <table class="table table-hover" style="td-height: 10px;"> - <thead> - <tr> - <td>Test</td> - <td>Unittests result</td> - <td><code>.token</code>-file result</td> - </tr> - </thead> - {% for ikey, ibody in qbody.tests.items() %} - <tr style="line-height: 10px; height: 10px;"> - <td class="table-success" id="tbl-{{ikey}}-title">{{ibody.title}} </td> - <td class="table-danger" id="tbl-{{ikey}}-unit">pass</td> - <td id="tbl-{{ikey}}-token">fail</td> - </tr> - <!-- - <div class="tab-pane fade" id="{{ikey}}-pane" role="tabpanel" aria-labelledby="{{ikey}}-pane-tab"> - --> - <!-- begin tab card --> - <!-- - <h1>{{qbody.title}}</h1>--> - {% endfor %} - - </table> - {% endfor %} - <h5>Hand-in instructions:</h5> -<p> -To hand in your results, you should run the file <code>{{grade_script}}</code>. You can either do this from your IDE, or by going to the directory: -<pre> - <code>{{root_dir}}</code> -</pre> -and from there run the command:<pre><code> - {{run_cmd_grade}}</code> -</pre> -This will generate a <code>.token</code> file which contains your answers and you should upload to DTU learn. -</p> - </div> - </div> - </div> - <div class="col-sm-2"> </div> - -</div> - </div> -{% endblock %} -{% block navigation %} - -<span class="test-running"></span> - <span role="tablist" aria-orientation="vertical"> - <ul class="list-unstyled ps-0"> - {% for qkey, qbody in questions.items() %} - {% set outer_loop = loop %} - <li class="mb-1"> - <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#{{qkey}}-collapse" aria-expanded="true" > - {{qbody.title}} </button> - <div class="collapse show" id="{{qkey}}-collapse"> - <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small"> - {% for ikey, ibody in qbody.tests.items() %} - <li> - - <div class="container" style=""> - <div class="row" style="background-color: white;"> - <div class="col col-lg-11 text-truncate" style="background-color: white;"> - <button class="btn rounded collapsed nav-link {{ 'active' if outer_loop.index == 1 and loop.index == 1 else ''}} text-left" style="width: 100%;" id="{{ikey}}-pane-tab" data-bs-toggle="pill" data-bs-target="#{{ikey}}-pane" type="button" role="tab" aria-controls="{{ikey}}-pane" aria-selected="false" data-toggle="tab"> - <span class="{{ikey}}-status"> - <span id="{{ikey}}-status"><i id="{{ikey}}-icon" class="bi bi-emoji-neutral"></i><span class="text-left">{{ibody.title}}</span></span> - </span> - </button> - </div> - <div class="col col-lg-auto" style="padding: 0px; backgrund-color: white;"> - <a onclick="re_run_test('{{ikey}}');" type="button" class="btn btn-primary btn-sm" style="padding: 0px; margin: 0px;"><i class="bi bi-arrow-clockwise"></i></a> - </div> - </div> - </div> - - - <!-- - <button class="nav-link" id="v-pills-settings-tab" data-bs-toggle="pill" data-bs-target="#v-pills-settings" type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false">Settings</button> - --> - - </li> - {% endfor %} <!-- - <li><a href="#" class="link-dark rounded">Updates</a></li> - <li><a href="#" class="link-dark rounded">Reports</a></li> --> - </ul> - </div> - </li> - {% endfor %} - </ul> - <hr/> - <button class="btn btn-success" style="width: 100%;" id="token-pane-tab" data-bs-toggle="pill" data-bs-target="#token-pane" type="button" role="tab" aria-controls="token-pane" aria-selected="false" data-toggle="tab"> - Submit results - </button> - - </span> - - -{% endblock %} \ No newline at end of file diff --git a/vue_flask_stuff/server/templates/old/scrap.html b/vue_flask_stuff/server/templates/old/scrap.html deleted file mode 100644 index 0f28275acc15311cc84bcabc9f87f97af29a7ddf..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/templates/old/scrap.html +++ /dev/null @@ -1,13 +0,0 @@ - <li class="mb-1"> - <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#dashboard-collapse" aria-expanded="false"> - Dashboard - </button> - <div class="collapse" id="dashboard-collapse"> - <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small"> - <li><a href="#" class="link-dark rounded">Overview</a></li> - <li><a href="#" class="link-dark rounded">Weekly</a></li> - <li><a href="#" class="link-dark rounded">Monthly</a></li> - <li><a href="#" class="link-dark rounded">Annually</a></li> - </ul> - </div> - </li> \ No newline at end of file diff --git a/vue_flask_stuff/server/templates/sidebar.html b/vue_flask_stuff/server/templates/sidebar.html deleted file mode 100644 index b76fdfb8a484f92fca011bbd29ebd5d527979512..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/templates/sidebar.html +++ /dev/null @@ -1,202 +0,0 @@ -<!doctype html> -<html lang="en"> - <head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <link rel="icon" type="image/x-icon" href="/static/favicon.ico"> - <meta name="description" content=""> - <meta name="author" content="Tue Herlau"> - <meta name="generator" content="Unitgrade"> - <title>Unitgrade dashboard</title> - - <link rel="canonical" href="https://getbootstrap.com/docs/5.0/examples/sidebars/"> - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous"> - <script src="https://unpkg.com/xterm@4.11.0/lib/xterm.js"></script> - <script src="https://unpkg.com/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script> - <script src="https://unpkg.com/xterm-addon-web-links@0.4.0/lib/xterm-addon-web-links.js"></script> - <script src="https://unpkg.com/xterm-addon-search@0.8.0/lib/xterm-addon-search.js"></script> - <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script> - <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script> -<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.3.1.min.js"></script> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css"> -<!-- terminal related --> - <link rel="stylesheet" href="https://unpkg.com/xterm@4.11.0/css/xterm.css"/> - <script src="https://unpkg.com/xterm@4.11.0/lib/xterm.js"></script> - <script src="https://unpkg.com/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script> - <script src="https://unpkg.com/xterm-addon-web-links@0.4.0/lib/xterm-addon-web-links.js"></script> - <script src="https://unpkg.com/xterm-addon-search@0.8.0/lib/xterm-addon-search.js"></script> - <!-- end terminal related --> - <style> - .bd-placeholder-img { - font-size: 1.125rem; - text-anchor: middle; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - } - - @media (min-width: 768px) { - .bd-placeholder-img-lg { - font-size: 3.5rem; - } - } - </style> - <!-- Custom styles for this template --> - <link href="static/sidebars.css" rel="stylesheet"> - <link href="static/wz_style_modified.css" rel="stylesheet"> - - <script> - var CONSOLE_MODE = false, - EVALEX = false, // console mode is possible. - EVALEX_TRUSTED = true, - SECRET = "Xbtn32ZR6AqRabFk2a3l"; - </script> - - <link href="static/unitgrade.css" rel="stylesheet"> - </head> - <body> - -<main> - <div class="flex-shrink-0 p-3 bg-white" style="width: 280px;"> - <a href="/" class="d-flex align-items-center pb-3 mb-3 link-dark text-decoration-none border-bottom"> - <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi me-2" width="30" height="24" viewBox="0 0 16 16"> - <path d="M8.211 2.047a.5.5 0 0 0-.422 0l-7.5 3.5a.5.5 0 0 0 .025.917l7.5 3a.5.5 0 0 0 .372 0L14 7.14V13a1 1 0 0 0-1 1v2h3v-2a1 1 0 0 0-1-1V6.739l.686-.275a.5.5 0 0 0 .025-.917l-7.5-3.5Z"/> - <path d="M4.176 9.032a.5.5 0 0 0-.656.327l-.5 1.7a.5.5 0 0 0 .294.605l4.5 1.8a.5.5 0 0 0 .372 0l4.5-1.8a.5.5 0 0 0 .294-.605l-.5-1.7a.5.5 0 0 0-.656-.327L8 10.466 4.176 9.032Z"/> -</svg> - <span class="fs-5 fw-semibold"> Unitgrade</span> - <span class="badge rounded-pill bg-success" id="status-connected">Connected</span> - <span class="badge rounded-pill bg-warning text-dark" id="status-connecting">Connecting</span> - - </a> - - - {% block pill %} {% endblock %} - {% block navigation %} {% endblock %} - <!-- - <span class="badge rounded-pill bg-primary">Primary</span> -<span class="badge rounded-pill bg-secondary">Secondary</span> -<span class="badge rounded-pill bg-success">Success</span> -<span class="badge rounded-pill bg-danger">Danger</span> -<span class="badge rounded-pill bg-warning text-dark">Warning</span> -<span class="badge rounded-pill bg-info text-dark">Info</span> -<span class="badge rounded-pill bg-light text-dark">Light</span> -<span class="badge rounded-pill bg-dark">Dark</span> ---> - - - <!-- - <ul class="list-unstyled ps-0"> - <li class="mb-1"> - <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#home-collapse" aria-expanded="true"> -<span class="bi bi-emoji-neutral icon-green"> </span> Home - </button> - <div class="collapse show" id="home-collapse"> - <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small"> - <li><a href="#" class="link-dark rounded">Overview</a></li> - <li><a href="#" class="link-dark rounded">Updates</a></li> - <li><a href="#" class="link-dark rounded">Reports</a></li> - </ul> - </div> - </li> - <li class="mb-1"> - <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#dashboard-collapse" aria-expanded="false"> - Dashboard - </button> - <div class="collapse" id="dashboard-collapse"> - <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small"> - <li><a href="#" class="link-dark rounded">Overview</a></li> - <li><a href="#" class="link-dark rounded">Weekly</a></li> - <li><a href="#" class="link-dark rounded">Monthly</a></li> - <li><a href="#" class="link-dark rounded">Annually</a></li> - </ul> - </div> - </li> - <li class="mb-1"> - <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#orders-collapse" aria-expanded="false"> - Orders - </button> - <div class="collapse" id="orders-collapse"> - <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small"> - <li><a href="#" class="link-dark rounded">New</a></li> - <li><a href="#" class="link-dark rounded">Processed</a></li> - <li><a href="#" class="link-dark rounded">Shipped</a></li> - <li><a href="#" class="link-dark rounded">Returned</a></li> - </ul> - </div> - </li> - <li class="border-top my-3"></li> - <li class="mb-1"> - <button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#account-collapse" aria-expanded="false"> - Account - </button> - <div class="collapse" id="account-collapse"> - <ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small"> - <li><a href="#" class="link-dark rounded">New...</a></li> - <li><a href="#" class="link-dark rounded">Profile</a></li> - <li><a href="#" class="link-dark rounded">Settings</a></li> - <li><a href="#" class="link-dark rounded">Sign out</a></li> - </ul> - </div> - </li> - </ul> - --> - </div> - <div class="b-example-divider"> - {% block content %} - - - {% endblock %} - <!-- - <h1>Subtest 1</h1> -<h4>Test integration and stuff.</h4> - - -<div class="card shadow mb-3 bg-white rounded"> - <div class="card-header"> -Terminal Output - </div> - <div class="card-body"> - <p class="card-text"> - stuff . - {% block terminal %}{% endblock %} - - With supporting text below as a natural lead-in to additional content.</p> - </div> -</div> - - <div class="row"> - <div class="col-sm-6"> - <div class="card shadow mb-5 bg-white rounded"> - <div class="card-header"> -Test outcome - </div> - <div class="card-body"> - {% block stacktrace %} {% endblock %} - <h5 class="card-title">Special title treatment</h5> - <p class="card-text">With supporting text below as a natural lead-in to additional content.</p> - <a href="#" class="btn btn-primary">Go somewhere</a> - </div> - </div> - </div> - <div class="col-sm-6"> - <div class="card shadow mb-5 bg-white rounded"> - <div class="card-header"> Hints </div> - <div class="card-body"> - {% block hints %} {% endblock %} - <h5 class="card-title">Special title treatment</h5> - <p class="card-text">With supporting text below as a natural lead-in to additional content.</p> - <a href="#" class="btn btn-primary">Go somewhere</a> - </div> - </div> - </div> -</div> ---> - </div> <!-- example divider ends --> -</main> -<!-- - <script src="../assets/dist/js/bootstrap.bundle.min.js"></script> --> - <script src="static/sidebars.js"></script> - <script src="static/unitgrade.js"></script> - <script src="/static/wz_js.js"></script> - </body> -</html> diff --git a/vue_flask_stuff/server/templates/terminal.html b/vue_flask_stuff/server/templates/terminal.html deleted file mode 100644 index 1a0cf82efafbe058d62ac0f8b74a39b4870e53af..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/templates/terminal.html +++ /dev/null @@ -1,103 +0,0 @@ -<html lang="en"> - <head> - <meta charset="utf-8" /> - <title>pyxterm.js</title> - <style> - html { - font-family: arial; - } - </style> - <link - rel="stylesheet" - href="https://unpkg.com/xterm@4.11.0/css/xterm.css" - /> - </head> - <body> - <span style="font-size: 1.4em">pyxterm.js</span> - <span style="font-size: small" - >status: - <span style="font-size: small" id="status">connecting...</span></span> - - <div style="width: 100%; height: calc(100% - 50px)" id="terminal"></div> - - <p style="text-align: right; font-size: small"> - built by <a href="https://chadsmith.dev">Chad Smith</a> - <a href="https://github.com/cs01">GitHub</a> - </p> - <!-- xterm --> - <script src="https://unpkg.com/xterm@4.11.0/lib/xterm.js"></script> - <script src="https://unpkg.com/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script> - <script src="https://unpkg.com/xterm-addon-web-links@0.4.0/lib/xterm-addon-web-links.js"></script> - <script src="https://unpkg.com/xterm-addon-search@0.8.0/lib/xterm-addon-search.js"></script> - <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script> - - <script> - const term = new Terminal({ - cursorBlink: true, - macOptionIsMeta: true, - scrollback: true, - }); - // https://github.com/xtermjs/xterm.js/issues/2941 - const fit = new FitAddon.FitAddon(); - term.loadAddon(fit); - term.loadAddon(new WebLinksAddon.WebLinksAddon()); - term.loadAddon(new SearchAddon.SearchAddon()); - - term.open(document.getElementById("terminal")); - fit.fit(); - term.resize(15, 50); - console.log(`size: ${term.cols} columns, ${term.rows} rows`); - fit.fit(); - term.writeln("Welcome to pyxterm.js!"); - term.writeln("https://github.com/cs01/pyxterm.js"); - term.onData((data) => { - console.log("key pressed in browser:", data); - socket.emit("pty-input", { input: data }); - }); - - const socket = io.connect("/pty"); - const status = document.getElementById("status"); - - - socket.of("/admin").on("state", function (data) { - console.log("new output received from server:", data.output); - term.write(data.output); - }); - - socket.on("pty-output", function (data) { - console.log("new output received from server:", data.output); - term.write(data.output); - }); - - socket.on("connect", () => { - fitToscreen(); - status.innerHTML = - '<span style="background-color: lightgreen;">connected</span>'; - }); - - socket.on("disconnect", () => { - status.innerHTML = - '<span style="background-color: #ff8383;">disconnected</span>'; - }); - - function fitToscreen() { - fit.fit(); - const dims = { cols: term.cols, rows: term.rows }; - console.log("sending new dimensions to server's pty", dims); - socket.emit("resize", dims); - } - - function debounce(func, wait_ms) { - let timeout; - return function (...args) { - const context = this; - clearTimeout(timeout); - timeout = setTimeout(() => func.apply(context, args), wait_ms); - }; - } - - const wait_ms = 50; - window.onresize = debounce(fitToscreen, wait_ms); - </script> - </body> -</html> \ No newline at end of file diff --git a/vue_flask_stuff/server/templates/wz.html b/vue_flask_stuff/server/templates/wz.html deleted file mode 100644 index 5307f9378edef9955b75767dc8fccf0ef8276e38..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/templates/wz.html +++ /dev/null @@ -1,295 +0,0 @@ -<!doctype html> -<html lang=en> - <head> - <title>AssertionError - // Werkzeug Debugger</title> - - - <link rel="canonical" href="https://getbootstrap.com/docs/5.0/examples/sidebars/"> - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous"> - <script src="https://unpkg.com/xterm@4.11.0/lib/xterm.js"></script> - <script src="https://unpkg.com/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script> - <script src="https://unpkg.com/xterm-addon-web-links@0.4.0/lib/xterm-addon-web-links.js"></script> - <script src="https://unpkg.com/xterm-addon-search@0.8.0/lib/xterm-addon-search.js"></script> - <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script> - <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script> -<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.3.1.min.js"></script> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css"> - - <style> - .bd-placeholder-img { - font-size: 1.125rem; - text-anchor: middle; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - } - - @media (min-width: 768px) { - .bd-placeholder-img-lg { - font-size: 3.5rem; - } - } - </style> - <!-- Custom styles for this template --> - <link href="static/sidebars.css" rel="stylesheet"> - <link href="static/wz_style.css" rel="stylesheet"> - <link href="static/unitgrade.css" rel="stylesheet"> - - - - <link rel="stylesheet" href="/static/wz_style_modified.css"> - <link rel="shortcut icon" - href="?__debugger__=yes&cmd=resource&f=console.png"> - <script src="/static/wz_js.js"></script> - <script> - var CONSOLE_MODE = false, - EVALEX = true, - EVALEX_TRUSTED = true, - SECRET = "Xbtn32ZR6AqRabFk2a3l"; - </script> - </head> - <body style="background-color: #fff"> - <div class="debugger"> -<h1>AssertionError</h1> -<div class="detail"> - <p class="errormsg">AssertionError -</p> -</div> -<h2 class="traceback">Traceback <em>(most recent call last)</em></h2> -<div class="traceback"> - <h3></h3> - <ul><li><div class="frame" id="frame-140166614937696"> - <h4>File <cite class="filename">"/usr/local/lib/python3.10/dist-packages/flask/app.py"</cite>, - line <em class="line">2548</em>, - in <code class="function">__call__</code></h4> - <div class="source library"><pre class="line before"><span class="ws"> </span>def __call__(self, environ: dict, start_response: t.Callable) -> t.Any:</pre> -<pre class="line before"><span class="ws"> </span>"""The WSGI server calls the Flask application object as the</pre> -<pre class="line before"><span class="ws"> </span>WSGI application. This calls :meth:`wsgi_app`, which can be</pre> -<pre class="line before"><span class="ws"> </span>wrapped to apply middleware.</pre> -<pre class="line before"><span class="ws"> </span>"""</pre> -<pre class="line current"><span class="ws"> </span>return self.wsgi_app(environ, start_response)</pre></div> -</div> - -<li><div class="frame" id="frame-140166595255472"> - <h4>File <cite class="filename">"/home/tuhe/.local/lib/python3.10/site-packages/flask_socketio/__init__.py"</cite>, - line <em class="line">43</em>, - in <code class="function">__call__</code></h4> - <div class="source "><pre class="line before"><span class="ws"> </span>socketio_path=socketio_path)</pre> -<pre class="line before"><span class="ws"></span> </pre> -<pre class="line before"><span class="ws"> </span>def __call__(self, environ, start_response):</pre> -<pre class="line before"><span class="ws"> </span>environ = environ.copy()</pre> -<pre class="line before"><span class="ws"> </span>environ['flask.app'] = self.flask_app</pre> -<pre class="line current"><span class="ws"> </span>return super(_SocketIOMiddleware, self).__call__(environ,</pre> -<pre class="line after"><span class="ws"> </span>start_response)</pre> -<pre class="line after"><span class="ws"></span> </pre> -<pre class="line after"><span class="ws"></span> </pre> -<pre class="line after"><span class="ws"></span>class _ManagedSession(dict, SessionMixin):</pre> -<pre class="line after"><span class="ws"> </span>"""This class is used for user sessions that are managed by</pre></div> -</div> - -<li><div class="frame" id="frame-140166595342432"> - <h4>File <cite class="filename">"/home/tuhe/.local/lib/python3.10/site-packages/engineio/middleware.py"</cite>, - line <em class="line">74</em>, - in <code class="function">__call__</code></h4> - <div class="source "><pre class="line before"><span class="ws"> </span>'200 OK',</pre> -<pre class="line before"><span class="ws"> </span>[('Content-Type', static_file['content_type'])])</pre> -<pre class="line before"><span class="ws"> </span>with open(static_file['filename'], 'rb') as f:</pre> -<pre class="line before"><span class="ws"> </span>return [f.read()]</pre> -<pre class="line before"><span class="ws"> </span>elif self.wsgi_app is not None:</pre> -<pre class="line current"><span class="ws"> </span>return self.wsgi_app(environ, start_response)</pre> -<pre class="line after"><span class="ws"> </span>return self.not_found(start_response)</pre> -<pre class="line after"><span class="ws"></span> </pre> -<pre class="line after"><span class="ws"> </span>def not_found(self, start_response):</pre> -<pre class="line after"><span class="ws"> </span>start_response("404 Not Found", [('Content-Type', 'text/plain')])</pre> -<pre class="line after"><span class="ws"> </span>return [b'Not Found']</pre></div> -</div> - -<li><div class="frame" id="frame-140166595344224"> - <h4>File <cite class="filename">"/usr/local/lib/python3.10/dist-packages/flask/app.py"</cite>, - line <em class="line">2528</em>, - in <code class="function">wsgi_app</code></h4> - <div class="source library"><pre class="line before"><span class="ws"> </span>try:</pre> -<pre class="line before"><span class="ws"> </span>ctx.push()</pre> -<pre class="line before"><span class="ws"> </span>response = self.full_dispatch_request()</pre> -<pre class="line before"><span class="ws"> </span>except Exception as e:</pre> -<pre class="line before"><span class="ws"> </span>error = e</pre> -<pre class="line current"><span class="ws"> </span>response = self.handle_exception(e)</pre> -<pre class="line after"><span class="ws"> </span>except: # noqa: B001</pre> -<pre class="line after"><span class="ws"> </span>error = sys.exc_info()[1]</pre> -<pre class="line after"><span class="ws"> </span>raise</pre> -<pre class="line after"><span class="ws"> </span>return response(environ, start_response)</pre> -<pre class="line after"><span class="ws"> </span>finally:</pre></div> -</div> - -<li><div class="frame" id="frame-140166595344336"> - <h4>File <cite class="filename">"/usr/local/lib/python3.10/dist-packages/flask/app.py"</cite>, - line <em class="line">2525</em>, - in <code class="function">wsgi_app</code></h4> - <div class="source library"><pre class="line before"><span class="ws"> </span>ctx = self.request_context(environ)</pre> -<pre class="line before"><span class="ws"> </span>error: t.Optional[BaseException] = None</pre> -<pre class="line before"><span class="ws"> </span>try:</pre> -<pre class="line before"><span class="ws"> </span>try:</pre> -<pre class="line before"><span class="ws"> </span>ctx.push()</pre> -<pre class="line current"><span class="ws"> </span>response = self.full_dispatch_request()</pre> -<pre class="line after"><span class="ws"> </span>except Exception as e:</pre> -<pre class="line after"><span class="ws"> </span>error = e</pre> -<pre class="line after"><span class="ws"> </span>response = self.handle_exception(e)</pre> -<pre class="line after"><span class="ws"> </span>except: # noqa: B001</pre> -<pre class="line after"><span class="ws"> </span>error = sys.exc_info()[1]</pre></div> -</div> - -<li><div class="frame" id="frame-140166595344448"> - <h4>File <cite class="filename">"/usr/local/lib/python3.10/dist-packages/flask/app.py"</cite>, - line <em class="line">1822</em>, - in <code class="function">full_dispatch_request</code></h4> - <div class="source library"><pre class="line before"><span class="ws"> </span>request_started.send(self)</pre> -<pre class="line before"><span class="ws"> </span>rv = self.preprocess_request()</pre> -<pre class="line before"><span class="ws"> </span>if rv is None:</pre> -<pre class="line before"><span class="ws"> </span>rv = self.dispatch_request()</pre> -<pre class="line before"><span class="ws"> </span>except Exception as e:</pre> -<pre class="line current"><span class="ws"> </span>rv = self.handle_user_exception(e)</pre> -<pre class="line after"><span class="ws"> </span>return self.finalize_request(rv)</pre> -<pre class="line after"><span class="ws"></span> </pre> -<pre class="line after"><span class="ws"> </span>def finalize_request(</pre> -<pre class="line after"><span class="ws"> </span>self,</pre> -<pre class="line after"><span class="ws"> </span>rv: t.Union[ft.ResponseReturnValue, HTTPException],</pre></div> -</div> - -<li><div class="frame" id="frame-140166595344560"> - <h4>File <cite class="filename">"/usr/local/lib/python3.10/dist-packages/flask/app.py"</cite>, - line <em class="line">1820</em>, - in <code class="function">full_dispatch_request</code></h4> - <div class="source library"><pre class="line before"><span class="ws"></span> </pre> -<pre class="line before"><span class="ws"> </span>try:</pre> -<pre class="line before"><span class="ws"> </span>request_started.send(self)</pre> -<pre class="line before"><span class="ws"> </span>rv = self.preprocess_request()</pre> -<pre class="line before"><span class="ws"> </span>if rv is None:</pre> -<pre class="line current"><span class="ws"> </span>rv = self.dispatch_request()</pre> -<pre class="line after"><span class="ws"> </span>except Exception as e:</pre> -<pre class="line after"><span class="ws"> </span>rv = self.handle_user_exception(e)</pre> -<pre class="line after"><span class="ws"> </span>return self.finalize_request(rv)</pre> -<pre class="line after"><span class="ws"></span> </pre> -<pre class="line after"><span class="ws"> </span>def finalize_request(</pre></div> -</div> - -<li><div class="frame" id="frame-140166595344672"> - <h4>File <cite class="filename">"/usr/local/lib/python3.10/dist-packages/flask/app.py"</cite>, - line <em class="line">1796</em>, - in <code class="function">dispatch_request</code></h4> - <div class="source library"><pre class="line before"><span class="ws"> </span>and req.method == "OPTIONS"</pre> -<pre class="line before"><span class="ws"> </span>):</pre> -<pre class="line before"><span class="ws"> </span>return self.make_default_options_response()</pre> -<pre class="line before"><span class="ws"> </span># otherwise dispatch to the handler for that endpoint</pre> -<pre class="line before"><span class="ws"> </span>view_args: t.Dict[str, t.Any] = req.view_args # type: ignore[assignment]</pre> -<pre class="line current"><span class="ws"> </span>return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)</pre> -<pre class="line after"><span class="ws"></span> </pre> -<pre class="line after"><span class="ws"> </span>def full_dispatch_request(self) -> Response:</pre> -<pre class="line after"><span class="ws"> </span>"""Dispatches the request and on top of that performs request</pre> -<pre class="line after"><span class="ws"> </span>pre and postprocessing as well as HTTP exception catching and</pre> -<pre class="line after"><span class="ws"> </span>error handling.</pre></div> -</div> - -<li><div class="frame" id="frame-140166595344784"> - <h4>File <cite class="filename">"/home/tuhe/Documents/unitgrade_private/vue_flask_stuff/server/app.py"</cite>, - line <em class="line">201</em>, - in <code class="function">navbar</code></h4> - <div class="source "><pre class="line before"><span class="ws"> </span>logging.debug("received input from browser: %s" % data["input"])</pre> -<pre class="line before"><span class="ws"> </span>os.write(app.config["fd"], data["input"].encode())</pre> -<pre class="line before"><span class="ws"></span> </pre> -<pre class="line before"><span class="ws"> </span>@app.route("/crash")</pre> -<pre class="line before"><span class="ws"> </span>def navbar():</pre> -<pre class="line current"><span class="ws"> </span>assert False</pre> -<pre class="line after"><span class="ws"> </span># return render_template("index3.html")</pre> -<pre class="line after"><span class="ws"></span> </pre> -<pre class="line after"><span class="ws"></span> </pre> -<pre class="line after"><span class="ws"></span> </pre> -<pre class="line after"><span class="ws"> </span>@socketio.on("reconnected", namespace="/status")</pre></div> -</div> -</ul> - <blockquote>AssertionError -</blockquote> -</div> - -<div class="plain"> - <p> - This is the Copy/Paste friendly version of the traceback. - </p> - <textarea cols="50" rows="10" name="code" readonly>Traceback (most recent call last): - File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 2548, in __call__ - return self.wsgi_app(environ, start_response) - File "/home/tuhe/.local/lib/python3.10/site-packages/flask_socketio/__init__.py", line 43, in __call__ - return super(_SocketIOMiddleware, self).__call__(environ, - File "/home/tuhe/.local/lib/python3.10/site-packages/engineio/middleware.py", line 74, in __call__ - return self.wsgi_app(environ, start_response) - File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 2528, in wsgi_app - response = self.handle_exception(e) - File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 2525, in wsgi_app - response = self.full_dispatch_request() - File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 1822, in full_dispatch_request - rv = self.handle_user_exception(e) - File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 1820, in full_dispatch_request - rv = self.dispatch_request() - File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 1796, in dispatch_request - return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) - File "/home/tuhe/Documents/unitgrade_private/vue_flask_stuff/server/app.py", line 201, in navbar - assert False -AssertionError -</textarea> -</div> -<div class="explanation"> - The debugger caught an exception in your WSGI application. You can now - look at the traceback which led to the error. <span class="nojavascript"> - If you enable JavaScript you can also use additional features such as code - execution (if the evalex feature is enabled), automatic pasting of the - exceptions and much more.</span> -</div> - <div class="footer"> - Brought to you by <strong class="arthur">DON'T PANIC</strong>, your - friendly Werkzeug powered traceback interpreter. - </div> - </div> - - <div class="pin-prompt"> - <div class="inner"> - <h3>Console Locked</h3> - <p> - The console is locked and needs to be unlocked by entering the PIN. - You can find the PIN printed out on the standard output of your - shell that runs the server. - <form> - <p>PIN: - <input type=text name=pin size=14> - <input type=submit name=btn value="Confirm Pin"> - </form> - </div> - </div> - - <script src="../assets/dist/js/bootstrap.bundle.min.js"></script> - <script src="static/sidebars.js"></script> - <script src="static/unitgrade.js"></script> - </body> -</html> - -<!-- - -Traceback (most recent call last): - File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 2548, in __call__ - return self.wsgi_app(environ, start_response) - File "/home/tuhe/.local/lib/python3.10/site-packages/flask_socketio/__init__.py", line 43, in __call__ - return super(_SocketIOMiddleware, self).__call__(environ, - File "/home/tuhe/.local/lib/python3.10/site-packages/engineio/middleware.py", line 74, in __call__ - return self.wsgi_app(environ, start_response) - File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 2528, in wsgi_app - response = self.handle_exception(e) - File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 2525, in wsgi_app - response = self.full_dispatch_request() - File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 1822, in full_dispatch_request - rv = self.handle_user_exception(e) - File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 1820, in full_dispatch_request - rv = self.dispatch_request() - File "/usr/local/lib/python3.10/dist-packages/flask/app.py", line 1796, in dispatch_request - return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) - File "/home/tuhe/Documents/unitgrade_private/vue_flask_stuff/server/app.py", line 201, in navbar - assert False -AssertionError diff --git a/vue_flask_stuff/server/watcher.py b/vue_flask_stuff/server/watcher.py deleted file mode 100644 index b7507b3cd7202897eb19c80fd0036a571b88582e..0000000000000000000000000000000000000000 --- a/vue_flask_stuff/server/watcher.py +++ /dev/null @@ -1,53 +0,0 @@ -import os -from watchdog.events import FileSystemEventHandler -import time -from watchdog.observers import Observer -import datetime -import fnmatch -import os - -class Watcher: - # DIRECTORY_TO_WATCH = "/path/to/my/directory" - - def __init__(self, base_directory, watched_files_dictionary, watched_files_lock): - self.base_directory = base_directory - self.watched_files_dictionary = watched_files_dictionary - self.watched_files_lock = watched_files_lock - self.observer = Observer() - - def run(self): - event_handler = Handler(self.watched_files_dictionary, self.watched_files_lock) - # directory = os.path.commonpath([os.path.dirname(f) for f in self.watched_files_dictionary.keys()]) - self.observer.schedule(event_handler, self.base_directory, recursive=True) - self.observer.start() - - def close(self): - print("Closing watcher..") - self.observer.stop() - self.observer.join() - print("closed") - - def __del__(self): - print("Stopping watcher...") - self.close() - - -class Handler(FileSystemEventHandler): - def __init__(self, watched_files_dictionary, watched_files_lock): - self.watched_files_dictionary = watched_files_dictionary - self.watched_files_lock = watched_files_lock - super().__init__() - - def on_any_event(self, event): - if event.is_directory: - return None - elif event.event_type == 'created' or event.event_type == 'modified': - with self.watched_files_lock: - fnd_ = None - for k in self.watched_files_dictionary: - if fnmatch.fnmatch(event.src_path, k): - fnd_ = k - break - if fnd_ is not None: - self.watched_files_dictionary[fnd_]['last_recorded_change'] = datetime.datetime.now() - self.watched_files_dictionary[fnd_]['file'] = event.src_path