From a7bed7e4a64eeb4c5661164015e90c843a3b833d Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Mon, 21 Aug 2023 13:51:02 -0700 Subject: [PATCH 01/46] initial conversion of autotest code from 0.7.x to 0.9.x --- nbgrader/apps/generateassignmentapp.py | 32 +- nbgrader/apps/quickstartapp.py | 5 + nbgrader/converters/__init__.py | 4 +- nbgrader/converters/generate_assignment.py | 2 + .../converters/generate_source_with_tests.py | 49 ++ nbgrader/coursedir.py | 12 + .../creating_and_grading_assignments.ipynb | 53 ++ nbgrader/docs/source/user_guide/grades.csv | 4 +- ...ograder_tests_autogenerated_tests_jlab.png | Bin 0 -> 139935 bytes .../images/autograder_tests_autotest_jlab.png | Bin 0 -> 68453 bytes .../managing_assignment_files.ipynb | 7 +- .../user_guide/source/ps1/problem3.ipynb | 399 +++++++++ .../source/user_guide/source/ps2/jupyter.png | Bin 0 -> 5733 bytes .../user_guide/source/ps2/problem.ipynb | 241 ++++++ nbgrader/docs/source/user_guide/tests.yml | 305 +++++++ nbgrader/preprocessors/__init__.py | 2 + nbgrader/preprocessors/clearsolutions.py | 1 + nbgrader/preprocessors/instantiatetests.py | 758 ++++++++++++++++++ nbgrader/tests/__init__.py | 15 + .../apps/files/autotest-hashed-changed.ipynb | 112 +++ .../files/autotest-hashed-unchanged.ipynb | 110 +++ .../tests/apps/files/autotest-hashed.ipynb | 82 ++ .../files/autotest-hidden-changed-right.ipynb | 85 ++ .../files/autotest-hidden-changed-wrong.ipynb | 85 ++ .../files/autotest-hidden-unchanged.ipynb | 83 ++ .../tests/apps/files/autotest-hidden.ipynb | 80 ++ .../apps/files/autotest-multi-changed.ipynb | 267 ++++++ .../apps/files/autotest-multi-unchanged.ipynb | 277 +++++++ .../tests/apps/files/autotest-multi.ipynb | 164 ++++ .../apps/files/autotest-simple-changed.ipynb | 92 +++ .../files/autotest-simple-unchanged.ipynb | 90 +++ .../tests/apps/files/autotest-simple.ipynb | 74 ++ .../files/test-no-metadata-autotest.ipynb | 225 ++++++ nbgrader/tests/apps/files/tests.yml | 310 +++++++ .../tests/apps/test_nbgrader_autograde.py | 180 +++++ .../apps/test_nbgrader_generate_assignment.py | 82 ++ .../apps/test_nbgrader_generate_feedback.py | 35 + .../preprocessors/test_instantiatetests.py | 204 +++++ pyproject.toml | 1 + 39 files changed, 4520 insertions(+), 7 deletions(-) create mode 100644 nbgrader/converters/generate_source_with_tests.py create mode 100644 nbgrader/docs/source/user_guide/images/autograder_tests_autogenerated_tests_jlab.png create mode 100644 nbgrader/docs/source/user_guide/images/autograder_tests_autotest_jlab.png create mode 100644 nbgrader/docs/source/user_guide/source/ps1/problem3.ipynb create mode 100644 nbgrader/docs/source/user_guide/source/ps2/jupyter.png create mode 100644 nbgrader/docs/source/user_guide/source/ps2/problem.ipynb create mode 100644 nbgrader/docs/source/user_guide/tests.yml create mode 100644 nbgrader/preprocessors/instantiatetests.py create mode 100644 nbgrader/tests/apps/files/autotest-hashed-changed.ipynb create mode 100644 nbgrader/tests/apps/files/autotest-hashed-unchanged.ipynb create mode 100644 nbgrader/tests/apps/files/autotest-hashed.ipynb create mode 100644 nbgrader/tests/apps/files/autotest-hidden-changed-right.ipynb create mode 100644 nbgrader/tests/apps/files/autotest-hidden-changed-wrong.ipynb create mode 100644 nbgrader/tests/apps/files/autotest-hidden-unchanged.ipynb create mode 100644 nbgrader/tests/apps/files/autotest-hidden.ipynb create mode 100644 nbgrader/tests/apps/files/autotest-multi-changed.ipynb create mode 100644 nbgrader/tests/apps/files/autotest-multi-unchanged.ipynb create mode 100644 nbgrader/tests/apps/files/autotest-multi.ipynb create mode 100644 nbgrader/tests/apps/files/autotest-simple-changed.ipynb create mode 100644 nbgrader/tests/apps/files/autotest-simple-unchanged.ipynb create mode 100644 nbgrader/tests/apps/files/autotest-simple.ipynb create mode 100644 nbgrader/tests/apps/files/test-no-metadata-autotest.ipynb create mode 100644 nbgrader/tests/apps/files/tests.yml create mode 100644 nbgrader/tests/preprocessors/test_instantiatetests.py diff --git a/nbgrader/apps/generateassignmentapp.py b/nbgrader/apps/generateassignmentapp.py index 7acf78974..b46a7a96a 100644 --- a/nbgrader/apps/generateassignmentapp.py +++ b/nbgrader/apps/generateassignmentapp.py @@ -2,10 +2,11 @@ import sys -from traitlets import default +from traitlets import default, Bool +from textwrap import dedent from .baseapp import NbGrader, nbgrader_aliases, nbgrader_flags -from ..converters import BaseConverter, GenerateAssignment, NbGraderException +from ..converters import BaseConverter, GenerateAssignment, NbGraderException, GenerateSourceWithTests from traitlets.traitlets import MetaHasTraits from typing import List, Any from traitlets.config.loader import Config @@ -51,6 +52,12 @@ {'BaseConverter': {'force': True}}, "Overwrite an assignment/submission if it already exists." ), + 'source_with_tests': ( + {'GenerateAssignmentApp': {'source_with_tests': True}}, + "Generate intermediate notebooks that contain both the autogenerated test code and the solutions. " + "Results will be saved in the source_with_tests/ folder. " + "This is useful for instructors to debug problematic autogenerated test code." + ), }) @@ -62,6 +69,17 @@ class GenerateAssignmentApp(NbGrader): aliases = aliases flags = flags + source_with_tests = Bool( + False, + help=dedent( + """ + Generate intermediate notebooks that contain both the autogenerated test code and the solutions. + Results will be saved in the source_with_tests/ folder. + This is useful for instructors to debug issues in autogenerated test code. + """ + ) + ).tag(config=True) + examples = """ Produce the version of the assignment that is intended to be released to students. This performs several modifications to the original assignment: @@ -112,7 +130,7 @@ class GenerateAssignmentApp(NbGrader): @default("classes") def _classes_default(self) -> List[MetaHasTraits]: classes = super(GenerateAssignmentApp, self)._classes_default() - classes.extend([BaseConverter, GenerateAssignment]) + classes.extend([BaseConverter, GenerateAssignment, GenerateSourceWithTests]) return classes def _load_config(self, cfg: Config, **kwargs: Any) -> None: @@ -141,6 +159,14 @@ def start(self) -> None: elif len(self.extra_args) == 1: self.coursedir.assignment_id = self.extra_args[0] + + if self.source_with_tests: + converter = GenerateSourceWithTests(coursedir=self.coursedir, parent=self) + try: + converter.start() + except NbGraderException: + sys.exit(1) + converter = GenerateAssignment(coursedir=self.coursedir, parent=self) try: converter.start() diff --git a/nbgrader/apps/quickstartapp.py b/nbgrader/apps/quickstartapp.py index 20512b072..0af1460e4 100644 --- a/nbgrader/apps/quickstartapp.py +++ b/nbgrader/apps/quickstartapp.py @@ -122,6 +122,11 @@ def start(self): ignore_html = shutil.ignore_patterns("*.html") shutil.copytree(example, os.path.join(course_path, "source"), ignore=ignore_html) + # copying the tests.yml file to the course directory + tests_file_path = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', 'docs', 'source', 'user_guide', 'tests.yml')) + shutil.copyfile(tests_file_path, os.path.join(course_path, 'tests.yml')) + # create the config file self.log.info("Generating example config file...") currdir = os.getcwd() diff --git a/nbgrader/converters/__init__.py b/nbgrader/converters/__init__.py index f0aab0f6a..7c42e28e3 100644 --- a/nbgrader/converters/__init__.py +++ b/nbgrader/converters/__init__.py @@ -5,6 +5,7 @@ from .feedback import Feedback from .generate_feedback import GenerateFeedback from .generate_solution import GenerateSolution +from .generate_source_with_tests import GenerateSourceWithTests __all__ = [ "BaseConverter", @@ -14,5 +15,6 @@ "Autograde", "Feedback", "GenerateFeedback", - "GenerateSolution" + "GenerateSolution", + "GenerateSourceWithTests" ] diff --git a/nbgrader/converters/generate_assignment.py b/nbgrader/converters/generate_assignment.py index 6d7601ef3..231e59bd4 100644 --- a/nbgrader/converters/generate_assignment.py +++ b/nbgrader/converters/generate_assignment.py @@ -8,6 +8,7 @@ from .base import BaseConverter, NbGraderException from ..preprocessors import ( IncludeHeaderFooter, + InstantiateTests, ClearSolutions, LockCells, ComputeChecksums, @@ -57,6 +58,7 @@ def _output_directory(self) -> str: preprocessors = List([ IncludeHeaderFooter, + InstantiateTests, LockCells, ClearSolutions, ClearOutput, diff --git a/nbgrader/converters/generate_source_with_tests.py b/nbgrader/converters/generate_source_with_tests.py new file mode 100644 index 000000000..346fc4f2e --- /dev/null +++ b/nbgrader/converters/generate_source_with_tests.py @@ -0,0 +1,49 @@ +import os +import re + +from traitlets import List, default + +from .base import BaseConverter +from ..preprocessors import ( + InstantiateTests, + ClearOutput, + CheckCellMetadata +) +from traitlets.config.loader import Config +from typing import Any +from ..coursedir import CourseDirectory + + +class GenerateSourceWithTests(BaseConverter): + + @default("permissions") + def _permissions_default(self) -> int: + return 664 if self.coursedir.groupshared else 644 + + @property + def _input_directory(self) -> str: + return self.coursedir.source_directory + + @property + def _output_directory(self) -> str: + return self.coursedir.release_directory + + preprocessors = List([ + InstantiateTests, + ClearOutput, + CheckCellMetadata + ]).tag(config=True) + + def _load_config(self, cfg: Config, **kwargs: Any) -> None: + super(GenerateSourceWithTests, self)._load_config(cfg, **kwargs) + + def __init__(self, coursedir: CourseDirectory = None, **kwargs: Any) -> None: + super(GenerateSourceWithTests, self).__init__(coursedir=coursedir, **kwargs) + + def start(self) -> None: + old_student_id = self.coursedir.student_id + self.coursedir.student_id = '.' + try: + super(GenerateSourceWithTests, self).start() + finally: + self.coursedir.student_id = old_student_id diff --git a/nbgrader/coursedir.py b/nbgrader/coursedir.py index bbbe6c54d..9242e56a7 100644 --- a/nbgrader/coursedir.py +++ b/nbgrader/coursedir.py @@ -142,6 +142,18 @@ def _validate_notebook_id(self, proposal: Bunch) -> str: ) ).tag(config=True) + source_with_tests_directory = Unicode( + 'source_with_tests', + help=dedent( + """ + The name of the directory that contains notebooks with both solutions + and instantiated test code (i.e., all AUTOTEST directives are removed + and replaced by actual test code). This corresponds to the + `nbgrader_step` variable in the `directory_structure` config option. + """ + ) + ).tag(config=True) + submitted_directory = Unicode( 'submitted', help=dedent( diff --git a/nbgrader/docs/source/user_guide/creating_and_grading_assignments.ipynb b/nbgrader/docs/source/user_guide/creating_and_grading_assignments.ipynb index 1699cc464..7c473c592 100644 --- a/nbgrader/docs/source/user_guide/creating_and_grading_assignments.ipynb +++ b/nbgrader/docs/source/user_guide/creating_and_grading_assignments.ipynb @@ -425,6 +425,59 @@ " These hidden tests are placed back into the \"Autograder tests\" cells when running ``nbgrader autograde``\n", " (see :ref:`autograde-assignments`)." ] + }, + { + "cell_type": "raw", + "metadata": {}, + "source": [ + ".. _autograder-tests-cell-automatic-test-code:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### \"Autograder tests\" cells with automatically generated test code" + ] + }, + { + "cell_type": "raw", + "metadata": {}, + "source": [ + ".. versionadded:: 0.9.0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Tests in \"Autograder tests\" cells can be automatically and dynamically generated through the use of a special syntax such as ``### AUTOTEST`` and ``### HASHED AUTOTEST``, for example:\n", + "\n", + "![autograder tests autotest tests](images/autograder_tests_autotest_jlab.png)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the release version, the above autotest statements get converted to the following test code that the students see:\n", + "![autograder tests autotest tests](images/autograder_tests_autogenerated_tests_jlab.png)" + ] + }, + { + "cell_type": "raw", + "metadata": {}, + "source": [ + "When creating the release version (see :ref:`assign-and-release-an-assignment`), the autotest lines (lines starting with the special syntax) will transform into automatically generated test cases (i.e., assert statements). The value of the expression(s) following the special syntax will be evaluated in the solution version to generate test cases that are checked in the student version. If this special syntax is not used, then the contents of the cell will remain as is.\n", + "\n", + ".. note::\n", + "\n", + " Lines starting with ### AUTOTEST will generate test code where the answer is visible to students. To generate test code where the answers are *hashed* (not viewable by students), begin the line with the syntax ### HASHED AUTOTEST instead.\n", + " \n", + ".. note:: \n", + "\n", + " You can put multiple expressions to be tested on a single ### AUTOTEST line (or ### HASHED AUTOTEST line), separated by semicolons." + ] }, { "cell_type": "raw", diff --git a/nbgrader/docs/source/user_guide/grades.csv b/nbgrader/docs/source/user_guide/grades.csv index cc3ad44db..c80687689 100644 --- a/nbgrader/docs/source/user_guide/grades.csv +++ b/nbgrader/docs/source/user_guide/grades.csv @@ -1,3 +1,3 @@ assignment,duedate,timestamp,student_id,last_name,first_name,email,raw_score,late_submission_penalty,score,max_score -ps1,,2015-02-02 22:58:23.948203,bitdiddle,,,,1.5,0.0,1.5,13.0 -ps1,,2015-02-01 17:28:58.749302,hacker,,,,3.0,0.0,3.0,13.0 +ps1,,2015-02-02 22:58:23.948203,bitdiddle,,,,1.5,0.0,1.5,23.0 +ps1,,2015-02-01 17:28:58.749302,hacker,,,,3.0,0.0,3.0,23.0 diff --git a/nbgrader/docs/source/user_guide/images/autograder_tests_autogenerated_tests_jlab.png b/nbgrader/docs/source/user_guide/images/autograder_tests_autogenerated_tests_jlab.png new file mode 100644 index 0000000000000000000000000000000000000000..11262fa14aca73c612ec5e1f1a92aa1e47f85ba4 GIT binary patch literal 139935 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
&meKNi< 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

o`N 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 literal 0 HcmV?d00001 diff --git a/nbgrader/docs/source/user_guide/images/autograder_tests_autotest_jlab.png b/nbgrader/docs/source/user_guide/images/autograder_tests_autotest_jlab.png new file mode 100644 index 0000000000000000000000000000000000000000..b90893cff81ecec33e51ec5cf776b05705f43fa6 GIT binary patch literal 68453 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

J@jogG4KhwddUddl3 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 Apa1{> literal 0 HcmV?d00001 diff --git a/nbgrader/docs/source/user_guide/managing_assignment_files.ipynb b/nbgrader/docs/source/user_guide/managing_assignment_files.ipynb index 4347958f6..498a6190a 100644 --- a/nbgrader/docs/source/user_guide/managing_assignment_files.ipynb +++ b/nbgrader/docs/source/user_guide/managing_assignment_files.ipynb @@ -465,7 +465,8 @@ "total ##\n", "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] jupyter.png\n", "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] problem1.ipynb\n", - "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] problem2.ipynb\n" + "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] problem2.ipynb\n", + "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] problem3.ipynb\n" ] } ], @@ -817,9 +818,11 @@ " Expected:\n", " \tproblem1.ipynb: MISSING\n", " \tproblem2.ipynb: FOUND\n", + " \tproblem3.ipynb: FOUND\n", " Submitted:\n", " \tmyproblem1.ipynb: EXTRA\n", " \tproblem2.ipynb: OK\n", + " \tproblem3.ipynb: OK\n", "[SubmitApp | INFO] Submitted as: example_course ps1 [timestamp] UTC\n" ] } @@ -896,9 +899,11 @@ " Expected:\n", " \tproblem1.ipynb: MISSING\n", " \tproblem2.ipynb: FOUND\n", + " \tproblem3.ipynb: FOUND\n", " Submitted:\n", " \tmyproblem1.ipynb: EXTRA\n", " \tproblem2.ipynb: OK\n", + " \tproblem3.ipynb: OK\n", "[SubmitApp | ERROR] nbgrader submit failed\n" ] } diff --git a/nbgrader/docs/source/user_guide/source/ps1/problem3.ipynb b/nbgrader/docs/source/user_guide/source/ps1/problem3.ipynb new file mode 100644 index 000000000..16d713402 --- /dev/null +++ b/nbgrader/docs/source/user_guide/source/ps1/problem3.ipynb @@ -0,0 +1,399 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "jupyter", + "locked": true, + "schema_version": 3, + "solution": false + } + }, + "source": [ + "For this problem set, we'll be using the Jupyter notebook:\n", + "\n", + "![](jupyter.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part A (2 points)\n", + "\n", + "Write a function that returns a list of numbers, such that $x_i=i^2$, for $1\\leq i \\leq n$. Make sure it handles the case where $n<1$ by raising a `ValueError`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "squares", + "locked": false, + "schema_version": 3, + "solution": true + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def squares(n):\n", + " \"\"\"Compute the squares of numbers from 1 to n, such that the \n", + " ith element of the returned list equals i^2.\n", + " \n", + " \"\"\"\n", + " ### BEGIN SOLUTION\n", + " if n < 1:\n", + " raise ValueError(\"n must be greater than or equal to 1\")\n", + " return [i ** 2 for i in range(1, n + 1)]\n", + " ### END SOLUTION" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Your function should print `[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]` for $n=10$. Check that it does:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "squares(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "correct_squares", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "\"\"\"Check that squares returns the correct output for several inputs\"\"\"\n", + "### AUTOTEST squares(1); squares(2)\n", + "### HASHED AUTOTEST squares(3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "squares_invalid_input", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "\"\"\"Check that squares raises an error for invalid inputs\"\"\"\n", + "def test_func_throws(func, ErrorType):\n", + " try:\n", + " func()\n", + " except ErrorType:\n", + " return True\n", + " else:\n", + " print('Did not raise right type of error!')\n", + " return False\n", + " \n", + "### AUTOTEST test_func_throws(lambda : squares(0), ValueError)\n", + "### AUTOTEST test_func_throws(lambda : squares(-4), ValueError);\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Part B (1 point)\n", + "\n", + "Using your `squares` function, write a function that computes the sum of the squares of the numbers from 1 to $n$. Your function should call the `squares` function -- it should NOT reimplement its functionality." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "sum_of_squares", + "locked": false, + "schema_version": 3, + "solution": true + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def sum_of_squares(n):\n", + " \"\"\"Compute the sum of the squares of numbers from 1 to n.\"\"\"\n", + " ### BEGIN SOLUTION\n", + " return sum(squares(n))\n", + " ### END SOLUTION" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The sum of squares from 1 to 10 should be 385. Verify that this is the answer you get:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "sum_of_squares(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "correct_sum_of_squares", + "locked": false, + "points": 0.5, + "schema_version": 3, + "solution": false + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "\"\"\"Check that sum_of_squares returns the correct answer for various inputs.\"\"\"\n", + "### AUTOTEST sum_of_squares(1)\n", + "### AUTOTEST sum_of_squares(2); sum_of_squares(10) \n", + "### AUTOTEST sum_of_squares(11) \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "sum_of_squares_uses_squares", + "locked": false, + "points": 0.5, + "schema_version": 3, + "solution": false + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "\"\"\"Check that sum_of_squares relies on squares.\"\"\"\n", + "\n", + "orig_squares = squares\n", + "del squares\n", + "\n", + "### AUTOTEST test_func_throws(lambda : sum_of_squares(1), NameError)\n", + "\n", + "squares = orig_squares\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part C (1 point)\n", + "\n", + "Using LaTeX math notation, write out the equation that is implemented by your `sum_of_squares` function." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "sum_of_squares_equation", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": true + } + }, + "source": [ + "$\\sum_{i=1}^n i^2$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part D (2 points)\n", + "\n", + "Find a usecase for your `sum_of_squares` function and implement that usecase in the cell below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "sum_of_squares_application", + "locked": false, + "points": 2, + "schema_version": 3, + "solution": true + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def pyramidal_number(n):\n", + " \"\"\"Returns the n^th pyramidal number\"\"\"\n", + " return sum_of_squares(n)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "cell-938593c4a215c6cc", + "locked": true, + "points": 4, + "schema_version": 3, + "solution": false, + "task": true + } + }, + "source": [ + "---\n", + "## Part E (4 points)\n", + "\n", + "State the formulae for an arithmetic and geometric sum and verify them numerically for an example of your choice." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part F (1 points)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "cell-d3df8cd59fd0eb74", + "locked": false, + "schema_version": 3, + "solution": true, + "task": false + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "my_dictionary = {\n", + " 'one' : 1,\n", + " 'two' : 2,\n", + " 'three' : 3\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "cell-6e9ff83aa5dfaf17", + "locked": true, + "points": 0, + "schema_version": 3, + "solution": false, + "task": false + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "### AUTOTEST my_dictionary\n", + "### AUTOTEST my_dictionary[\"one\"]" + ] + } + ], + "metadata": { + "celltoolbar": "Create Assignment", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbgrader/docs/source/user_guide/source/ps2/jupyter.png b/nbgrader/docs/source/user_guide/source/ps2/jupyter.png new file mode 100644 index 0000000000000000000000000000000000000000..201fc09ce423a6e83c74829d25b711f65ba47f1a GIT binary patch literal 5733 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 SF7+@s1{7peq$?y%g8u^szt&R# literal 0 HcmV?d00001 diff --git a/nbgrader/docs/source/user_guide/source/ps2/problem.ipynb b/nbgrader/docs/source/user_guide/source/ps2/problem.ipynb new file mode 100644 index 000000000..2286cd1eb --- /dev/null +++ b/nbgrader/docs/source/user_guide/source/ps2/problem.ipynb @@ -0,0 +1,241 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "jupyter", + "locked": true, + "schema_version": 3, + "solution": false + } + }, + "source": [ + "For this problem set, we'll be using the Jupyter notebook:\n", + "\n", + "![](jupyter.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part A (2 points)\n", + "\n", + "Write a function that returns a vector of numbers, such that $x_i=i^2$, for $1\\leq i \\leq n$. Make sure it handles the case where $n<1$ by using `stop(\"n must be greater than or equal to 1\")`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "squares", + "locked": false, + "schema_version": 3, + "solution": true + } + }, + "outputs": [], + "source": [ + "squares <- function(n){\n", + " ### BEGIN SOLUTION\n", + " if (n < 1){\n", + " stop(\"n must be greater than or equal to 1\")\n", + " }\n", + " ret <- c()\n", + " for (i in 1:n){\n", + " ret <- append(ret, i**2)\n", + " }\n", + " return(ret)\n", + " ### END SOLUTION\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Your function should print `[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]` for $n=10$. Check that it does:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "squares(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "correct_squares", + "locked": false, + "points": 2, + "schema_version": 3, + "solution": false + } + }, + "outputs": [], + "source": [ + "### AUTOTEST squares(1); squares(2); squares(15)\n", + "### AUTOTEST squares(10)\n", + "### AUTOTEST squares(11)\n", + "### AUTOTEST 3\n", + "### AUTOTEST squares(3)\n", + "### HASHED AUTOTEST squares(3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Part B (1 point)\n", + "\n", + "Using your `squares` function, write a function that computes the sum of the squares of the numbers from 1 to $n$. Your function should call the `squares` function -- it should NOT reimplement its functionality." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "sum_of_squares", + "locked": false, + "schema_version": 3, + "solution": true + } + }, + "outputs": [], + "source": [ + "sum_of_squares <- function(n) {\n", + " ### BEGIN SOLUTION\n", + " return(sum(squares(n)))\n", + " ### END SOLUTION\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The sum of squares from 1 to 10 should be 385. Verify that this is the answer you get:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sum_of_squares(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "correct_sum_of_squares", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + } + }, + "outputs": [], + "source": [ + "### AUTOTEST sum_of_squares(1)\n", + "### AUTOTEST sum_of_squares(2); sum_of_squares(10) \n", + "### AUTOTEST sum_of_squares(11) \n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part C (1 point)\n", + "\n", + "Using LaTeX math notation, write out the equation that is implemented by your `sum_of_squares` function." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "sum_of_squares_equation", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": true + } + }, + "source": [ + "$\\sum_{i=1}^n i^2$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part D (2 points)\n", + "\n", + "Find a usecase for your `sum_of_squares` function and implement that usecase in the cell below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "sum_of_squares_application", + "locked": false, + "points": 2, + "schema_version": 3, + "solution": true + } + }, + "outputs": [], + "source": [ + "pyramidal_number <- function(n){\n", + " return(sum_of_squares(n))\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part E (4 points)\n", + "\n", + "State the formulae for an arithmetic and geometric sum and verify them numerically for an example of your choice." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "R", + "language": "R", + "name": "ir" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbgrader/docs/source/user_guide/tests.yml b/nbgrader/docs/source/user_guide/tests.yml new file mode 100644 index 000000000..d1ed36e11 --- /dev/null +++ b/nbgrader/docs/source/user_guide/tests.yml @@ -0,0 +1,305 @@ +python3: + setup: "from hashlib import sha1" + hash: 'sha1({{snippet}}.encode("utf-8")+b"{{salt}}").hexdigest()' + dispatch: "type({{snippet}})" + normalize: "str({{snippet}})" + check: 'assert {{snippet}} == """{{value}}""", """{{message}}"""' + success: "print('Success!')" + + templates: + default: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "value of {{snippet}} is not correct" + + int: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()" + + - test: "{{snippet}}" + fail: "value of {{snippet}} is not correct" + + float: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not float. Please make sure it is float and not np.float64, etc. You can cast your value into a float using float()" + + - test: "round({{snippet}}, 2)" + fail: "value of {{snippet}} is not correct (rounded to 2 decimal places)" + + set: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not set. {{snippet}} should be a set" + + - test: "len({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "value of {{snippet}} is not correct" + + list: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not list. {{snippet}} should be a list" + + - test: "len({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "sorted(map(str, {{snippet}}))" + fail: "values of {{snippet}} are not correct" + + - test: "{{snippet}}" + fail: "order of elements of {{snippet}} is not correct" + + tuple: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not tuple. {{snippet}} should be a tuple" + + - test: "len({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "sorted(map(str, {{snippet}}))" + fail: "values of {{snippet}} are not correct" + + - test: "{{snippet}}" + fail: "order of elements of {{snippet}} is not correct" + + str: + + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not str. {{snippet}} should be an str" + + - test: "len({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "{{snippet}}.lower()" + fail: "value of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "correct string value of {{snippet}} but incorrect case of letters" + + dict: + + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not dict. {{snippet}} should be a dict" + + - test: "len(list({{snippet}}.keys()))" + fail: "number of keys of {{snippet}} is not correct" + + - test: "sorted(map(str, {{snippet}}.keys()))" + fail: "keys of {{snippet}} are not correct" + + - test: "sorted(map(str, {{snippet}}.values()))" + fail: "correct keys, but values of {{snippet}} are not correct" + + - test: "{{snippet}}" + fail: "correct keys and values, but incorrect correspondence in keys and values of {{snippet}}" + + bool: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not bool. {{snippet}} should be a bool" + + - test: "{{snippet}}" + fail: "boolean value of {{snippet}} is not correct" + + type: + - test: "{{snippet}}" + fail: "type of {{snippet}} is not correct" + + pandas.core.frame.DataFrame: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not pandas.core.frame.DataFrame. {{snippet}} should be a DataFrame" + + - test: "{{snippet}}.reindex(sorted({{snippet}}.columns), axis=1)" + fail: "some or all elements of {{snippet}} are not correct" + +# --------------------------------------------- + +python: + setup: "from hashlib import sha1" + hash: 'sha1({{snippet}}.encode("utf-8")+b"{{salt}}").hexdigest()' + dispatch: "type({{snippet}})" + normalize: "str({{snippet}})" + check: 'assert {{snippet}} == """{{value}}""", """{{message}}"""' + success: "print('Success!')" + + templates: + default: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "value of {{snippet}} is not correct" + + int: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()" + + - test: "{{snippet}}" + fail: "value of {{snippet}} is not correct" + + float: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not float. Please make sure it is float and not np.float64, etc. You can cast your value into a float using float()" + + - test: "round({{snippet}}, 2)" + fail: "value of {{snippet}} is not correct (rounded to 2 decimal places)" + + set: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not set. {{snippet}} should be a set" + + - test: "len({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "value of {{snippet}} is not correct" + + list: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not list. {{snippet}} should be a list" + + - test: "len({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "sorted(map(str, {{snippet}}))" + fail: "values of {{snippet}} are not correct" + + - test: "{{snippet}}" + fail: "order of elements of {{snippet}} is not correct" + + tuple: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not tuple. {{snippet}} should be a tuple" + + - test: "len({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "sorted(map(str, {{snippet}}))" + fail: "values of {{snippet}} are not correct" + + - test: "{{snippet}}" + fail: "order of elements of {{snippet}} is not correct" + + str: + + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not str. {{snippet}} should be an str" + + - test: "len({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "{{snippet}}.lower()" + fail: "value of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "correct string value of {{snippet}} but incorrect case of letters" + + dict: + + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not dict. {{snippet}} should be a dict" + + - test: "len(list({{snippet}}.keys()))" + fail: "number of keys of {{snippet}} is not correct" + + - test: "sorted(map(str, {{snippet}}.keys()))" + fail: "keys of {{snippet}} are not correct" + + - test: "sorted(map(str, {{snippet}}.values()))" + fail: "correct keys, but values of {{snippet}} are not correct" + + - test: "{{snippet}}" + fail: "correct keys and values, but incorrect correspondence in keys and values of {{snippet}}" + + bool: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not bool. {{snippet}} should be a bool" + + - test: "{{snippet}}" + fail: "boolean value of {{snippet}} is not correct" + + type: + - test: "{{snippet}}" + fail: "type of {{snippet}} is not correct" + + pandas.core.frame.DataFrame: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not pandas.core.frame.DataFrame. {{snippet}} should be a DataFrame" + + - test: "{{snippet}}.reindex(sorted({{snippet}}.columns), axis=1)" + fail: "some or all elements of {{snippet}} are not correct" + + +# -------------------------------------------------------------------------------------------------- +ir: + setup: 'library(digest)' + hash: 'digest(paste({{snippet}}, "{{salt}}"))' + dispatch: 'class({{snippet}})' + normalize: 'toString({{snippet}})' + check: 'stopifnot("{{message}}"= setequal({{snippet}}, "{{value}}"))' + success: "print('Success!')" + + templates: + default: + - test: "class({{snippet}})" + fail: "type of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "value of {{snippet}} is not correct" + + integer: + - test: "class({{snippet}})" + fail: "type of {{snippet}} is not integer" + + - test: "length({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "sort({{snippet}})" + fail: "values of {{snippet}} are not correct" + + numeric: + - test: "class({{snippet}})" + fail: "type of {{snippet}} is not double" + + - test: "round({{snippet}}, 2)" + fail: "value of {{snippet}} is not correct (rounded to 2 decimal places)" + + - test: "length({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "sort({{snippet}})" + fail: "values of {{snippet}} are not correct" + + list: + - test: "class({{snippet}})" + fail: "type of {{snippet}} is not list" + + - test: "length({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "sort(c(names({{snippet}})))" + fail: "values of {{snippet}} names are not correct" + + - test: "{{snippet}}" + fail: "order of elements of {{snippet}} is not correct" + + character: + - test: "class({{snippet}})" + fail: "type of {{snippet}} is not list" + + - test: "length({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "tolower({{snippet}})" + fail: "value of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "correct string value of {{snippet}} but incorrect case of letters" + + logical: + - test: "class({{snippet}})" + fail: "type of {{snippet}} is not logical" + + - test: "{{snippet}}" + fail: "logical value of {{snippet}} is not correct" diff --git a/nbgrader/preprocessors/__init__.py b/nbgrader/preprocessors/__init__.py index 1507ee0aa..874fc1e69 100644 --- a/nbgrader/preprocessors/__init__.py +++ b/nbgrader/preprocessors/__init__.py @@ -8,6 +8,7 @@ from .overwritecells import OverwriteCells from .checkcellmetadata import CheckCellMetadata from .execute import Execute +from .instantiatetests import InstantiateTests from .getgrades import GetGrades from .clearoutput import ClearOutput from .limitoutput import LimitOutput @@ -28,6 +29,7 @@ "OverwriteCells", "CheckCellMetadata", "Execute", + "InstantiateTests", "GetGrades", "ClearOutput", "LimitOutput", diff --git a/nbgrader/preprocessors/clearsolutions.py b/nbgrader/preprocessors/clearsolutions.py index 03ab3c13f..85a1fb7d2 100644 --- a/nbgrader/preprocessors/clearsolutions.py +++ b/nbgrader/preprocessors/clearsolutions.py @@ -15,6 +15,7 @@ class ClearSolutions(NbGraderPreprocessor): code_stub = Dict( dict(python="# YOUR CODE HERE\nraise NotImplementedError()", + R="# YOUR CODE HERE\nfail()", matlab="% YOUR CODE HERE\nerror('No Answer Given!')", octave="% YOUR CODE HERE\nerror('No Answer Given!')", sas="/* YOUR CODE HERE */\n %notImplemented;", diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py new file mode 100644 index 000000000..c208af9a8 --- /dev/null +++ b/nbgrader/preprocessors/instantiatetests.py @@ -0,0 +1,758 @@ +import os +import yaml +import jinja2 as j2 +import re +from .. import utils +from traitlets import Bool, List, Integer, Unicode, Dict, Callable +from textwrap import dedent +from . import Execute +import secrets +import asyncio +import inspect +import typing as t +from nbformat import NotebookNode +from queue import Empty +import datetime +from typing import Optional +from nbclient.exceptions import ( + CellControlSignal, + CellExecutionComplete, + CellExecutionError, + CellTimeoutError, + DeadKernelError, +) + +try: + from time import monotonic # Py 3 +except ImportError: + from time import time as monotonic # Py 2 + + +######################################################################################### +class CellExecutionComplete(Exception): + """ + Used as a control signal for cell execution across run_cell and + process_message function calls. Raised when all execution requests + are completed and no further messages are expected from the kernel + over zeromq channels. + """ + pass + + +######################################################################################### +class CellExecutionError(Exception): + """ + Custom exception to propagate exceptions that are raised during + notebook execution to the caller. This is mostly useful when + using nbconvert as a library, since it allows dealing with + failures gracefully. + """ + + # ------------------------------------------------------------------------------------- + def __init__(self, traceback): + super(CellExecutionError, self).__init__(traceback) + self.traceback = traceback + + # ------------------------------------------------------------------------------------- + def __str__(self): + s = self.__unicode__() + if not isinstance(s, str): + s = s.encode('utf8', 'replace') + return s + + # ------------------------------------------------------------------------------------- + def __unicode__(self): + return self.traceback + + # ------------------------------------------------------------------------------------- + @classmethod + def from_code_and_msg(cls, code, msg): + """Instantiate from a code cell object and a message contents + (message is either execute_reply or error) + """ + tb = '\n'.join(msg.get('traceback', [])) + return cls(exec_err_msg.format(code=code, traceback=tb)) + # ------------------------------------------------------------------------------------- + + +######################################################################################### +class CodeExecutionError(Exception): + """ + Custom exception to propagate exceptions that are raised during + code snippet execution to the caller. This is mostly useful when + using nbconvert as a library, since it allows dealing with + failures gracefully. + """ + + +######################################################################################### + +exec_err_msg = u"""\ +An error occurred while executing the following code: +------------------ +{code} +------------------ +{traceback} +""" + + +######################################################################################### +class InstantiateTests(Execute): + tests = None + + autotest_filename = Unicode( + "tests.yml", + help="The filename where automatic testing code is stored" + ).tag(config=True) + + autotest_delimiter = Unicode( + "AUTOTEST", + help="The delimiter prior to snippets to be autotested" + ).tag(config=True) + + hashed_delimiter = Unicode( + "HASHED", + help="The delimiter prior to an autotest block if snippet results should be protected by a hash function" + ).tag(config=True) + + use_salt = Bool( + True, + help="Whether to add a salt to digested answers" + ).tag(config=True) + + enforce_metadata = Bool( + True, + help=dedent( + """ + Whether or not to complain if cells containing autotest delimiters + are not marked as grade cells. WARNING: disabling this will potentially cause + things to break if you are using the full nbgrader pipeline. ONLY + disable this option if you are only ever planning to use nbgrader + assign. + """ + ) + ).tag(config=True) + + comment_strs = Dict( + key_trait=Unicode(), + value_trait=Unicode(), + default_value={ + 'ir': '#', + 'python': '#', + 'python3': '#' + }, + help=dedent( + """ + A dictionary mapping each Jupyter kernel's name to the comment string for that kernel. + For an example, one of the entries in this dictionary is "python" : "#", because # is the comment + character in python. + """ + ) + ).tag(config=True) + + sanitizers = Dict( + key_trait=Unicode(), + value_trait=Callable(), + default_value={ + 'ir': lambda s: re.sub(r'\[\d+\]\s+', '', s).strip('"').strip("'"), + 'python': lambda s: s.strip('"').strip("'"), + 'python3': lambda s: s.strip('"').strip("'") + }, + help=dedent( + """ + A dictionary mapping each Jupyter kernel's name to the function that is used to + sanitize the output from the kernel within InstantiateTests. + """ + ) + ).tag(config=True) + + sanitizer = None + global_tests_loaded = False + + def preprocess(self, nb, resources): + # avoid starting the kernel at all/processing the notebook if there are no autotest delimiters + for index, cell in enumerate(nb.cells): + # ignore non-code cells + if cell.cell_type != 'code': + continue + # look for an autotest delimiter in this cell's source; if we find one, process this notebook + if self.autotest_delimiter in cell.source: + nb, resources = super(InstantiateTests, self).preprocess(nb, resources) + return nb, resources + # if not, just return + return nb, resources + + def preprocess_cell(self, cell, resources, index): + # new_lines will store the replacement code after autotest template instantiation + new_lines = [] + + # first, run the cell normally + # cell, resources = super(InstantiateTests, self).preprocess_cell(cell, resources, index) + + kernel_name = self.nb.metadata.get("kernelspec", {}).get("name", "") + if kernel_name not in self.comment_strs: + raise ValueError( + "kernel '{}' has not been specified in " + "InstantiateTests.comment_strs".format(kernel_name)) + resources["kernel_name"] = kernel_name + + # if it's not a code cell, or it's empty, just return + if cell.cell_type != 'code': + return cell, resources + + # determine whether the cell is a grade cell + is_grade_flag = utils.is_grade(cell) + + # get the comment string for this language + comment_str = self.comment_strs[resources['kernel_name']] + + # split the code lines into separate strings + lines = cell.source.split("\n") + + setup_code_inserted_into_cell = False + + non_autotest_code_lines = [] + + if self.sanitizer is None: + self.log.debug('Setting sanitizer for language ' + resources['kernel_name']) + self.sanitizer = self.sanitizers.get(resources['kernel_name'], lambda x: x) + + for line in lines: + + # if the current line doesn't have the autotest_delimiter or is not a comment + # then just append the line to the new cell code and go to the next line + if self.autotest_delimiter not in line or line.strip()[:len(comment_str)] != comment_str: + new_lines.append(line) + non_autotest_code_lines.append(line) + continue + + # run all code lines prior to the current line containing the autotest_delimiter + asyncio.run(self._async_execute_code_snippet("\n".join(non_autotest_code_lines))) + non_autotest_code_lines = [] + + # there are autotests; we should check that it is a grading cell + if not is_grade_flag: + if not self.enforce_metadata: + self.log.warning( + "Autotest region detected in a non-grade cell; " + "please make sure all autotest regions are within " + "'Autograder tests' cells." + ) + else: + self.log.error( + "Autotest region detected in a non-grade cell; " + "please make sure all autotest regions are within " + "'Autograder tests' cells." + ) + raise Exception + + self.log.debug('') + self.log.debug('') + self.log.debug('Autotest delimiter found on line. Preprocessing...') + + # the first time we run into an autotest delimiter, obtain the + # tests object from the tests.yml template file for the assignment + # and append any setup code to the cell block we're in + # also figure out what language we're using + + # loading the template tests file + if not self.global_tests_loaded: + self.log.debug('Loading tests template file') + self._load_test_template_file(resources) + self.global_tests_loaded = True + + # if the setup_code is successfully obtained from the template file and + # the current cell does not already have the setup code, add the setup_code + if (self.setup_code is not None) and (not setup_code_inserted_into_cell): + new_lines.append(self.setup_code) + setup_code_inserted_into_cell = True + asyncio.run(self._async_execute_code_snippet(self.setup_code)) + + # decide whether to use hashing based on whether the self.hashed_delimiter token + # appears in the line before the self.autotest_delimiter token + use_hash = (self.hashed_delimiter in line[:line.find(self.autotest_delimiter)]) + if use_hash: + self.log.debug('Hashing delimiter found, using template: ' + self.hash_template) + else: + self.log.debug('Hashing delimiter not found') + + # take everything after the autotest_delimiter as code snippets separated by semicolons + snippets = [snip.strip() for snip in + line[line.find(self.autotest_delimiter) + len(self.autotest_delimiter):].strip(';').split(';')] + + # remove empty snippets + if '' in snippets: + snippets.remove('') + + # print autotest snippets to log + self.log.debug('Found snippets to autotest: ') + for snippet in snippets: + self.log.debug(snippet) + + # generate the test for each snippet + for snippet in snippets: + self.log.debug('Running autotest generation for snippet ' + snippet) + + # create a random salt for this test + if use_hash: + salt = secrets.token_hex(8) + self.log.debug('Using salt: ' + salt) + else: + salt = None + + # get the normalized(/hashed) template tests for this code snippet + self.log.debug( + 'Instantiating normalized' + ('/hashed ' if use_hash else ' ') + 'test templates based on type') + instantiated_tests, test_values, fail_messages = self._instantiate_tests(snippet, salt) + + # add all the lines to the cell + self.log.debug('Inserting test code into cell') + template = j2.Environment(loader=j2.BaseLoader).from_string(self.check_template) + for i in range(len(instantiated_tests)): + check_code = template.render(snippet=instantiated_tests[i], value=test_values[i], + message=fail_messages[i]) + self.log.debug('Test: ' + check_code) + new_lines.append(check_code) + + # add an empty line after this block of test code + new_lines.append('') + + # run the trailing non-autotest lines, if any remain + if len(non_autotest_code_lines) > 0: + asyncio.run(self._async_execute_code_snippet("\n".join(non_autotest_code_lines))) + + # add the final success message + if is_grade_flag and self.global_tests_loaded: + if self.autotest_delimiter in cell.source: + new_lines.append(self.success_code) + + # replace the cell source + cell.source = "\n".join(new_lines) + + # remove the execution metainfo + cell.pop('execution', None) + + return cell, resources + + # ------------------------------------------------------------------------------------- + def _load_test_template_file(self, resources): + """ + attempts to load the tests.yml file within the assignment directory. In case such file is not found + or perhaps cannot be loaded, it will attempt to load the default_tests.yaml file with the course_directory + """ + self.log.debug('loading template tests.yml...') + self.log.debug('kernel_name: ' + resources["kernel_name"]) + try: + with open(os.path.join(resources['metadata']['path'], self.autotest_filename), 'r') as tests_file: + tests = yaml.safe_load(tests_file) + self.log.debug(tests) + + except FileNotFoundError: + # if there is no tests file, just load a default tests dict + self.log.warning( + 'No tests.yml file found in the assignment directory. Loading the default tests.yml file in the course root directory') + # tests = {} + try: + with open(os.path.join(self.autotest_filename), 'r') as tests_file: + tests = yaml.safe_load(tests_file) + except FileNotFoundError: + # if there is no tests file, just create a default empty tests dict + self.log.warning( + 'No tests.yml file found. If AUTOTESTS appears in testing cells, an error will be thrown.') + tests = {} + except yaml.parser.ParserError as e: + self.log.error('tests.yml contains invalid YAML code.') + self.log.error(e.msg) + raise + + except yaml.parser.ParserError as e: + self.log.error('tests.yml contains invalid YAML code.') + self.log.error(e.msg) + raise + + # get kernel specific data + tests = tests[resources["kernel_name"]] + + # get the test templates + self.test_templates_by_type = tests['templates'] + + # get the test dispatch code template + self.dispatch_template = tests['dispatch'] + + # get the success message template + self.success_code = tests['success'] + + # get the hash code template + self.hash_template = tests['hash'] + + # get the hash code template + self.check_template = tests['check'] + + # get the hash code template + self.normalize_template = tests['normalize'] + + # get the setup code if it's there + self.setup_code = tests.get('setup', None) + + # ------------------------------------------------------------------------------------- + def _instantiate_tests(self, snippet, salt=None): + # get the type of the snippet output (used to dispatch autotest) + template = j2.Environment(loader=j2.BaseLoader).from_string(self.dispatch_template) + dispatch_code = template.render(snippet=snippet) + dispatch_result = asyncio.run(self._async_execute_code_snippet(dispatch_code)) + self.log.debug('Dispatch result returned by kernel: ', dispatch_result) + # get the test code; if the type isn't in our dict, just default to 'default' + # if default isn't in the tests code, this will throw an error + try: + tests = self.test_templates_by_type.get(dispatch_result, self.test_templates_by_type['default']) + except KeyError: + self.log.error('tests.yml must contain a top-level "default" key with corresponding test code') + raise + try: + test_templs = [t['test'] for t in tests] + fail_msgs = [t['fail'] for t in tests] + except KeyError: + self.log.error('each type in tests.yml must have a list of dictionaries with a "test" and "fail" key') + self.log.error('the "test" item should store the test template code, ' + 'and the "fail" item should store a failure message') + raise + + # + rendered_fail_msgs = [] + for templ in fail_msgs: + template = j2.Environment(loader=j2.BaseLoader).from_string(templ) + fmsg = template.render(snippet=snippet) + # escape double quotes + fmsg = fmsg.replace("\"", "\\\"") + rendered_fail_msgs.append(fmsg) + + # normalize the templates + normalized_templs = [] + for templ in test_templs: + template = j2.Environment(loader=j2.BaseLoader).from_string(self.normalize_template) + normalized_templs.append(template.render(snippet=templ)) + + # hashify the templates + processed_templs = [] + if salt is not None: + for templ in normalized_templs: + template = j2.Environment(loader=j2.BaseLoader).from_string(self.hash_template) + processed_templs.append(template.render(snippet=templ, salt=salt)) + else: + processed_templs = normalized_templs + + # instantiate and evaluate the tests + instantiated_tests = [] + test_values = [] + for templ in processed_templs: + # instantiate the template snippet + template = j2.Environment(loader=j2.BaseLoader).from_string(templ) + instantiated_test = template.render(snippet=snippet) + # run the instantiated template code + test_value = asyncio.run(self._async_execute_code_snippet(instantiated_test)) + instantiated_tests.append(instantiated_test) + test_values.append(test_value) + + return instantiated_tests, test_values, rendered_fail_msgs + + # ------------------------------------------------------------------------------------- + + ######################### + # async version of nbgrader interaction with kernel + # the below functions were adapted from the jupyter/nbclient GitHub repo, commit: + # https://github.com/jupyter/nbclient/commit/0c08e27c1ec655cffe9b35cf637da742cdab36e8 + ######################### + + # ------------------------------------------------------------------------------------- + # adapted from nbclient.util.ensure_async + async def _ensure_async(self, obj): + """Convert a non-awaitable object to a coroutine if needed, + and await it if it was not already awaited. + adapted from nbclient.util._ensure_async + """ + if inspect.isawaitable(obj): + try: + result = await obj + except RuntimeError as e: + if str(e) == 'cannot reuse already awaited coroutine': + return obj + raise + return result + return obj + + # ------------------------------------------------------------------------------------- + # adapted from nbclient.client._async_handle_timeout + async def _async_handle_timeout(self, timeout: int) -> None: + + self.log.error("Timeout waiting for execute reply (%is)." % timeout) + if self.interrupt_on_timeout: + self.log.error("Interrupting kernel") + assert self.km is not None + await _ensure_async(self.km.interrupt_kernel()) + else: + raise CellTimeoutError.error_from_timeout_and_cell( + "Cell execution timed out", timeout + ) + + # ------------------------------------------------------------------------------------- + # adapted from nbclient.client._async_check_alive + async def _async_check_alive(self) -> None: + assert self.kc is not None + if not await self._ensure_async(self.kc.is_alive()): + self.log.error("Kernel died while waiting for execute reply.") + raise DeadKernelError("Kernel died") + + # ------------------------------------------------------------------------------------- + # adapted from nbclient.client._async_poll_output_msg + async def _async_poll_output_msg_code( + self, parent_msg_id: str, code + ) -> None: + + assert self.kc is not None + while True: + msg = await self._ensure_async(self.kc.iopub_channel.get_msg(timeout=None)) + if msg['parent_header'].get('msg_id') == parent_msg_id: + try: + msg_type = msg['msg_type'] + self.log.debug("msg_type: %s", msg_type) + content = msg['content'] + self.log.debug("content: %s", content) + + if msg_type in {'execute_result', 'display_data', 'update_display_data'}: + return self.sanitizer(content['data']['text/plain']) + + if msg_type == 'error': + self.log.error("Failed to run code: \n%s", code) + self.log.error("Runtime error from the kernel: \n%s", content['evalue']) + raise CodeExecutionError() + + if msg_type == 'status': + if content['execution_state'] == 'idle': + raise CellExecutionComplete() + + except CellExecutionComplete: + return + + # ------------------------------------------------------------------------------------- + # adapted from nbclient.client.async_wait_for_reply + async def _async_wait_for_reply( + self, msg_id: str, cell: t.Optional[NotebookNode] = None + ) -> t.Optional[t.Dict]: + + assert self.kc is not None + # wait for finish, with timeout + timeout = self._get_timeout(cell) + cummulative_time = 0 + while True: + try: + msg = await _ensure_async( + self.kc.shell_channel.get_msg(timeout=self.shell_timeout_interval) + ) + except Empty: + await self._async_check_alive() + cummulative_time += self.shell_timeout_interval + if timeout and cummulative_time > timeout: + await self._async_async_handle_timeout(timeout, cell) + break + else: + if msg['parent_header'].get('msg_id') == msg_id: + return msg + return None + + # ------------------------------------------------------------------------------------- + # adapted from nbclient.client._async_poll_for_reply + async def _async_poll_for_reply_code( + self, + msg_id: str, + timeout: t.Optional[int], + task_poll_output_msg: asyncio.Future, + task_poll_kernel_alive: asyncio.Future, + ) -> t.Dict: + + assert self.kc is not None + + self.log.debug("Executing _async_poll_for_reply:\n%s", msg_id) + + if timeout is not None: + deadline = monotonic() + timeout + new_timeout = float(timeout) + + while True: + try: + shell_msg = await self._ensure_async(self.kc.shell_channel.get_msg(timeout=new_timeout)) + if shell_msg['parent_header'].get('msg_id') == msg_id: + try: + msg = await asyncio.wait_for(task_poll_output_msg, new_timeout) + except (asyncio.TimeoutError, Empty): + task_poll_kernel_alive.cancel() + raise CellExecutionError("Timeout waiting for IOPub output") + self.log.debug("Get _async_poll_for_reply:\n%s", msg) + + return msg if msg != None else "" + else: + if new_timeout is not None: + new_timeout = max(0, deadline - monotonic()) + except Empty: + self.log.debug("Empty _async_poll_for_reply:\n%s", msg_id) + task_poll_kernel_alive.cancel() + await self._async_check_alive() + await self._async_handle_timeout() + + # ------------------------------------------------------------------------------------- + # adapted from nbclient.client.async_execute_cell + async def _async_execute_code_snippet(self, code): + assert self.kc is not None + + self.log.debug("Executing cell:\n%s", code) + + parent_msg_id = await self._ensure_async(self.kc.execute(code, stop_on_error=not self.allow_errors)) + + task_poll_kernel_alive = asyncio.ensure_future(self._async_check_alive()) + + task_poll_output_msg = asyncio.ensure_future(self._async_poll_output_msg_code(parent_msg_id, code)) + + task_poll_for_reply = asyncio.ensure_future( + self._async_poll_for_reply_code(parent_msg_id, self.timeout, task_poll_output_msg, task_poll_kernel_alive)) + + try: + msg = await task_poll_for_reply + except asyncio.CancelledError: + # can only be cancelled by task_poll_kernel_alive when the kernel is dead + task_poll_output_msg.cancel() + raise DeadKernelError("Kernel died") + except Exception as e: + # Best effort to cancel request if it hasn't been resolved + try: + # Check if the task_poll_output is doing the raising for us + if not isinstance(e, CellControlSignal): + task_poll_output_msg.cancel() + finally: + raise + + return msg + + # ------------------------------------------------------------------------------------- + async def async_execute_cell( + self, + cell: NotebookNode, + cell_index: int, + execution_count: t.Optional[int] = None, + store_history: bool = True, + ) -> NotebookNode: + """ + Executes a single code cell. + + To execute all cells see :meth:`execute`. + + Parameters + ---------- + cell : nbformat.NotebookNode + The cell which is currently being processed. + cell_index : int + The position of the cell within the notebook object. + execution_count : int + The execution count to be assigned to the cell (default: Use kernel response) + store_history : bool + Determines if history should be stored in the kernel (default: False). + Specific to ipython kernels, which can store command histories. + + Returns + ------- + output : dict + The execution output payload (or None for no output). + + Raises + ------ + CellExecutionError + If execution failed and should raise an exception, this will be raised + with defaults about the failure. + + Returns + ------- + cell : NotebookNode + The cell which was just processed. + """ + assert self.kc is not None + + await run_hook(self.on_cell_start, cell=cell, cell_index=cell_index) + + if cell.cell_type != 'code' or not cell.source.strip(): + self.log.debug("Skipping non-executing cell %s", cell_index) + return cell + + if self.skip_cells_with_tag in cell.metadata.get("tags", []): + self.log.debug("Skipping tagged cell %s", cell_index) + return cell + + if self.record_timing: # clear execution metadata prior to execution + cell['metadata']['execution'] = {} + + self.log.debug("Executing cell:\n%s", cell.source) + + cell_allows_errors = (not self.force_raise_errors) and ( + self.allow_errors or "raises-exception" in cell.metadata.get("tags", []) + ) + + await run_hook(self.on_cell_execute, cell=cell, cell_index=cell_index) + parent_msg_id = await _ensure_async( + self.kc.execute( + cell.source, store_history=store_history, stop_on_error=not cell_allows_errors + ) + ) + await run_hook(self.on_cell_complete, cell=cell, cell_index=cell_index) + # We launched a code cell to execute + self.code_cells_executed += 1 + exec_timeout = self._get_timeout(cell) + + cell.outputs = [] + self.clear_before_next_output = False + + task_poll_kernel_alive = asyncio.ensure_future(self._async_poll_kernel_alive()) + task_poll_output_msg = asyncio.ensure_future( + self._async_poll_output_msg_code(parent_msg_id, code) + ) + self.task_poll_for_reply = asyncio.ensure_future( + self._async_poll_for_reply_code( + parent_msg_id, exec_timeout, task_poll_output_msg, task_poll_kernel_alive + ) + ) + try: + exec_reply = await self.task_poll_for_reply + except asyncio.CancelledError: + # can only be cancelled by task_poll_kernel_alive when the kernel is dead + task_poll_output_msg.cancel() + raise DeadKernelError("Kernel died") + except Exception as e: + # Best effort to cancel request if it hasn't been resolved + try: + # Check if the task_poll_output is doing the raising for us + if not isinstance(e, CellControlSignal): + task_poll_output_msg.cancel() + finally: + raise + + if execution_count: + cell['execution_count'] = execution_count + await self._check_raise_for_error(cell, cell_index, exec_reply) + self.nb['cells'][cell_index] = cell + return cell + # ------------------------------------------------------------------------------------- + + +def timestamp(msg: Optional[Dict] = None) -> str: + if msg and 'header' in msg: # The test mocks don't provide a header, so tolerate that + msg_header = msg['header'] + if 'date' in msg_header and isinstance(msg_header['date'], datetime.datetime): + try: + # reformat datetime into expected format + formatted_time = datetime.datetime.strftime( + msg_header['date'], '%Y-%m-%dT%H:%M:%S.%fZ' + ) + if ( + formatted_time + ): # docs indicate strftime may return empty string, so let's catch that too + return formatted_time + except Exception: + pass # fallback to a local time + + return datetime.datetime.utcnow().isoformat() + 'Z' diff --git a/nbgrader/tests/__init__.py b/nbgrader/tests/__init__.py index 5f57d10ff..82992b1ba 100644 --- a/nbgrader/tests/__init__.py +++ b/nbgrader/tests/__init__.py @@ -251,3 +251,18 @@ def get_free_ports(n): for s in sockets: s.close() return ports + +def create_autotest_solution_cell(): + source = """ + answer = 'answer' + """ + cell = new_code_cell(source=source) + return cell + + +def create_autotest_test_cell(): + source = """ + ### AUTOTEST answer + """ + cell = new_code_cell(source=source) + return cell diff --git a/nbgrader/tests/apps/files/autotest-hashed-changed.ipynb b/nbgrader/tests/apps/files/autotest-hashed-changed.ipynb new file mode 100644 index 000000000..8ddbb8633 --- /dev/null +++ b/nbgrader/tests/apps/files/autotest-hashed-changed.ipynb @@ -0,0 +1,112 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "nbgrader": { + "cell_type": "code", + "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", + "grade": false, + "grade_id": "soln", + "locked": false, + "schema_version": 3, + "solution": true + } + }, + "outputs": [], + "source": [ + "# YOUR CODE HERE\n", + "a = 5\n", + "b = \"hello\"\n", + "c = [1, 2, \"test\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "editable": false, + "max_height": 100, + "nbgrader": { + "cell_type": "code", + "checksum": "a0a263f3cd77437ecaaaa68ffd10ca2f", + "grade": true, + "grade_id": "test_hashed", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + } + }, + "outputs": [], + "source": [ + "from hashlib import sha1\n", + "assert sha1(str(type(a)).encode(\"utf-8\")+b\"9231f34cdc3da9ea\").hexdigest() == \"659113e142e34b819add5d3c95d33a1cf10a09ae\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert sha1(str(a).encode(\"utf-8\")+b\"9231f34cdc3da9ea\").hexdigest() == \"455510a3efa0017fa35841cd77fb1554ee665d0a\", \"value of a is not correct\"\n", + "\n", + "assert sha1(str(type(b)).encode(\"utf-8\")+b\"13937391b113e128\").hexdigest() == \"a7a6d540df4104f3adb629c5c3ea3c7c461eaa6f\", \"type of b is not str. b should be an str\"\n", + "assert sha1(str(len(b)).encode(\"utf-8\")+b\"13937391b113e128\").hexdigest() == \"b8eda0d698f28aedc80f0f318c35af3447c18f29\", \"length of b is not correct\"\n", + "assert sha1(str(b.lower()).encode(\"utf-8\")+b\"13937391b113e128\").hexdigest() == \"2755a4e47f3b60f679787659f804488276ec718e\", \"value of b is not correct\"\n", + "assert sha1(str(b).encode(\"utf-8\")+b\"13937391b113e128\").hexdigest() == \"2755a4e47f3b60f679787659f804488276ec718e\", \"correct string value of b but incorrect case of letters\"\n", + "\n", + "assert sha1(str(type(c)).encode(\"utf-8\")+b\"e33308cee3b48f12\").hexdigest() == \"2da3504fe36c50ab1d44ffb61bd4a02af8f0fdbd\", \"type of c is not list. c should be a list\"\n", + "assert sha1(str(len(c)).encode(\"utf-8\")+b\"e33308cee3b48f12\").hexdigest() == \"5f8b7c79f0b7ee5fafa410947a40589acbf2b644\", \"length of c is not correct\"\n", + "assert sha1(str(sorted(map(str, c))).encode(\"utf-8\")+b\"e33308cee3b48f12\").hexdigest() == \"10c943e426df5e88a394da50cc9cc07f6cb2fa33\", \"values of c are not correct\"\n", + "assert sha1(str(c).encode(\"utf-8\")+b\"e33308cee3b48f12\").hexdigest() == \"c538ca013d170994051b3d55fb2c488d2fadb776\", \"order of elements of c is not correct\"\n", + "\n", + "\n", + "# differing spacings, num comment characters, trailing whitespace\n", + "assert sha1(str(type(a)).encode(\"utf-8\")+b\"cfe4dae897378d1a\").hexdigest() == \"463fc87d8b90bcf558e04b5bff454d0fe10f0447\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert sha1(str(a).encode(\"utf-8\")+b\"cfe4dae897378d1a\").hexdigest() == \"586e477f328cc5e3c852148517751d072d36afee\", \"value of a is not correct\"\n", + "\n", + "assert sha1(str(type(a)).encode(\"utf-8\")+b\"ec2be017656b8d95\").hexdigest() == \"2abb3c781a9ed14b1dec5a00736dc170f3301ce2\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert sha1(str(a).encode(\"utf-8\")+b\"ec2be017656b8d95\").hexdigest() == \"b86b5db1c86084688de67ed5b3e2001206365a44\", \"value of a is not correct\"\n", + "\n", + "assert sha1(str(type(a)).encode(\"utf-8\")+b\"da663014407e8b68\").hexdigest() == \"4c5e40a4f0ec62a3dbf5232e9d22faba40b1d394\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert sha1(str(a).encode(\"utf-8\")+b\"da663014407e8b68\").hexdigest() == \"419c839a51a681a8fe1bd658d02c285ab657a1ef\", \"value of a is not correct\"\n", + "\n", + "assert sha1(str(type(a)).encode(\"utf-8\")+b\"f97fab4e29c2f008\").hexdigest() == \"21248e96318cac95a37157dcded2b3e9b3778668\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert sha1(str(a).encode(\"utf-8\")+b\"f97fab4e29c2f008\").hexdigest() == \"0e27de567679deef32bd63ebd416cb835db76ba4\", \"value of a is not correct\"\n", + "\n", + "assert sha1(str(type(a)).encode(\"utf-8\")+b\"72eea5ecf1bd1e19\").hexdigest() == \"6008755168651743936153fa68a21c1c4369b61a\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert sha1(str(a).encode(\"utf-8\")+b\"72eea5ecf1bd1e19\").hexdigest() == \"b5d66cbe4aaaaf8a45e1ceef0e2e03f70fcefa59\", \"value of a is not correct\"\n", + "\n", + "assert sha1(str(type(a)).encode(\"utf-8\")+b\"93205c8cd03cafd5\").hexdigest() == \"2292f651355b47a594a97fe2e04253414aefe4bd\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert sha1(str(a).encode(\"utf-8\")+b\"93205c8cd03cafd5\").hexdigest() == \"f5eba7f0391b94b32a57691d8f982659d50fbf9f\", \"value of a is not correct\"\n", + "\n", + "print('Success!')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbgrader/tests/apps/files/autotest-hashed-unchanged.ipynb b/nbgrader/tests/apps/files/autotest-hashed-unchanged.ipynb new file mode 100644 index 000000000..86f485097 --- /dev/null +++ b/nbgrader/tests/apps/files/autotest-hashed-unchanged.ipynb @@ -0,0 +1,110 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "nbgrader": { + "cell_type": "code", + "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", + "grade": false, + "grade_id": "soln", + "locked": false, + "schema_version": 3, + "solution": true + } + }, + "outputs": [], + "source": [ + "# YOUR CODE HERE\n", + "raise NotImplementedError()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "editable": false, + "max_height": 100, + "nbgrader": { + "cell_type": "code", + "checksum": "a0a263f3cd77437ecaaaa68ffd10ca2f", + "grade": true, + "grade_id": "test_hashed", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + } + }, + "outputs": [], + "source": [ + "from hashlib import sha1\n", + "assert sha1(str(type(a)).encode(\"utf-8\")+b\"9231f34cdc3da9ea\").hexdigest() == \"659113e142e34b819add5d3c95d33a1cf10a09ae\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert sha1(str(a).encode(\"utf-8\")+b\"9231f34cdc3da9ea\").hexdigest() == \"455510a3efa0017fa35841cd77fb1554ee665d0a\", \"value of a is not correct\"\n", + "\n", + "assert sha1(str(type(b)).encode(\"utf-8\")+b\"13937391b113e128\").hexdigest() == \"a7a6d540df4104f3adb629c5c3ea3c7c461eaa6f\", \"type of b is not str. b should be an str\"\n", + "assert sha1(str(len(b)).encode(\"utf-8\")+b\"13937391b113e128\").hexdigest() == \"b8eda0d698f28aedc80f0f318c35af3447c18f29\", \"length of b is not correct\"\n", + "assert sha1(str(b.lower()).encode(\"utf-8\")+b\"13937391b113e128\").hexdigest() == \"2755a4e47f3b60f679787659f804488276ec718e\", \"value of b is not correct\"\n", + "assert sha1(str(b).encode(\"utf-8\")+b\"13937391b113e128\").hexdigest() == \"2755a4e47f3b60f679787659f804488276ec718e\", \"correct string value of b but incorrect case of letters\"\n", + "\n", + "assert sha1(str(type(c)).encode(\"utf-8\")+b\"e33308cee3b48f12\").hexdigest() == \"2da3504fe36c50ab1d44ffb61bd4a02af8f0fdbd\", \"type of c is not list. c should be a list\"\n", + "assert sha1(str(len(c)).encode(\"utf-8\")+b\"e33308cee3b48f12\").hexdigest() == \"5f8b7c79f0b7ee5fafa410947a40589acbf2b644\", \"length of c is not correct\"\n", + "assert sha1(str(sorted(map(str, c))).encode(\"utf-8\")+b\"e33308cee3b48f12\").hexdigest() == \"10c943e426df5e88a394da50cc9cc07f6cb2fa33\", \"values of c are not correct\"\n", + "assert sha1(str(c).encode(\"utf-8\")+b\"e33308cee3b48f12\").hexdigest() == \"c538ca013d170994051b3d55fb2c488d2fadb776\", \"order of elements of c is not correct\"\n", + "\n", + "\n", + "# differing spacings, num comment characters, trailing whitespace\n", + "assert sha1(str(type(a)).encode(\"utf-8\")+b\"cfe4dae897378d1a\").hexdigest() == \"463fc87d8b90bcf558e04b5bff454d0fe10f0447\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert sha1(str(a).encode(\"utf-8\")+b\"cfe4dae897378d1a\").hexdigest() == \"586e477f328cc5e3c852148517751d072d36afee\", \"value of a is not correct\"\n", + "\n", + "assert sha1(str(type(a)).encode(\"utf-8\")+b\"ec2be017656b8d95\").hexdigest() == \"2abb3c781a9ed14b1dec5a00736dc170f3301ce2\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert sha1(str(a).encode(\"utf-8\")+b\"ec2be017656b8d95\").hexdigest() == \"b86b5db1c86084688de67ed5b3e2001206365a44\", \"value of a is not correct\"\n", + "\n", + "assert sha1(str(type(a)).encode(\"utf-8\")+b\"da663014407e8b68\").hexdigest() == \"4c5e40a4f0ec62a3dbf5232e9d22faba40b1d394\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert sha1(str(a).encode(\"utf-8\")+b\"da663014407e8b68\").hexdigest() == \"419c839a51a681a8fe1bd658d02c285ab657a1ef\", \"value of a is not correct\"\n", + "\n", + "assert sha1(str(type(a)).encode(\"utf-8\")+b\"f97fab4e29c2f008\").hexdigest() == \"21248e96318cac95a37157dcded2b3e9b3778668\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert sha1(str(a).encode(\"utf-8\")+b\"f97fab4e29c2f008\").hexdigest() == \"0e27de567679deef32bd63ebd416cb835db76ba4\", \"value of a is not correct\"\n", + "\n", + "assert sha1(str(type(a)).encode(\"utf-8\")+b\"72eea5ecf1bd1e19\").hexdigest() == \"6008755168651743936153fa68a21c1c4369b61a\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert sha1(str(a).encode(\"utf-8\")+b\"72eea5ecf1bd1e19\").hexdigest() == \"b5d66cbe4aaaaf8a45e1ceef0e2e03f70fcefa59\", \"value of a is not correct\"\n", + "\n", + "assert sha1(str(type(a)).encode(\"utf-8\")+b\"93205c8cd03cafd5\").hexdigest() == \"2292f651355b47a594a97fe2e04253414aefe4bd\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert sha1(str(a).encode(\"utf-8\")+b\"93205c8cd03cafd5\").hexdigest() == \"f5eba7f0391b94b32a57691d8f982659d50fbf9f\", \"value of a is not correct\"\n", + "\n", + "print('Success!')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbgrader/tests/apps/files/autotest-hashed.ipynb b/nbgrader/tests/apps/files/autotest-hashed.ipynb new file mode 100644 index 000000000..1b4df7b83 --- /dev/null +++ b/nbgrader/tests/apps/files/autotest-hashed.ipynb @@ -0,0 +1,82 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "soln", + "locked": false, + "schema_version": 3, + "solution": true + } + }, + "outputs": [], + "source": [ + "### BEGIN SOLUTION\n", + "a = 5\n", + "b = \"hello\"\n", + "c = [1, 2, \"test\"]\n", + "### END SOLUTION" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "test_hashed", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + } + }, + "outputs": [], + "source": [ + "### HASHED AUTOTEST a\n", + "### HASHED AUTOTEST b\n", + "### HASHED AUTOTEST c\n", + "\n", + "# differing spacings, num comment characters, trailing whitespace\n", + "### HASHED AUTOTEST a \n", + "#HASHED AUTOTEST a\n", + "# HASHED AUTOTEST a\n", + "## HASHED AUTOTEST a\n", + "# HASHED AUTOTEST a\n", + "# # # # HASHED AUTOTEST a" + ] + } + ], + "metadata": { + "celltoolbar": "Create Assignment", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbgrader/tests/apps/files/autotest-hidden-changed-right.ipynb b/nbgrader/tests/apps/files/autotest-hidden-changed-right.ipynb new file mode 100644 index 000000000..d17fa7735 --- /dev/null +++ b/nbgrader/tests/apps/files/autotest-hidden-changed-right.ipynb @@ -0,0 +1,85 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "nbgrader": { + "cell_type": "code", + "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", + "grade": false, + "grade_id": "soln", + "locked": false, + "schema_version": 3, + "solution": true + } + }, + "outputs": [], + "source": [ + "# YOUR CODE HERE\n", + "a = 5\n", + "b = \"hello\"\n", + "c = [1, 2, \"test\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "editable": false, + "max_height": 100, + "nbgrader": { + "cell_type": "code", + "checksum": "2fd15c162690346636dd1f2881f6b31e", + "grade": true, + "grade_id": "test_hidden", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + } + }, + "outputs": [], + "source": [ + "\n", + "assert str(type(a)) == \"\", \"type of type(a) is not correct\"\n", + "\n", + "assert str(type(b)) == \"\", \"type of type(b) is not correct\"\n", + "\n", + "assert str(type(c)) == \"\", \"type of type(c) is not correct\"\n", + "\n", + "print('Success!')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbgrader/tests/apps/files/autotest-hidden-changed-wrong.ipynb b/nbgrader/tests/apps/files/autotest-hidden-changed-wrong.ipynb new file mode 100644 index 000000000..e84452ef5 --- /dev/null +++ b/nbgrader/tests/apps/files/autotest-hidden-changed-wrong.ipynb @@ -0,0 +1,85 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "nbgrader": { + "cell_type": "code", + "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", + "grade": false, + "grade_id": "soln", + "locked": false, + "schema_version": 3, + "solution": true + } + }, + "outputs": [], + "source": [ + "# YOUR CODE HERE\n", + "a = 7\n", + "b = \"notright\"\n", + "c = [3, 4, \"hi\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "editable": false, + "max_height": 100, + "nbgrader": { + "cell_type": "code", + "checksum": "2fd15c162690346636dd1f2881f6b31e", + "grade": true, + "grade_id": "test_hidden", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + } + }, + "outputs": [], + "source": [ + "\n", + "assert str(type(a)) == \"\", \"type of type(a) is not correct\"\n", + "\n", + "assert str(type(b)) == \"\", \"type of type(b) is not correct\"\n", + "\n", + "assert str(type(c)) == \"\", \"type of type(c) is not correct\"\n", + "\n", + "print('Success!')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbgrader/tests/apps/files/autotest-hidden-unchanged.ipynb b/nbgrader/tests/apps/files/autotest-hidden-unchanged.ipynb new file mode 100644 index 000000000..be153756b --- /dev/null +++ b/nbgrader/tests/apps/files/autotest-hidden-unchanged.ipynb @@ -0,0 +1,83 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "nbgrader": { + "cell_type": "code", + "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", + "grade": false, + "grade_id": "soln", + "locked": false, + "schema_version": 3, + "solution": true + } + }, + "outputs": [], + "source": [ + "# YOUR CODE HERE\n", + "raise NotImplementedError()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "editable": false, + "max_height": 100, + "nbgrader": { + "cell_type": "code", + "checksum": "2fd15c162690346636dd1f2881f6b31e", + "grade": true, + "grade_id": "test_hidden", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + } + }, + "outputs": [], + "source": [ + "\n", + "assert str(type(a)) == \"\", \"type of type(a) is not correct\"\n", + "\n", + "assert str(type(b)) == \"\", \"type of type(b) is not correct\"\n", + "\n", + "assert str(type(c)) == \"\", \"type of type(c) is not correct\"\n", + "\n", + "print('Success!')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbgrader/tests/apps/files/autotest-hidden.ipynb b/nbgrader/tests/apps/files/autotest-hidden.ipynb new file mode 100644 index 000000000..c2e6e8e23 --- /dev/null +++ b/nbgrader/tests/apps/files/autotest-hidden.ipynb @@ -0,0 +1,80 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "soln", + "locked": false, + "schema_version": 3, + "solution": true + } + }, + "outputs": [], + "source": [ + "### BEGIN SOLUTION\n", + "a = 5\n", + "b = \"hello\"\n", + "c = [1, 2, \"test\"]\n", + "### END SOLUTION" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "test_hidden", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + } + }, + "outputs": [], + "source": [ + "### BEGIN HIDDEN TESTS\n", + "### AUTOTEST a\n", + "### AUTOTEST b\n", + "### AUTOTEST c\n", + "### END HIDDEN TESTS\n", + "\n", + "### AUTOTEST type(a)\n", + "### AUTOTEST type(b)\n", + "### AUTOTEST type(c)" + ] + } + ], + "metadata": { + "celltoolbar": "Create Assignment", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbgrader/tests/apps/files/autotest-multi-changed.ipynb b/nbgrader/tests/apps/files/autotest-multi-changed.ipynb new file mode 100644 index 000000000..04470cca4 --- /dev/null +++ b/nbgrader/tests/apps/files/autotest-multi-changed.ipynb @@ -0,0 +1,267 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "nbgrader": { + "cell_type": "code", + "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", + "grade": false, + "grade_id": "soln", + "locked": false, + "schema_version": 3, + "solution": true + } + }, + "outputs": [], + "source": [ + "# YOUR CODE HERE\n", + "a = 5\n", + "b = \"hello\"\n", + "c = [1, 2, \"test\"]\n", + "d = {1 : 6, 3: 5} #right type, wrong value\n", + "e = [5, -3.3, 'd'] #right values, wrong type\n", + "f = True # wrong value\n", + "def fun(x):\n", + " if x < 0:\n", + " raise ValueError\n", + " else:\n", + " return x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "editable": false, + "max_height": 100, + "nbgrader": { + "cell_type": "code", + "checksum": "620860a42fa05b556da01013ef99ed8c", + "grade": true, + "grade_id": "test_multi", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + } + }, + "outputs": [], + "source": [ + "# the basic tests from the simple notebook\n", + "from hashlib import sha1\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "assert str(type(b)) == \"\", \"type of b is not str. b should be an str\"\n", + "assert str(len(b)) == \"5\", \"length of b is not correct\"\n", + "assert str(b.lower()) == \"hello\", \"value of b is not correct\"\n", + "assert str(b) == \"hello\", \"correct string value of b but incorrect case of letters\"\n", + "\n", + "assert str(type(c)) == \"\", \"type of c is not list. c should be a list\"\n", + "assert str(len(c)) == \"3\", \"length of c is not correct\"\n", + "assert str(sorted(map(str, c))) == \"['1', '2', 'test']\", \"values of c are not correct\"\n", + "assert str(c) == \"[1, 2, 'test']\", \"order of elements of c is not correct\"\n", + "\n", + "print('Success!')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "editable": false, + "max_height": 100, + "nbgrader": { + "cell_type": "code", + "checksum": "c46928757ed303ce204ef97299b39f82", + "grade": true, + "grade_id": "cell-f2803ba7c42d03ab", + "locked": true, + "points": 1, + "schema_version": 3, + "solution": false, + "task": false + } + }, + "outputs": [], + "source": [ + "# multiple expressions per line\n", + "from hashlib import sha1\n", + "assert str(type(a)) == \"\", \"type of type(a) is not correct\"\n", + "\n", + "assert str(type(str(a))) == \"\", \"type of str(a) is not str. str(a) should be an str\"\n", + "assert str(len(str(a))) == \"1\", \"length of str(a) is not correct\"\n", + "assert str(str(a).lower()) == \"5\", \"value of str(a) is not correct\"\n", + "assert str(str(a)) == \"5\", \"correct string value of str(a) but incorrect case of letters\"\n", + "\n", + "assert str(type(a == \"5\")) == \"\", \"type of a == \\\"5\\\" is not bool. a == \\\"5\\\" should be a bool\"\n", + "assert str(a == \"5\") == \"False\", \"boolean value of a == \\\"5\\\" is not correct\"\n", + "\n", + "assert str(type([ch for ch in b])) == \"\", \"type of [ch for ch in b] is not list. [ch for ch in b] should be a list\"\n", + "assert str(len([ch for ch in b])) == \"5\", \"length of [ch for ch in b] is not correct\"\n", + "assert str(sorted(map(str, [ch for ch in b]))) == \"['e', 'h', 'l', 'l', 'o']\", \"values of [ch for ch in b] are not correct\"\n", + "assert str([ch for ch in b]) == \"['h', 'e', 'l', 'l', 'o']\", \"order of elements of [ch for ch in b] is not correct\"\n", + "\n", + "assert str(type(len(c))) == \"\", \"type of len(c) is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(len(c)) == \"3\", \"value of len(c) is not correct\"\n", + "\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "\n", + "# intervening regular code\n", + "print(\"hello!\")\n", + "\n", + "# differing spacings, numbers of comment characters, trailing whitespace\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "\n", + "\n", + "print('Success!')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "editable": false, + "max_height": 100, + "nbgrader": { + "cell_type": "code", + "checksum": "2fc8066fae8e7aeb865116c88d1be31a", + "grade": true, + "grade_id": "cell-693350420ec62f1b", + "locked": true, + "points": 1, + "schema_version": 3, + "solution": false, + "task": false + } + }, + "outputs": [], + "source": [ + "# a few common types\n", + "from hashlib import sha1\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "assert str(type(b)) == \"\", \"type of b is not str. b should be an str\"\n", + "assert str(len(b)) == \"5\", \"length of b is not correct\"\n", + "assert str(b.lower()) == \"hello\", \"value of b is not correct\"\n", + "assert str(b) == \"hello\", \"correct string value of b but incorrect case of letters\"\n", + "\n", + "assert str(type(c)) == \"\", \"type of c is not list. c should be a list\"\n", + "assert str(len(c)) == \"3\", \"length of c is not correct\"\n", + "assert str(sorted(map(str, c))) == \"['1', '2', 'test']\", \"values of c are not correct\"\n", + "assert str(c) == \"[1, 2, 'test']\", \"order of elements of c is not correct\"\n", + "\n", + "assert str(type(d)) == \"\", \"type of d is not dict. d should be a dict\"\n", + "assert str(len(list(d.keys()))) == \"2\", \"number of keys of d is not correct\"\n", + "assert str(sorted(map(str, d.keys()))) == \"['3', 'a']\", \"keys of d are not correct\"\n", + "assert str(sorted(map(str, d.values()))) == \"['[1.2, 3]', 'f']\", \"correct keys, but values of d are not correct\"\n", + "assert str(d) == \"{'a': 'f', 3: [1.2, 3]}\", \"correct keys and values, but incorrect correspondence in keys and values of d\"\n", + "\n", + "assert str(type(e)) == \"\", \"type of e is not tuple. e should be a tuple\"\n", + "assert str(len(e)) == \"3\", \"length of e is not correct\"\n", + "assert str(sorted(map(str, e))) == \"['-3.3', '5', 'd']\", \"values of e are not correct\"\n", + "assert str(e) == \"(5, -3.3, 'd')\", \"order of elements of e is not correct\"\n", + "\n", + "assert str(type(f)) == \"\", \"type of f is not bool. f should be a bool\"\n", + "assert str(f) == \"False\", \"boolean value of f is not correct\"\n", + "\n", + "print('Success!')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "editable": false, + "max_height": 100, + "nbgrader": { + "cell_type": "code", + "checksum": "cc9122036c0a86a93446786109a3558e", + "grade": true, + "grade_id": "cell-13479eb0e5fff152", + "locked": true, + "points": 1, + "schema_version": 3, + "solution": false, + "task": false + } + }, + "outputs": [], + "source": [ + "\n", + "# a function that checks whether a function throws an error\n", + "\"\"\"Check that squares raises an error for invalid inputs\"\"\"\n", + "def test_func_throws(func, ErrorType):\n", + " try:\n", + " func()\n", + " except ErrorType:\n", + " return True\n", + " else:\n", + " print('Did not raise right type of error!')\n", + " return False\n", + "\n", + "# test a custom function\n", + "from hashlib import sha1\n", + "assert str(type(fun(3))) == \"\", \"type of fun(3) is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(fun(3)) == \"3\", \"value of fun(3) is not correct\"\n", + "\n", + "assert str(type(test_func_throws(lambda : fun(-4), ValueError))) == \"\", \"type of test_func_throws(lambda : fun(-4), ValueError) is not bool. test_func_throws(lambda : fun(-4), ValueError) should be a bool\"\n", + "assert str(test_func_throws(lambda : fun(-4), ValueError)) == \"True\", \"boolean value of test_func_throws(lambda : fun(-4), ValueError) is not correct\"\n", + "\n", + "assert str(type(test_func_throws(lambda : fun(0), ValueError))) == \"\", \"type of test_func_throws(lambda : fun(0), ValueError) is not bool. test_func_throws(lambda : fun(0), ValueError) should be a bool\"\n", + "assert str(test_func_throws(lambda : fun(0), ValueError)) == \"False\", \"boolean value of test_func_throws(lambda : fun(0), ValueError) is not correct\"\n", + "\n", + "\n", + "print('Success!')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbgrader/tests/apps/files/autotest-multi-unchanged.ipynb b/nbgrader/tests/apps/files/autotest-multi-unchanged.ipynb new file mode 100644 index 000000000..212fee372 --- /dev/null +++ b/nbgrader/tests/apps/files/autotest-multi-unchanged.ipynb @@ -0,0 +1,277 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "deletable": false, + "nbgrader": { + "cell_type": "code", + "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", + "grade": false, + "grade_id": "soln", + "locked": false, + "schema_version": 3, + "solution": true + } + }, + "outputs": [], + "source": [ + "# YOUR CODE HERE\n", + "raise NotImplementedError()" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "deletable": false, + "editable": false, + "max_height": 100, + "nbgrader": { + "cell_type": "code", + "checksum": "620860a42fa05b556da01013ef99ed8c", + "grade": true, + "grade_id": "test_multi", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Success!\n" + ] + } + ], + "source": [ + "# the basic tests from the simple notebook\n", + "from hashlib import sha1\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "assert str(type(b)) == \"\", \"type of b is not str. b should be an str\"\n", + "assert str(len(b)) == \"5\", \"length of b is not correct\"\n", + "assert str(b.lower()) == \"hello\", \"value of b is not correct\"\n", + "assert str(b) == \"hello\", \"correct string value of b but incorrect case of letters\"\n", + "\n", + "assert str(type(c)) == \"\", \"type of c is not list. c should be a list\"\n", + "assert str(len(c)) == \"3\", \"length of c is not correct\"\n", + "assert str(sorted(map(str, c))) == \"['1', '2', 'test']\", \"values of c are not correct\"\n", + "assert str(c) == \"[1, 2, 'test']\", \"order of elements of c is not correct\"\n", + "\n", + "print('Success!')" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "deletable": false, + "editable": false, + "max_height": 100, + "nbgrader": { + "cell_type": "code", + "checksum": "c46928757ed303ce204ef97299b39f82", + "grade": true, + "grade_id": "cell-f2803ba7c42d03ab", + "locked": true, + "points": 1, + "schema_version": 3, + "solution": false, + "task": false + } + }, + "outputs": [ + { + "ename": "AssertionError", + "evalue": "value of str(a) is not correct", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/tmp/ipykernel_645570/554255095.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtype\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m\"\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"type of str(a) is not str. str(a) should be an str\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m\"1\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"length of str(a) is not correct\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 7\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlower\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m\"5\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"value of str(a) is not correct\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 8\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m\"5\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"correct string value of str(a) but incorrect case of letters\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mAssertionError\u001b[0m: value of str(a) is not correct" + ] + } + ], + "source": [ + "# multiple expressions per line\n", + "from hashlib import sha1\n", + "assert str(type(a)) == \"\", \"type of type(a) is not correct\"\n", + "\n", + "assert str(type(str(a))) == \"\", \"type of str(a) is not str. str(a) should be an str\"\n", + "assert str(len(str(a))) == \"1\", \"length of str(a) is not correct\"\n", + "assert str(str(a).lower()) == \"5\", \"value of str(a) is not correct\"\n", + "assert str(str(a)) == \"5\", \"correct string value of str(a) but incorrect case of letters\"\n", + "\n", + "assert str(type(a == \"5\")) == \"\", \"type of a == \\\"5\\\" is not bool. a == \\\"5\\\" should be a bool\"\n", + "assert str(a == \"5\") == \"False\", \"boolean value of a == \\\"5\\\" is not correct\"\n", + "\n", + "assert str(type([ch for ch in b])) == \"\", \"type of [ch for ch in b] is not list. [ch for ch in b] should be a list\"\n", + "assert str(len([ch for ch in b])) == \"5\", \"length of [ch for ch in b] is not correct\"\n", + "assert str(sorted(map(str, [ch for ch in b]))) == \"['e', 'h', 'l', 'l', 'o']\", \"values of [ch for ch in b] are not correct\"\n", + "assert str([ch for ch in b]) == \"['h', 'e', 'l', 'l', 'o']\", \"order of elements of [ch for ch in b] is not correct\"\n", + "\n", + "assert str(type(len(c))) == \"\", \"type of len(c) is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(len(c)) == \"3\", \"value of len(c) is not correct\"\n", + "\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "\n", + "# intervening regular code\n", + "print(\"hello!\")\n", + "\n", + "# differing spacings, numbers of comment characters, trailing whitespace\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "\n", + "\n", + "print('Success!')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "editable": false, + "max_height": 100, + "nbgrader": { + "cell_type": "code", + "checksum": "2fc8066fae8e7aeb865116c88d1be31a", + "grade": true, + "grade_id": "cell-693350420ec62f1b", + "locked": true, + "points": 1, + "schema_version": 3, + "solution": false, + "task": false + } + }, + "outputs": [], + "source": [ + "# a few common types\n", + "from hashlib import sha1\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "assert str(type(b)) == \"\", \"type of b is not str. b should be an str\"\n", + "assert str(len(b)) == \"5\", \"length of b is not correct\"\n", + "assert str(b.lower()) == \"hello\", \"value of b is not correct\"\n", + "assert str(b) == \"hello\", \"correct string value of b but incorrect case of letters\"\n", + "\n", + "assert str(type(c)) == \"\", \"type of c is not list. c should be a list\"\n", + "assert str(len(c)) == \"3\", \"length of c is not correct\"\n", + "assert str(sorted(map(str, c))) == \"['1', '2', 'test']\", \"values of c are not correct\"\n", + "assert str(c) == \"[1, 2, 'test']\", \"order of elements of c is not correct\"\n", + "\n", + "assert str(type(d)) == \"\", \"type of d is not dict. d should be a dict\"\n", + "assert str(len(list(d.keys()))) == \"2\", \"number of keys of d is not correct\"\n", + "assert str(sorted(map(str, d.keys()))) == \"['3', 'a']\", \"keys of d are not correct\"\n", + "assert str(sorted(map(str, d.values()))) == \"['[1.2, 3]', 'f']\", \"correct keys, but values of d are not correct\"\n", + "assert str(d) == \"{'a': 'f', 3: [1.2, 3]}\", \"correct keys and values, but incorrect correspondence in keys and values of d\"\n", + "\n", + "assert str(type(e)) == \"\", \"type of e is not tuple. e should be a tuple\"\n", + "assert str(len(e)) == \"3\", \"length of e is not correct\"\n", + "assert str(sorted(map(str, e))) == \"['-3.3', '5', 'd']\", \"values of e are not correct\"\n", + "assert str(e) == \"(5, -3.3, 'd')\", \"order of elements of e is not correct\"\n", + "\n", + "assert str(type(f)) == \"\", \"type of f is not bool. f should be a bool\"\n", + "assert str(f) == \"False\", \"boolean value of f is not correct\"\n", + "\n", + "print('Success!')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "editable": false, + "max_height": 100, + "nbgrader": { + "cell_type": "code", + "checksum": "cc9122036c0a86a93446786109a3558e", + "grade": true, + "grade_id": "cell-13479eb0e5fff152", + "locked": true, + "points": 1, + "schema_version": 3, + "solution": false, + "task": false + } + }, + "outputs": [], + "source": [ + "\n", + "# a function that checks whether a function throws an error\n", + "\"\"\"Check that squares raises an error for invalid inputs\"\"\"\n", + "def test_func_throws(func, ErrorType):\n", + " try:\n", + " func()\n", + " except ErrorType:\n", + " return True\n", + " else:\n", + " print('Did not raise right type of error!')\n", + " return False\n", + "\n", + "# test a custom function\n", + "from hashlib import sha1\n", + "assert str(type(fun(3))) == \"\", \"type of fun(3) is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(fun(3)) == \"3\", \"value of fun(3) is not correct\"\n", + "\n", + "assert str(type(test_func_throws(lambda : fun(-4), ValueError))) == \"\", \"type of test_func_throws(lambda : fun(-4), ValueError) is not bool. test_func_throws(lambda : fun(-4), ValueError) should be a bool\"\n", + "assert str(test_func_throws(lambda : fun(-4), ValueError)) == \"True\", \"boolean value of test_func_throws(lambda : fun(-4), ValueError) is not correct\"\n", + "\n", + "assert str(type(test_func_throws(lambda : fun(0), ValueError))) == \"\", \"type of test_func_throws(lambda : fun(0), ValueError) is not bool. test_func_throws(lambda : fun(0), ValueError) should be a bool\"\n", + "assert str(test_func_throws(lambda : fun(0), ValueError)) == \"False\", \"boolean value of test_func_throws(lambda : fun(0), ValueError) is not correct\"\n", + "\n", + "\n", + "print('Success!')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbgrader/tests/apps/files/autotest-multi.ipynb b/nbgrader/tests/apps/files/autotest-multi.ipynb new file mode 100644 index 000000000..7488e64df --- /dev/null +++ b/nbgrader/tests/apps/files/autotest-multi.ipynb @@ -0,0 +1,164 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "soln", + "locked": false, + "schema_version": 3, + "solution": true + } + }, + "outputs": [], + "source": [ + "### BEGIN SOLUTION\n", + "a = 5\n", + "b = \"hello\"\n", + "c = [1, 2, \"test\"]\n", + "d = {\"a\" : \"f\", 3 : [1.2, 3]}\n", + "e = (5, -3.3, \"d\")\n", + "f = False\n", + "def fun(x):\n", + " if x < 0:\n", + " raise ValueError\n", + " else:\n", + " return x\n", + "### END SOLUTION" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "test_multi", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + } + }, + "outputs": [], + "source": [ + "# the basic tests from the simple notebook\n", + "### AUTOTEST a\n", + "### AUTOTEST b\n", + "### AUTOTEST c" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "cell-f2803ba7c42d03ab", + "locked": true, + "points": 1, + "schema_version": 3, + "solution": false, + "task": false + } + }, + "outputs": [], + "source": [ + "# multiple expressions per line\n", + "### AUTOTEST type(a); str(a); a == \"5\";\n", + "### AUTOTEST [ch for ch in b]; len(c);\n", + "### AUTOTEST a;\n", + "\n", + "# intervening regular code\n", + "print(\"hello!\")\n", + "\n", + "# differing spacings, numbers of comment characters, trailing whitespace\n", + "### AUTOTEST a \n", + "#AUTOTEST a\n", + "# AUTOTEST a\n", + "## AUTOTEST a\n", + "# AUTOTEST a\n", + "# # # # AUTOTEST a\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "cell-693350420ec62f1b", + "locked": true, + "points": 1, + "schema_version": 3, + "solution": false, + "task": false + } + }, + "outputs": [], + "source": [ + "# a few common types\n", + "### AUTOTEST a; b; c; d; e; f;" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "cell-13479eb0e5fff152", + "locked": true, + "points": 1, + "schema_version": 3, + "solution": false, + "task": false + } + }, + "outputs": [], + "source": [ + "\n", + "# a function that checks whether a function throws an error\n", + "\"\"\"Check that squares raises an error for invalid inputs\"\"\"\n", + "def test_func_throws(func, ErrorType):\n", + " try:\n", + " func()\n", + " except ErrorType:\n", + " return True\n", + " else:\n", + " print('Did not raise right type of error!')\n", + " return False\n", + "\n", + "# test a custom function\n", + "### AUTOTEST fun(3)\n", + "### AUTOTEST test_func_throws(lambda : fun(-4), ValueError)\n", + "### AUTOTEST test_func_throws(lambda : fun(0), ValueError)\n" + ] + } + ], + "metadata": { + "celltoolbar": "Create Assignment", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbgrader/tests/apps/files/autotest-simple-changed.ipynb b/nbgrader/tests/apps/files/autotest-simple-changed.ipynb new file mode 100644 index 000000000..d918a0df8 --- /dev/null +++ b/nbgrader/tests/apps/files/autotest-simple-changed.ipynb @@ -0,0 +1,92 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "nbgrader": { + "cell_type": "code", + "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", + "grade": false, + "grade_id": "soln", + "locked": false, + "schema_version": 3, + "solution": true + } + }, + "outputs": [], + "source": [ + "# YOUR CODE HERE\n", + "a = 5\n", + "b = \"hello\"\n", + "c = [1, 2, \"test\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "editable": false, + "max_height": 100, + "nbgrader": { + "cell_type": "code", + "checksum": "f1736680e92dee1b5debd87ec5790f10", + "grade": true, + "grade_id": "test_simple", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + } + }, + "outputs": [], + "source": [ + "from hashlib import sha1\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "assert str(type(b)) == \"\", \"type of b is not str. b should be an str\"\n", + "assert str(len(b)) == \"5\", \"length of b is not correct\"\n", + "assert str(b.lower()) == \"hello\", \"value of b is not correct\"\n", + "assert str(b) == \"hello\", \"correct string value of b but incorrect case of letters\"\n", + "\n", + "assert str(type(c)) == \"\", \"type of c is not list. c should be a list\"\n", + "assert str(len(c)) == \"3\", \"length of c is not correct\"\n", + "assert str(sorted(map(str, c))) == \"['1', '2', 'test']\", \"values of c are not correct\"\n", + "assert str(c) == \"[1, 2, 'test']\", \"order of elements of c is not correct\"\n", + "\n", + "print('Success!')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbgrader/tests/apps/files/autotest-simple-unchanged.ipynb b/nbgrader/tests/apps/files/autotest-simple-unchanged.ipynb new file mode 100644 index 000000000..214e5fbbb --- /dev/null +++ b/nbgrader/tests/apps/files/autotest-simple-unchanged.ipynb @@ -0,0 +1,90 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "nbgrader": { + "cell_type": "code", + "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", + "grade": false, + "grade_id": "soln", + "locked": false, + "schema_version": 3, + "solution": true + } + }, + "outputs": [], + "source": [ + "# YOUR CODE HERE\n", + "raise NotImplementedError()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "deletable": false, + "editable": false, + "max_height": 100, + "nbgrader": { + "cell_type": "code", + "checksum": "f1736680e92dee1b5debd87ec5790f10", + "grade": true, + "grade_id": "test_simple", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + } + }, + "outputs": [], + "source": [ + "from hashlib import sha1\n", + "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", + "assert str(a) == \"5\", \"value of a is not correct\"\n", + "\n", + "assert str(type(b)) == \"\", \"type of b is not str. b should be an str\"\n", + "assert str(len(b)) == \"5\", \"length of b is not correct\"\n", + "assert str(b.lower()) == \"hello\", \"value of b is not correct\"\n", + "assert str(b) == \"hello\", \"correct string value of b but incorrect case of letters\"\n", + "\n", + "assert str(type(c)) == \"\", \"type of c is not list. c should be a list\"\n", + "assert str(len(c)) == \"3\", \"length of c is not correct\"\n", + "assert str(sorted(map(str, c))) == \"['1', '2', 'test']\", \"values of c are not correct\"\n", + "assert str(c) == \"[1, 2, 'test']\", \"order of elements of c is not correct\"\n", + "\n", + "print('Success!')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbgrader/tests/apps/files/autotest-simple.ipynb b/nbgrader/tests/apps/files/autotest-simple.ipynb new file mode 100644 index 000000000..42c87b071 --- /dev/null +++ b/nbgrader/tests/apps/files/autotest-simple.ipynb @@ -0,0 +1,74 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "soln", + "locked": false, + "schema_version": 3, + "solution": true + } + }, + "outputs": [], + "source": [ + "### BEGIN SOLUTION\n", + "a = 5\n", + "b = \"hello\"\n", + "c = [1, 2, \"test\"]\n", + "### END SOLUTION" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "test_simple", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + } + }, + "outputs": [], + "source": [ + "### AUTOTEST a\n", + "### AUTOTEST b\n", + "### AUTOTEST c" + ] + } + ], + "metadata": { + "celltoolbar": "Create Assignment", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbgrader/tests/apps/files/test-no-metadata-autotest.ipynb b/nbgrader/tests/apps/files/test-no-metadata-autotest.ipynb new file mode 100644 index 000000000..9139f364e --- /dev/null +++ b/nbgrader/tests/apps/files/test-no-metadata-autotest.ipynb @@ -0,0 +1,225 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For this problem set, we'll be using the Jupyter notebook:\n", + "\n", + "![](jupyter.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part A (2 points)\n", + "\n", + "Write a function that returns a list of numbers, such that $x_i=i^2$, for $1\\leq i \\leq n$. Make sure it handles the case where $n<1$ by raising a `ValueError`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "def squares(n):\n", + " \"\"\"Compute the squares of numbers from 1 to n, such that the \n", + " ith element of the returned list equals i^2.\n", + " \n", + " \"\"\"\n", + " ### BEGIN SOLUTION\n", + " if n < 1:\n", + " raise ValueError(\"n must be greater than or equal to 1\")\n", + " return [i ** 2 for i in range(1, n + 1)]\n", + " ### END SOLUTION" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Your function should print `[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]` for $n=10$. Check that it does:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "squares(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "\"\"\"Check that squares returns the correct output for several inputs\"\"\"\n", + "### AUTOTEST squares(1)\n", + "### AUTOTEST squares(10)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "\"\"\"Check that squares raises an error for invalid inputs\"\"\"\n", + "try:\n", + " squares(0)\n", + "except ValueError:\n", + " pass\n", + "else:\n", + " raise AssertionError(\"did not raise\")\n", + "\n", + "try:\n", + " squares(-4)\n", + "except ValueError:\n", + " pass\n", + "else:\n", + " raise AssertionError(\"did not raise\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Part B (1 point)\n", + "\n", + "Using your `squares` function, write a function that computes the sum of the squares of the numbers from 1 to $n$. Your function should call the `squares` function -- it should NOT reimplement its functionality." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "def sum_of_squares(n):\n", + " \"\"\"Compute the sum of the squares of numbers from 1 to n.\"\"\"\n", + " ### BEGIN SOLUTION\n", + " return sum(squares(n))\n", + " ### END SOLUTION" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The sum of squares from 1 to 10 should be 385. Verify that this is the answer you get:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "sum_of_squares(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "\"\"\"Check that sum_of_squares returns the correct answer for various inputs.\"\"\"\n", + "### AUTOTEST sum_of_squares(1)\n", + "### AUTOTEST sum_of_squares(10)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "\"\"\"Check that sum_of_squares relies on squares.\"\"\"\n", + "orig_squares = squares\n", + "del squares\n", + "try:\n", + " sum_of_squares(1)\n", + "except NameError:\n", + " pass\n", + "else:\n", + " raise AssertionError(\"sum_of_squares does not use squares\")\n", + "finally:\n", + " squares = orig_squares" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part C (1 point)\n", + "\n", + "Using LaTeX math notation, write out the equation that is implemented by your `sum_of_squares` function." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "$\\sum_{i=1}^n i^2$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part D (2 points)\n", + "\n", + "Find a usecase for your `sum_of_squares` function and implement that usecase in the cell below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def pyramidal_number(n):\n", + " \"\"\"Returns the n^th pyramidal number\"\"\"\n", + " return sum_of_squares(n)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python", + "language": "python", + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/nbgrader/tests/apps/files/tests.yml b/nbgrader/tests/apps/files/tests.yml new file mode 100644 index 000000000..cfd000cb5 --- /dev/null +++ b/nbgrader/tests/apps/files/tests.yml @@ -0,0 +1,310 @@ +#kernel_name: +# setup: +# hash: +# dispatch: +# normalize: +# check: +# success: +# +# templates: +# default: +# - test: +# fail: +# +# datatype: +# - test: +# fail: + + +python3: + setup: "from hashlib import sha1" + hash: 'sha1({{snippet}}.encode("utf-8")+b"{{salt}}").hexdigest()' + dispatch: "type({{snippet}})" + normalize: "str({{snippet}})" + check: 'assert {{snippet}} == "{{value}}", "{{message}}"' + success: "print('Success!')" + + templates: + default: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "value of {{snippet}} is not correct" + + int: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()" + + - test: "{{snippet}}" + fail: "value of {{snippet}} is not correct" + + float: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not float. Please make sure it is float and not np.float64, etc. You can cast your value into a float using float()" + + - test: "round({{snippet}}, 2)" + fail: "value of {{snippet}} is not correct (rounded to 2 decimal places)" + + set: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not set. {{snippet}} should be a set" + + - test: "len({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "value of {{snippet}} is not correct" + + list: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not list. {{snippet}} should be a list" + + - test: "len({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "sorted(map(str, {{snippet}}))" + fail: "values of {{snippet}} are not correct" + + - test: "{{snippet}}" + fail: "order of elements of {{snippet}} is not correct" + + tuple: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not tuple. {{snippet}} should be a tuple" + + - test: "len({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "sorted(map(str, {{snippet}}))" + fail: "values of {{snippet}} are not correct" + + - test: "{{snippet}}" + fail: "order of elements of {{snippet}} is not correct" + + str: + + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not str. {{snippet}} should be an str" + + - test: "len({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "{{snippet}}.lower()" + fail: "value of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "correct string value of {{snippet}} but incorrect case of letters" + + dict: + + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not dict. {{snippet}} should be a dict" + + - test: "len(list({{snippet}}.keys()))" + fail: "number of keys of {{snippet}} is not correct" + + - test: "sorted(map(str, {{snippet}}.keys()))" + fail: "keys of {{snippet}} are not correct" + + - test: "sorted(map(str, {{snippet}}.values()))" + fail: "correct keys, but values of {{snippet}} are not correct" + + - test: "{{snippet}}" + fail: "correct keys and values, but incorrect correspondence in keys and values of {{snippet}}" + + bool: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not bool. {{snippet}} should be a bool" + + - test: "{{snippet}}" + fail: "boolean value of {{snippet}} is not correct" + + type: + - test: "{{snippet}}" + fail: "type of {{snippet}} is not correct" + +# --------------------------------------------- + +python: + setup: "from hashlib import sha1" + hash: 'sha1({{snippet}}.encode("utf-8")+b"{{salt}}").hexdigest()' + dispatch: "type({{snippet}})" + normalize: "str({{snippet}})" + check: 'assert {{snippet}} == "{{value}}", "{{message}}"' + success: "print('Success!')" + + templates: + default: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "value of {{snippet}} is not correct" + + int: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()" + + - test: "{{snippet}}" + fail: "value of {{snippet}} is not correct" + + float: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not float. Please make sure it is float and not np.float64, etc. You can cast your value into a float using float()" + + - test: "round({{snippet}}, 2)" + fail: "value of {{snippet}} is not correct (rounded to 2 decimal places)" + + set: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not set. {{snippet}} should be a set" + + - test: "len({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "value of {{snippet}} is not correct" + + list: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not list. {{snippet}} should be a list" + + - test: "len({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "sorted(map(str, {{snippet}}))" + fail: "values of {{snippet}} are not correct" + + - test: "{{snippet}}" + fail: "order of elements of {{snippet}} is not correct" + + tuple: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not tuple. {{snippet}} should be a tuple" + + - test: "len({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "sorted(map(str, {{snippet}}))" + fail: "values of {{snippet}} are not correct" + + - test: "{{snippet}}" + fail: "order of elements of {{snippet}} is not correct" + + str: + + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not str. {{snippet}} should be an str" + + - test: "len({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "{{snippet}}.lower()" + fail: "value of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "correct string value of {{snippet}} but incorrect case of letters" + + dict: + + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not dict. {{snippet}} should be a dict" + + - test: "len(list({{snippet}}.keys()))" + fail: "number of keys of {{snippet}} is not correct" + + - test: "sorted(map(str, {{snippet}}.keys()))" + fail: "keys of {{snippet}} are not correct" + + - test: "sorted(map(str, {{snippet}}.values()))" + fail: "correct keys, but values of {{snippet}} are not correct" + + - test: "{{snippet}}" + fail: "correct keys and values, but incorrect correspondence in keys and values of {{snippet}}" + + bool: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not bool. {{snippet}} should be a bool" + + - test: "{{snippet}}" + fail: "boolean value of {{snippet}} is not correct" + + type: + - test: "{{snippet}}" + fail: "type of {{snippet}} is not correct" + + + +# -------------------------------------------------------------------------------------------------- +ir: + setup: 'library(digest)' + hash: 'digest(paste({{snippet}}, "{{salt}}"))' + dispatch: 'class({{snippet}})' + normalize: 'toString({{snippet}})' + check: 'stopifnot("{{message}}"= setequal({{snippet}}, "{{value}}"))' + success: "print('Success!')" + + templates: + default: + - test: "class({{snippet}})" + fail: "type of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "value of {{snippet}} is not correct" + + integer: + - test: "class({{snippet}})" + fail: "type of {{snippet}} is not integer" + + - test: "length({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "sort({{snippet}})" + fail: "values of {{snippet}} are not correct" + + numeric: + - test: "class({{snippet}})" + fail: "type of {{snippet}} is not double" + + - test: "round({{snippet}}, 2)" + fail: "value of {{snippet}} is not correct (rounded to 2 decimal places)" + + - test: "length({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "sort({{snippet}})" + fail: "values of {{snippet}} are not correct" + + list: + - test: "class({{snippet}})" + fail: "type of {{snippet}} is not list" + + - test: "length({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "sort(c(names({{snippet}})))" + fail: "values of {{snippet}} names are not correct" + + - test: "{{snippet}}" + fail: "order of elements of {{snippet}} is not correct" + + character: + - test: "class({{snippet}})" + fail: "type of {{snippet}} is not list" + + - test: "length({{snippet}})" + fail: "length of {{snippet}} is not correct" + + - test: "tolower({{snippet}})" + fail: "value of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "correct string value of {{snippet}} but incorrect case of letters" + + logical: + - test: "class({{snippet}})" + fail: "type of {{snippet}} is not logical" + + - test: "{{snippet}}" + fail: "logical value of {{snippet}} is not correct" diff --git a/nbgrader/tests/apps/test_nbgrader_autograde.py b/nbgrader/tests/apps/test_nbgrader_autograde.py index be4e04838..1f00939f8 100644 --- a/nbgrader/tests/apps/test_nbgrader_autograde.py +++ b/nbgrader/tests/apps/test_nbgrader_autograde.py @@ -94,6 +94,97 @@ def test_grade(self, db, course_dir): assert comment1.comment == None assert comment2.comment == None + + def test_grade_autotest(self, db, course_dir): + """Can files including autotest commands be graded?""" + run_nbgrader(["db", "assignment", "add", "ps1", "--db", db, "--duedate", "2015-02-02 14:58:23.948203 America/Los_Angeles"]) + run_nbgrader(["db", "student", "add", "foo", "--db", db]) + run_nbgrader(["db", "student", "add", "bar", "--db", db]) + + self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "source", "ps1", "p1.ipynb")) + self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + run_nbgrader(["generate_assignment", "ps1", "--db", db]) + + self._copy_file(join("files", "autotest-simple-unchanged.ipynb"), join(course_dir, "submitted", "foo", "ps1", "p1.ipynb")) + self._copy_file(join("files", "autotest-simple-changed.ipynb"), join(course_dir, "submitted", "bar", "ps1", "p1.ipynb")) + run_nbgrader(["autograde", "ps1", "--db", db]) + + assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "p1.ipynb")) + assert not os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "timestamp.txt")) + assert os.path.isfile(join(course_dir, "autograded", "bar", "ps1", "p1.ipynb")) + assert not os.path.isfile(join(course_dir, "autograded", "bar", "ps1", "timestamp.txt")) + + with Gradebook(db) as gb: + notebook = gb.find_submission_notebook("p1", "ps1", "foo") + assert notebook.score == 0 + assert notebook.max_score == 1 + assert notebook.needs_manual_grade == False + + notebook = gb.find_submission_notebook("p1", "ps1", "bar") + assert notebook.score == 1 + assert notebook.max_score == 1 + assert notebook.needs_manual_grade == False + + def test_grade_hashed_autotest(self, db, course_dir): + """Can files including hashed autotest commands be graded?""" + run_nbgrader(["db", "assignment", "add", "ps1", "--db", db, "--duedate", "2015-02-02 14:58:23.948203 America/Los_Angeles"]) + run_nbgrader(["db", "student", "add", "foo", "--db", db]) + run_nbgrader(["db", "student", "add", "bar", "--db", db]) + + self._copy_file(join("files", "autotest-hashed.ipynb"), join(course_dir, "source", "ps1", "p1.ipynb")) + self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + run_nbgrader(["generate_assignment", "ps1", "--db", db]) + + self._copy_file(join("files", "autotest-hashed-unchanged.ipynb"), join(course_dir, "submitted", "foo", "ps1", "p1.ipynb")) + self._copy_file(join("files", "autotest-hashed-changed.ipynb"), join(course_dir, "submitted", "bar", "ps1", "p1.ipynb")) + run_nbgrader(["autograde", "ps1", "--db", db]) + + assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "p1.ipynb")) + assert not os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "timestamp.txt")) + assert os.path.isfile(join(course_dir, "autograded", "bar", "ps1", "p1.ipynb")) + assert not os.path.isfile(join(course_dir, "autograded", "bar", "ps1", "timestamp.txt")) + + with Gradebook(db) as gb: + notebook = gb.find_submission_notebook("p1", "ps1", "foo") + assert notebook.score == 0 + assert notebook.max_score == 1 + assert notebook.needs_manual_grade == False + + notebook = gb.find_submission_notebook("p1", "ps1", "bar") + assert notebook.score == 1 + assert notebook.max_score == 1 + assert notebook.needs_manual_grade == False + + def test_grade_complex_autotest(self, db, course_dir): + """Can files including complicated autotest commands be graded?""" + run_nbgrader(["db", "assignment", "add", "ps1", "--db", db, "--duedate", "2015-02-02 14:58:23.948203 America/Los_Angeles"]) + run_nbgrader(["db", "student", "add", "foo", "--db", db]) + run_nbgrader(["db", "student", "add", "bar", "--db", db]) + + self._copy_file(join("files", "autotest-multi.ipynb"), join(course_dir, "source", "ps1", "p1.ipynb")) + self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + run_nbgrader(["generate_assignment", "ps1", "--db", db]) + + self._copy_file(join("files", "autotest-multi-unchanged.ipynb"), join(course_dir, "submitted", "foo", "ps1", "p1.ipynb")) + self._copy_file(join("files", "autotest-multi-changed.ipynb"), join(course_dir, "submitted", "bar", "ps1", "p1.ipynb")) + run_nbgrader(["autograde", "ps1", "--db", db]) + + assert os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "p1.ipynb")) + assert not os.path.isfile(join(course_dir, "autograded", "foo", "ps1", "timestamp.txt")) + assert os.path.isfile(join(course_dir, "autograded", "bar", "ps1", "p1.ipynb")) + assert not os.path.isfile(join(course_dir, "autograded", "bar", "ps1", "timestamp.txt")) + + with Gradebook(db) as gb: + notebook = gb.find_submission_notebook("p1", "ps1", "foo") + assert notebook.score == 0 + assert notebook.max_score == 4 + assert notebook.needs_manual_grade == False + + notebook = gb.find_submission_notebook("p1", "ps1", "bar") + assert notebook.score == 3 + assert notebook.max_score == 4 + assert notebook.needs_manual_grade == False + def test_showtraceback_exploit(self, db, course_dir): """Can students exploit showtraceback to hide errors from all future cell outputs to receive free points for incorrect cells?""" run_nbgrader(["db", "assignment", "add", "ps1", "--db", db, "--duedate", "2015-02-02 14:58:23.948203 America/Los_Angeles"]) @@ -842,6 +933,95 @@ def test_hidden_tests_single_notebook(self, db, course_dir): nb1 = submission.notebooks[0] assert nb1.score == 1.5 + def test_hidden_tests_autotest(self, db, course_dir): + """Can files with hidden autotests be graded?""" + run_nbgrader(["db", "assignment", "add", "ps1", "--db", db, "--duedate", + "2015-02-02 14:58:23.948203 America/Los_Angeles"]) + run_nbgrader(["db", "student", "add", "foo", "--db", db]) + run_nbgrader(["db", "student", "add", "bar", "--db", db]) + run_nbgrader(["db", "student", "add", "baz", "--db", db]) + with open("nbgrader_config.py", "a") as fh: + fh.write("""c.ClearSolutions.code_stub=dict(python="# YOUR CODE HERE")""") + + self._copy_file(join("files", "autotest-hidden.ipynb"), join(course_dir, "source", "ps1", "p1.ipynb")) + self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + run_nbgrader(["generate_assignment", "ps1", "--db", db]) + + self._copy_file(join("files", "autotest-hidden-unchanged.ipynb"), join(course_dir, "submitted", "foo", "ps1", "p1.ipynb")) + self._copy_file(join("files", "autotest-hidden-changed-wrong.ipynb"), join(course_dir, "submitted", "bar", "ps1", "p1.ipynb")) + self._copy_file(join("files", "autotest-hidden-changed-right.ipynb"), join(course_dir, "submitted", "baz", "ps1", "p1.ipynb")) + + # make sure submitted validates for both bar and baz (should only fail on hidden tests), but not foo (missing any input and visible type checks will fail) + output = run_nbgrader([ + "validate", join(course_dir, "submitted", "foo", "ps1", "p1.ipynb"), + ], stdout=True) + assert output.splitlines()[0] == ( + "VALIDATION FAILED ON 1 CELL(S)! If you submit your assignment " + "as it is, you WILL NOT" + ) + output = run_nbgrader([ + "validate", join(course_dir, "submitted", "bar", "ps1", "p1.ipynb") + ], stdout=True) + assert output.strip() == "Success! Your notebook passes all the tests." + + output = run_nbgrader([ + "validate", join(course_dir, "submitted", "baz", "ps1", "p1.ipynb") + ], stdout=True) + assert output.strip() == "Success! Your notebook passes all the tests." + + # autograde + run_nbgrader(["autograde", "ps1", "--db", db]) + assert os.path.exists(join(course_dir, "autograded", "foo", "ps1", "p1.ipynb")) + assert os.path.exists(join(course_dir, "autograded", "bar", "ps1", "p1.ipynb")) + assert os.path.exists(join(course_dir, "autograded", "baz", "ps1", "p1.ipynb")) + + # make sure hidden tests are placed back in autograded + sub_nb = join(course_dir, "autograded", "foo", "ps1", "p1.ipynb") + with io.open(sub_nb, mode='r', encoding='utf-8') as nb: + source = nb.read() + assert "BEGIN HIDDEN TESTS" in source + sub_nb = join(course_dir, "autograded", "bar", "ps1", "p1.ipynb") + with io.open(sub_nb, mode='r', encoding='utf-8') as nb: + source = nb.read() + assert "BEGIN HIDDEN TESTS" in source + sub_nb = join(course_dir, "autograded", "baz", "ps1", "p1.ipynb") + with io.open(sub_nb, mode='r', encoding='utf-8') as nb: + source = nb.read() + assert "BEGIN HIDDEN TESTS" in source + + # make sure autograded for foo does not validate, should fail on visible and hidden tests + output = run_nbgrader([ + "validate", join(course_dir, "autograded", "foo", "ps1", "p1.ipynb"), + ], stdout=True) + assert output.splitlines()[0] == ( + "VALIDATION FAILED ON 1 CELL(S)! If you submit your assignment " + "as it is, you WILL NOT" + ) + # make sure autograded for bar does not, should fail on hidden tests + output = run_nbgrader([ + "validate", join(course_dir, "autograded", "bar", "ps1", "p1.ipynb"), + ], stdout=True) + assert output.splitlines()[0] == ( + "VALIDATION FAILED ON 1 CELL(S)! If you submit your assignment " + "as it is, you WILL NOT" + ) + # make sure autograded for bar validates, should succeed on hidden tests + output = run_nbgrader([ + "validate", join(course_dir, "autograded", "baz", "ps1", "p1.ipynb"), + ], stdout=True) + assert output.strip() == "Success! Your notebook passes all the tests." + + with Gradebook(db) as gb: + submission = gb.find_submission("ps1", "foo") + nb1 = submission.notebooks[0] + assert nb1.score == 0 + submission = gb.find_submission("ps1", "bar") + nb1 = submission.notebooks[0] + assert nb1.score == 0 + submission = gb.find_submission("ps1", "baz") + nb1 = submission.notebooks[0] + assert nb1.score == 1 + def test_handle_failure(self, course_dir): run_nbgrader(["db", "assignment", "add", "ps1", "--duedate", "2015-02-02 14:58:23.948203 America/Los_Angeles"]) diff --git a/nbgrader/tests/apps/test_nbgrader_generate_assignment.py b/nbgrader/tests/apps/test_nbgrader_generate_assignment.py index 34cce609a..1defff6e9 100644 --- a/nbgrader/tests/apps/test_nbgrader_generate_assignment.py +++ b/nbgrader/tests/apps/test_nbgrader_generate_assignment.py @@ -47,6 +47,88 @@ def test_single_file(self, course_dir, temp_cwd): run_nbgrader(["generate_assignment", "ps1"]) assert os.path.isfile(join(course_dir, "release", "ps1", "foo.ipynb")) + def test_autotests_simple(self, course_dir, temp_cwd): + """Can a notebook with simple autotests be generated with a default yaml location, and is autotest code removed?""" + self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "source", "ps1", "foo.ipynb")) + self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + run_nbgrader(["db", "assignment", "add", "ps1"]) + run_nbgrader(["generate_assignment", "ps1"]) + assert os.path.isfile(join(course_dir, "release", "ps1", "foo.ipynb")) + assert not os.path.isfile(join(course_dir, "release", "ps1", "tests.yml")) + + foo = self._file_contents(join(course_dir, "release", "ps1", "foo.ipynb")) + assert "AUTOTEST" not in foo + + def test_autotests_simple(self, course_dir, temp_cwd): + """Can a notebook with simple autotests be generated with an assignment-specific yaml, and is autotest code removed?""" + self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "source", "ps1", "foo.ipynb")) + self._copy_file(join("files", "tests.yml"), join(course_dir, "source", "ps1", "tests.yml")) + run_nbgrader(["db", "assignment", "add", "ps1"]) + run_nbgrader(["generate_assignment", "ps1"]) + assert os.path.isfile(join(course_dir, "release", "ps1", "foo.ipynb")) + assert os.path.isfile(join(course_dir, "release", "ps1", "tests.yml")) + + foo = self._file_contents(join(course_dir, "release", "ps1", "foo.ipynb")) + assert "AUTOTEST" not in foo + + def test_autotests_needs_yaml(self, course_dir, temp_cwd): + """Can a notebook with autotests be generated without a yaml file?""" + self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "source", "ps1", "foo.ipynb")) + run_nbgrader(["db", "assignment", "add", "ps1"]) + run_nbgrader(["generate_assignment", "ps1"], retcode=1) + + + def test_autotests_fancy(self, course_dir, temp_cwd): + """Can a more complicated autotests notebook be generated, and is autotest code removed?""" + self._copy_file(join("files", "autotest-multi.ipynb"), join(course_dir, "source", "ps1", "foo.ipynb")) + self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + run_nbgrader(["db", "assignment", "add", "ps1"]) + run_nbgrader(["generate_assignment", "ps1"]) + assert os.path.isfile(join(course_dir, "release", "ps1", "foo.ipynb")) + + foo = self._file_contents(join(course_dir, "release", "ps1", "foo.ipynb")) + assert "AUTOTEST" not in foo + + def test_autotests_hidden(self, course_dir, temp_cwd): + """Can a notebook with hidden autotest be generated, and is autotest/hidden sections removed?""" + self._copy_file(join("files", "autotest-hidden.ipynb"), join(course_dir, "source", "ps1", "foo.ipynb")) + self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + run_nbgrader(["db", "assignment", "add", "ps1"]) + run_nbgrader(["generate_assignment", "ps1"]) + assert os.path.isfile(join(course_dir, "release", "ps1", "foo.ipynb")) + + foo = self._file_contents(join(course_dir, "release", "ps1", "foo.ipynb")) + assert "AUTOTEST" not in foo + assert "HIDDEN" not in foo + + def test_autotests_hashed(self, course_dir, temp_cwd): + """Can a notebook with hashed autotests be generated, and is hashed autotest code removed?""" + self._copy_file(join("files", "autotest-hashed.ipynb"), join(course_dir, "source", "ps1", "foo.ipynb")) + self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + run_nbgrader(["db", "assignment", "add", "ps1"]) + run_nbgrader(["generate_assignment", "ps1"]) + assert os.path.isfile(join(course_dir, "release", "ps1", "foo.ipynb")) + + foo = self._file_contents(join(course_dir, "release", "ps1", "foo.ipynb")) + assert "AUTOTEST" not in foo + assert "HASHED" not in foo + + def test_generate_source_with_tests_flag(self, course_dir, temp_cwd): + """Does setting the flag --generate_source_with_tests also create a notebook with solution and tests in the + source_with_tests directory""" + self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "source", "ps1", "foo.ipynb")) + self._copy_file(join("files", "tests.yml"), join(course_dir, "source", "ps1", "tests.yml")) + run_nbgrader(["db", "assignment", "add", "ps1"]) + run_nbgrader(["generate_assignment", "ps1", "--generate_source_with_tests"]) + assert os.path.isfile(join(course_dir, "release", "ps1", "foo.ipynb")) + assert os.path.isfile(join(course_dir, "source_with_tests", "ps1", "foo.ipynb")) + + foo = self._file_contents(join(course_dir, "source_with_tests", "ps1", "foo.ipynb")) + assert "AUTOTEST" not in foo + assert "BEGIN SOLUTION" in foo + assert "END SOLUTION" in foo + assert "raise NotImplementedError" not in foo + def test_deprecation(self, course_dir, temp_cwd): """Can a single file be assigned?""" self._empty_notebook(join(course_dir, 'source', 'ps1', 'foo.ipynb')) diff --git a/nbgrader/tests/apps/test_nbgrader_generate_feedback.py b/nbgrader/tests/apps/test_nbgrader_generate_feedback.py index 6b59bfab9..6ff20a70b 100644 --- a/nbgrader/tests/apps/test_nbgrader_generate_feedback.py +++ b/nbgrader/tests/apps/test_nbgrader_generate_feedback.py @@ -326,6 +326,41 @@ def test_update_newer_single_notebook(self, course_dir): assert p1 != self._file_contents(join(course_dir, "feedback", "foo", "ps1", "p1.html")) assert p2 == self._file_contents(join(course_dir, "feedback", "foo", "ps1", "p2.html")) + def test_autotests(self, course_dir): + """Can feedback be generated for an assignment with autotests?""" + run_nbgrader(["db", "assignment", "add", "ps1"]) + run_nbgrader(["db", "student", "add", "foo"]) + + self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "source", "ps1", "p1.ipynb")) + self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "source", "ps1", "p2.ipynb")) + self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + run_nbgrader(["generate_assignment", "ps1"]) + + self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "submitted", "foo", "ps1", "p1.ipynb")) + self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "submitted", "foo", "ps1", "p2.ipynb")) + self._make_file(join(course_dir, "submitted", "foo", "ps1", "timestamp.txt"), "2015-02-02 15:58:23.948203 America/Los_Angeles") + run_nbgrader(["autograde", "ps1"]) + run_nbgrader(["generate_feedback", "ps1"]) + + assert exists(join(course_dir, "feedback", "foo", "ps1", "p1.html")) + assert exists(join(course_dir, "feedback", "foo", "ps1", "p2.html")) + assert isfile(join(course_dir, "feedback", "foo", "ps1", "timestamp.txt")) + assert self._file_contents(join(course_dir, "feedback", "foo", "ps1", "timestamp.txt")) == "2015-02-02 15:58:23.948203 America/Los_Angeles" + p1 = self._file_contents(join(course_dir, "feedback", "foo", "ps1", "p1.html")) + p2 = self._file_contents(join(course_dir, "feedback", "foo", "ps1", "p2.html")) + + self._empty_notebook(join(course_dir, "autograded", "foo", "ps1", "p1.ipynb")) + self._empty_notebook(join(course_dir, "autograded", "foo", "ps1", "p2.ipynb")) + self._make_file(join(course_dir, "autograded", "foo", "ps1", "timestamp.txt"), "2015-02-02 16:58:23.948203 America/Los_Angeles") + run_nbgrader(["generate_feedback", "ps1", "--notebook", "p1"]) + + assert exists(join(course_dir, "feedback", "foo", "ps1", "p1.html")) + assert exists(join(course_dir, "feedback", "foo", "ps1", "p2.html")) + assert isfile(join(course_dir, "feedback", "foo", "ps1", "timestamp.txt")) + assert self._file_contents(join(course_dir, "feedback", "foo", "ps1", "timestamp.txt")) == "2015-02-02 16:58:23.948203 America/Los_Angeles" + assert p1 != self._file_contents(join(course_dir, "feedback", "foo", "ps1", "p1.html")) + assert p2 == self._file_contents(join(course_dir, "feedback", "foo", "ps1", "p2.html")) + def test_single_user(self, course_dir): run_nbgrader(["db", "assignment", "add", "ps1", "--duedate", "2015-02-02 14:58:23.948203 America/Los_Angeles"]) diff --git a/nbgrader/tests/preprocessors/test_instantiatetests.py b/nbgrader/tests/preprocessors/test_instantiatetests.py new file mode 100644 index 000000000..ac3328557 --- /dev/null +++ b/nbgrader/tests/preprocessors/test_instantiatetests.py @@ -0,0 +1,204 @@ +import pytest +import os +from textwrap import dedent +from ...preprocessors import InstantiateTests +from .base import BaseTestPreprocessor +from .. import create_code_cell, create_text_cell, create_autotest_solution_cell, create_autotest_test_cell +from nbformat.v4 import new_notebook +from nbclient.client import NotebookClient + + +@pytest.fixture +def preprocessor(): + return InstantiateTests() + + +class TestInstantiateTests(BaseTestPreprocessor): + + def test_load_test_template_file(self, preprocessor): + resources = { + 'kernel_name': 'python3', + 'metadata': {'path': 'nbgrader/docs/source/user_guide'} + } + preprocessor._load_test_template_file(resources=resources) + assert preprocessor.test_templates_by_type is not None + assert preprocessor.dispatch_template is not None + assert preprocessor.success_code is not None + assert preprocessor.hash_template is not None + assert preprocessor.check_template is not None + assert preprocessor.normalize_template is not None + assert preprocessor.setup_code is not None + + def test_has_sanitizers(self, preprocessor): + assert 'python' in preprocessor.sanitizers.keys() + assert 'python3' in preprocessor.sanitizers.keys() + assert 'ir' in preprocessor.sanitizers.keys() + + def test_has_comment_strs(self, preprocessor): + assert 'python' in preprocessor.comment_strs.keys() + assert 'python3' in preprocessor.comment_strs.keys() + assert 'ir' in preprocessor.comment_strs.keys() + + def test_replace_autotest_code(self, preprocessor): + sol_cell = create_autotest_solution_cell() + test_cell = create_autotest_test_cell() + test_cell.metadata['nbgrader'] = {'grade': True} + nb = new_notebook() + nb.metadata['kernelspec'] = { + "name": "python3" + } + nb.cells.append(sol_cell) + nb.cells.append(test_cell) + resources = { + 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} + } + nb, resources = preprocessor.preprocess(nb, resources) + assert 'assert' in nb['cells'][1]['source'] + + # test that a warning is thrown when we set enforce_metadata = False and have an AUTOTEST directive in a + # non-grade cell + def test_warning_autotest_nongrade(self, preprocessor, caplog): + preprocessor.enforce_metadata = False + sol_cell = create_autotest_solution_cell() + test_cell = create_autotest_test_cell() + test_cell.metadata['nbgrader'] = {'grade': False} + nb = new_notebook() + nb.metadata['kernelspec'] = { + "name": "python3" + } + nb.cells.append(sol_cell) + nb.cells.append(test_cell) + resources = { + 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} + } + + nb, resources = preprocessor.preprocess(nb, resources) + assert "Autotest region detected in a non-grade cell; " in caplog.text + + # test that an error is thrown when we have an AUTOTEST directive in a non-grade cell + def test_error_autotest_nongrade(self, preprocessor, caplog): + sol_cell = create_autotest_solution_cell() + test_cell = create_autotest_test_cell() + test_cell.metadata['nbgrader'] = {'grade': False} + nb = new_notebook() + nb.metadata['kernelspec'] = { + "name": "python3" + } + nb.cells.append(sol_cell) + nb.cells.append(test_cell) + resources = { + 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} + } + with pytest.raises(Exception): + nb, resources = preprocessor.preprocess(nb, resources) + + assert "Autotest region detected in a non-grade cell; " in caplog.text + + # test that invalid python statements in AUTOTEST directives cause errors + def test_error_bad_autotest_code(self, preprocessor): + sol_cell = create_autotest_solution_cell() + test_cell = create_autotest_test_cell() + test_cell.source = """ + ### AUTOTEST length(answer) + """ + test_cell.metadata['nbgrader'] = {'grade': True} + nb = new_notebook() + nb.metadata['kernelspec'] = { + "name": "python3" + } + nb.cells.append(sol_cell) + nb.cells.append(test_cell) + resources = { + 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} + } + with pytest.raises(Exception): + nb, resources = preprocessor.preprocess(nb, resources) + + # test the code generated for some basic types; ensure correct solution gives success, a few wrong solutions give + # failures + def test_int_autotest(self, preprocessor): + sol_cell = create_autotest_solution_cell() + sol_cell.source = """ + answer = 7 + """ + test_cell = create_autotest_test_cell() + + test_cell.metadata['nbgrader'] = {'grade': True} + nb = new_notebook() + nb.metadata['kernelspec'] = { + "name": "python3" + } + nb.cells.append(sol_cell) + nb.cells.append(test_cell) + resources = { + 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} + } + nb, resources = preprocessor.preprocess(nb, resources) + executed_nb = NotebookClient(nb=nb).execute() + assert executed_nb['cells'][1]['outputs'][0]['text'] == 'Success!\n' + + def test_float_autotest(self, preprocessor): + sol_cell = create_autotest_solution_cell() + sol_cell.source = """ + answer = 7.7 + """ + test_cell = create_autotest_test_cell() + + test_cell.metadata['nbgrader'] = {'grade': True} + nb = new_notebook() + nb.metadata['kernelspec'] = { + "name": "python3" + } + nb.cells.append(sol_cell) + nb.cells.append(test_cell) + resources = { + 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} + } + nb, resources = preprocessor.preprocess(nb, resources) + executed_nb = NotebookClient(nb=nb).execute() + assert executed_nb['cells'][1]['outputs'][0]['text'] == 'Success!\n' + + def test_string_autotest(self, preprocessor): + sol_cell = create_autotest_solution_cell() + sol_cell.source = """ + answer = 'seven' + """ + test_cell = create_autotest_test_cell() + + test_cell.metadata['nbgrader'] = {'grade': True} + nb = new_notebook() + nb.metadata['kernelspec'] = { + "name": "python3" + } + nb.cells.append(sol_cell) + nb.cells.append(test_cell) + resources = { + 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} + } + nb, resources = preprocessor.preprocess(nb, resources) + executed_nb = NotebookClient(nb=nb).execute() + assert executed_nb['cells'][1]['outputs'][0]['text'] == 'Success!\n' + + def test_list_autotest(self, preprocessor): + sol_cell = create_autotest_solution_cell() + sol_cell.source = """ + answer = [1, 2, 3, 4, 5, 6, 7] + """ + test_cell = create_autotest_test_cell() + + test_cell.metadata['nbgrader'] = {'grade': True} + nb = new_notebook() + nb.metadata['kernelspec'] = { + "name": "python3" + } + nb.cells.append(sol_cell) + nb.cells.append(test_cell) + resources = { + 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} + } + nb, resources = preprocessor.preprocess(nb, resources) + executed_nb = NotebookClient(nb=nb).execute() + assert executed_nb['cells'][1]['outputs'][0]['text'] == 'Success!\n' + + + diff --git a/pyproject.toml b/pyproject.toml index d52bfeeba..46213205a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ dependencies = [ "rapidfuzz>=1.8", "requests>=2.26", "sqlalchemy>=1.4,<3", + "PyYAML>=6.0" ] version = "0.9.0a1" From 19b089ef62eff30b90032e08cce5846d3de68d27 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Mon, 21 Aug 2023 14:06:53 -0700 Subject: [PATCH 02/46] added docs html files for new problems to .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 3cecd390f..7da289b32 100644 --- a/.gitignore +++ b/.gitignore @@ -84,14 +84,18 @@ nbgrader/docs/source/user_guide/managing_assignment_files_manually.rst nbgrader/docs/source/user_guide/managing_the_database.rst nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/problem1.html nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/problem2.html +nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/problem3.html nbgrader/docs/source/user_guide/autograded/hacker/ps1/problem1.html nbgrader/docs/source/user_guide/autograded/hacker/ps1/problem2.html +nbgrader/docs/source/user_guide/autograded/hacker/ps1/problem3.html nbgrader/docs/source/user_guide/downloaded/ps1/archive/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.html nbgrader/docs/source/user_guide/release/ps1/problem1.html nbgrader/docs/source/user_guide/release/ps1/problem2.html +nbgrader/docs/source/user_guide/release/ps1/problem3.html nbgrader/docs/source/user_guide/source/header.html nbgrader/docs/source/user_guide/source/ps1/problem1.html nbgrader/docs/source/user_guide/source/ps1/problem2.html +nbgrader/docs/source/user_guide/source/ps1/problem3.html # components stuff node_modules From 67052278a612d57b97ea05fa9c219b83a54ad6cf Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Mon, 21 Aug 2023 15:24:47 -0700 Subject: [PATCH 03/46] minor bugfix: shortened --source_with_tests flag in tests --- nbgrader/tests/apps/test_nbgrader_generate_assignment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nbgrader/tests/apps/test_nbgrader_generate_assignment.py b/nbgrader/tests/apps/test_nbgrader_generate_assignment.py index 1defff6e9..444f1f08f 100644 --- a/nbgrader/tests/apps/test_nbgrader_generate_assignment.py +++ b/nbgrader/tests/apps/test_nbgrader_generate_assignment.py @@ -114,12 +114,12 @@ def test_autotests_hashed(self, course_dir, temp_cwd): assert "HASHED" not in foo def test_generate_source_with_tests_flag(self, course_dir, temp_cwd): - """Does setting the flag --generate_source_with_tests also create a notebook with solution and tests in the + """Does setting the flag --source_with_tests also create a notebook with solution and tests in the source_with_tests directory""" self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "source", "ps1", "foo.ipynb")) self._copy_file(join("files", "tests.yml"), join(course_dir, "source", "ps1", "tests.yml")) run_nbgrader(["db", "assignment", "add", "ps1"]) - run_nbgrader(["generate_assignment", "ps1", "--generate_source_with_tests"]) + run_nbgrader(["generate_assignment", "ps1", "--source_with_tests"]) assert os.path.isfile(join(course_dir, "release", "ps1", "foo.ipynb")) assert os.path.isfile(join(course_dir, "source_with_tests", "ps1", "foo.ipynb")) From d823f3e224a60a224f21c02ea46e5ef699e4a8d9 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Mon, 21 Aug 2023 17:58:10 -0700 Subject: [PATCH 04/46] output for source_with_tests set to the right directory --- nbgrader/converters/generate_source_with_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbgrader/converters/generate_source_with_tests.py b/nbgrader/converters/generate_source_with_tests.py index 346fc4f2e..2cebdb87b 100644 --- a/nbgrader/converters/generate_source_with_tests.py +++ b/nbgrader/converters/generate_source_with_tests.py @@ -26,7 +26,7 @@ def _input_directory(self) -> str: @property def _output_directory(self) -> str: - return self.coursedir.release_directory + return self.coursedir.source_with_tests_directory preprocessors = List([ InstantiateTests, From 48837d9057a0c133983ea14ef490b52e7f3bbf14 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Tue, 22 Aug 2023 18:04:36 -0700 Subject: [PATCH 05/46] simplified kernel execution code --- nbgrader/preprocessors/instantiatetests.py | 515 ++++----------------- 1 file changed, 98 insertions(+), 417 deletions(-) diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index c208af9a8..d21e61814 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -5,7 +5,6 @@ from .. import utils from traitlets import Bool, List, Integer, Unicode, Dict, Callable from textwrap import dedent -from . import Execute import secrets import asyncio import inspect @@ -21,83 +20,16 @@ CellTimeoutError, DeadKernelError, ) +from . import NbGraderPreprocessor +from jupyter_client.manager import start_new_kernel try: from time import monotonic # Py 3 except ImportError: from time import time as monotonic # Py 2 - -######################################################################################### -class CellExecutionComplete(Exception): - """ - Used as a control signal for cell execution across run_cell and - process_message function calls. Raised when all execution requests - are completed and no further messages are expected from the kernel - over zeromq channels. - """ - pass - - ######################################################################################### -class CellExecutionError(Exception): - """ - Custom exception to propagate exceptions that are raised during - notebook execution to the caller. This is mostly useful when - using nbconvert as a library, since it allows dealing with - failures gracefully. - """ - - # ------------------------------------------------------------------------------------- - def __init__(self, traceback): - super(CellExecutionError, self).__init__(traceback) - self.traceback = traceback - - # ------------------------------------------------------------------------------------- - def __str__(self): - s = self.__unicode__() - if not isinstance(s, str): - s = s.encode('utf8', 'replace') - return s - - # ------------------------------------------------------------------------------------- - def __unicode__(self): - return self.traceback - - # ------------------------------------------------------------------------------------- - @classmethod - def from_code_and_msg(cls, code, msg): - """Instantiate from a code cell object and a message contents - (message is either execute_reply or error) - """ - tb = '\n'.join(msg.get('traceback', [])) - return cls(exec_err_msg.format(code=code, traceback=tb)) - # ------------------------------------------------------------------------------------- - - -######################################################################################### -class CodeExecutionError(Exception): - """ - Custom exception to propagate exceptions that are raised during - code snippet execution to the caller. This is mostly useful when - using nbconvert as a library, since it allows dealing with - failures gracefully. - """ - - -######################################################################################### - -exec_err_msg = u"""\ -An error occurred while executing the following code: ------------------- -{code} ------------------- -{traceback} -""" - - -######################################################################################### -class InstantiateTests(Execute): +class InstantiateTests(NbGraderPreprocessor): tests = None autotest_filename = Unicode( @@ -167,44 +99,64 @@ class InstantiateTests(Execute): ).tag(config=True) sanitizer = None - global_tests_loaded = False + kernel_name = None + kc = None + execute_result = None def preprocess(self, nb, resources): # avoid starting the kernel at all/processing the notebook if there are no autotest delimiters for index, cell in enumerate(nb.cells): - # ignore non-code cells - if cell.cell_type != 'code': - continue # look for an autotest delimiter in this cell's source; if we find one, process this notebook - if self.autotest_delimiter in cell.source: + # short-circuit ignore non-code cells + if (cell.cell_type == 'code') and (self.autotest_delimiter in cell.source): + # get the kernel name from the notebook + kernel_name = nb.metadata.get("kernelspec", {}).get("name", "") + if kernel_name not in self.comment_strs: + raise ValueError(f"Kernel {kernel_name} has not been specified in InstantiateTests.comment_strs") + if kernel_name not in self.sanitizers: + raise ValueError(f"Kernel {kernel_name} has not been specified in InstantiateTests.sanitizers") + self.log.debug(f"Found kernel {kernel_name}") + self.kernel_name = kernel_name + + # load the template tests file + self.log.debug('Loading template tests file') + self._load_test_template_file(resources) + self.global_tests_loaded = True + + # set up the sanitizer + self.log.debug('Setting sanitizer for kernel {kernel_name}') + self.sanitizer = self.sanitizers[kernel_name] + #start the kernel + self.log.debug('Starting client for kernel {kernel_name}') + km, self.kc = start_new_kernel(kernel_name = kernel_name) + + # run the preprocessor + self.log.debug('Running InstantiateTests preprocessor') nb, resources = super(InstantiateTests, self).preprocess(nb, resources) + + # shut down and cleanup the kernel + self.log.debug('Shutting down / cleaning up kernel') + km.shutdown_kernel() + self.kc = None + self.sanitizer = None + self.execute_result = None + + # return the modified notebook return nb, resources + # if not, just return return nb, resources def preprocess_cell(self, cell, resources, index): - # new_lines will store the replacement code after autotest template instantiation - new_lines = [] - - # first, run the cell normally - # cell, resources = super(InstantiateTests, self).preprocess_cell(cell, resources, index) - - kernel_name = self.nb.metadata.get("kernelspec", {}).get("name", "") - if kernel_name not in self.comment_strs: - raise ValueError( - "kernel '{}' has not been specified in " - "InstantiateTests.comment_strs".format(kernel_name)) - resources["kernel_name"] = kernel_name - - # if it's not a code cell, or it's empty, just return - if cell.cell_type != 'code': + # if it's not a code cell, or if the cell's source is empty, just return + if (cell.cell_type != 'code') or (len(cell.source) == 0): return cell, resources # determine whether the cell is a grade cell is_grade_flag = utils.is_grade(cell) # get the comment string for this language - comment_str = self.comment_strs[resources['kernel_name']] + comment_str = self.comment_strs[self.kernel_name] # split the code lines into separate strings lines = cell.source.split("\n") @@ -213,12 +165,10 @@ def preprocess_cell(self, cell, resources, index): non_autotest_code_lines = [] - if self.sanitizer is None: - self.log.debug('Setting sanitizer for language ' + resources['kernel_name']) - self.sanitizer = self.sanitizers.get(resources['kernel_name'], lambda x: x) + # new_lines will store the replacement code after autotest template instantiation + new_lines = [] for line in lines: - # if the current line doesn't have the autotest_delimiter or is not a comment # then just append the line to the new cell code and go to the next line if self.autotest_delimiter not in line or line.strip()[:len(comment_str)] != comment_str: @@ -227,20 +177,20 @@ def preprocess_cell(self, cell, resources, index): continue # run all code lines prior to the current line containing the autotest_delimiter - asyncio.run(self._async_execute_code_snippet("\n".join(non_autotest_code_lines))) + self._execute_code_snippet("\n".join(non_autotest_code_lines)) non_autotest_code_lines = [] # there are autotests; we should check that it is a grading cell if not is_grade_flag: if not self.enforce_metadata: self.log.warning( - "Autotest region detected in a non-grade cell; " + "AutoTest region detected in a non-grade cell; " "please make sure all autotest regions are within " "'Autograder tests' cells." ) else: self.log.error( - "Autotest region detected in a non-grade cell; " + "AutoTest region detected in a non-grade cell; " "please make sure all autotest regions are within " "'Autograder tests' cells." ) @@ -248,25 +198,17 @@ def preprocess_cell(self, cell, resources, index): self.log.debug('') self.log.debug('') - self.log.debug('Autotest delimiter found on line. Preprocessing...') + self.log.debug('AutoTest delimiter found on line. Preprocessing...') - # the first time we run into an autotest delimiter, obtain the - # tests object from the tests.yml template file for the assignment - # and append any setup code to the cell block we're in - # also figure out what language we're using - - # loading the template tests file - if not self.global_tests_loaded: - self.log.debug('Loading tests template file') - self._load_test_template_file(resources) - self.global_tests_loaded = True + # the first time we run into an autotest delimiter, + # append any setup code to the cell block we're in # if the setup_code is successfully obtained from the template file and # the current cell does not already have the setup code, add the setup_code if (self.setup_code is not None) and (not setup_code_inserted_into_cell): new_lines.append(self.setup_code) setup_code_inserted_into_cell = True - asyncio.run(self._async_execute_code_snippet(self.setup_code)) + self._execute_code_snippet(self.setup_code) # decide whether to use hashing based on whether the self.hashed_delimiter token # appears in the line before the self.autotest_delimiter token @@ -319,7 +261,7 @@ def preprocess_cell(self, cell, resources, index): # run the trailing non-autotest lines, if any remain if len(non_autotest_code_lines) > 0: - asyncio.run(self._async_execute_code_snippet("\n".join(non_autotest_code_lines))) + self._execute_code_snippet("\n".join(non_autotest_code_lines)) # add the final success message if is_grade_flag and self.global_tests_loaded: @@ -341,7 +283,7 @@ def _load_test_template_file(self, resources): or perhaps cannot be loaded, it will attempt to load the default_tests.yaml file with the course_directory """ self.log.debug('loading template tests.yml...') - self.log.debug('kernel_name: ' + resources["kernel_name"]) + self.log.debug(f'kernel_name: {self.kernel_name}') try: with open(os.path.join(resources['metadata']['path'], self.autotest_filename), 'r') as tests_file: tests = yaml.safe_load(tests_file) @@ -371,7 +313,7 @@ def _load_test_template_file(self, resources): raise # get kernel specific data - tests = tests[resources["kernel_name"]] + tests = tests[self.kernel_name] # get the test templates self.test_templates_by_type = tests['templates'] @@ -399,7 +341,7 @@ def _instantiate_tests(self, snippet, salt=None): # get the type of the snippet output (used to dispatch autotest) template = j2.Environment(loader=j2.BaseLoader).from_string(self.dispatch_template) dispatch_code = template.render(snippet=snippet) - dispatch_result = asyncio.run(self._async_execute_code_snippet(dispatch_code)) + dispatch_result = self._execute_code_snippet(dispatch_code) self.log.debug('Dispatch result returned by kernel: ', dispatch_result) # get the test code; if the type isn't in our dict, just default to 'default' # if default isn't in the tests code, this will throw an error @@ -449,310 +391,49 @@ def _instantiate_tests(self, snippet, salt=None): template = j2.Environment(loader=j2.BaseLoader).from_string(templ) instantiated_test = template.render(snippet=snippet) # run the instantiated template code - test_value = asyncio.run(self._async_execute_code_snippet(instantiated_test)) + test_value = self._execute_code_snippet(instantiated_test) instantiated_tests.append(instantiated_test) test_values.append(test_value) return instantiated_tests, test_values, rendered_fail_msgs - # ------------------------------------------------------------------------------------- - - ######################### - # async version of nbgrader interaction with kernel - # the below functions were adapted from the jupyter/nbclient GitHub repo, commit: - # https://github.com/jupyter/nbclient/commit/0c08e27c1ec655cffe9b35cf637da742cdab36e8 - ######################### - - # ------------------------------------------------------------------------------------- - # adapted from nbclient.util.ensure_async - async def _ensure_async(self, obj): - """Convert a non-awaitable object to a coroutine if needed, - and await it if it was not already awaited. - adapted from nbclient.util._ensure_async - """ - if inspect.isawaitable(obj): - try: - result = await obj - except RuntimeError as e: - if str(e) == 'cannot reuse already awaited coroutine': - return obj - raise - return result - return obj - - # ------------------------------------------------------------------------------------- - # adapted from nbclient.client._async_handle_timeout - async def _async_handle_timeout(self, timeout: int) -> None: - - self.log.error("Timeout waiting for execute reply (%is)." % timeout) - if self.interrupt_on_timeout: - self.log.error("Interrupting kernel") - assert self.km is not None - await _ensure_async(self.km.interrupt_kernel()) - else: - raise CellTimeoutError.error_from_timeout_and_cell( - "Cell execution timed out", timeout - ) - - # ------------------------------------------------------------------------------------- - # adapted from nbclient.client._async_check_alive - async def _async_check_alive(self) -> None: - assert self.kc is not None - if not await self._ensure_async(self.kc.is_alive()): - self.log.error("Kernel died while waiting for execute reply.") - raise DeadKernelError("Kernel died") - - # ------------------------------------------------------------------------------------- - # adapted from nbclient.client._async_poll_output_msg - async def _async_poll_output_msg_code( - self, parent_msg_id: str, code - ) -> None: - - assert self.kc is not None - while True: - msg = await self._ensure_async(self.kc.iopub_channel.get_msg(timeout=None)) - if msg['parent_header'].get('msg_id') == parent_msg_id: - try: - msg_type = msg['msg_type'] - self.log.debug("msg_type: %s", msg_type) - content = msg['content'] - self.log.debug("content: %s", content) - - if msg_type in {'execute_result', 'display_data', 'update_display_data'}: - return self.sanitizer(content['data']['text/plain']) - - if msg_type == 'error': - self.log.error("Failed to run code: \n%s", code) - self.log.error("Runtime error from the kernel: \n%s", content['evalue']) - raise CodeExecutionError() - - if msg_type == 'status': - if content['execution_state'] == 'idle': - raise CellExecutionComplete() - - except CellExecutionComplete: - return - - # ------------------------------------------------------------------------------------- - # adapted from nbclient.client.async_wait_for_reply - async def _async_wait_for_reply( - self, msg_id: str, cell: t.Optional[NotebookNode] = None - ) -> t.Optional[t.Dict]: - - assert self.kc is not None - # wait for finish, with timeout - timeout = self._get_timeout(cell) - cummulative_time = 0 - while True: - try: - msg = await _ensure_async( - self.kc.shell_channel.get_msg(timeout=self.shell_timeout_interval) - ) - except Empty: - await self._async_check_alive() - cummulative_time += self.shell_timeout_interval - if timeout and cummulative_time > timeout: - await self._async_async_handle_timeout(timeout, cell) - break - else: - if msg['parent_header'].get('msg_id') == msg_id: - return msg - return None - - # ------------------------------------------------------------------------------------- - # adapted from nbclient.client._async_poll_for_reply - async def _async_poll_for_reply_code( - self, - msg_id: str, - timeout: t.Optional[int], - task_poll_output_msg: asyncio.Future, - task_poll_kernel_alive: asyncio.Future, - ) -> t.Dict: - - assert self.kc is not None - - self.log.debug("Executing _async_poll_for_reply:\n%s", msg_id) - - if timeout is not None: - deadline = monotonic() + timeout - new_timeout = float(timeout) - - while True: - try: - shell_msg = await self._ensure_async(self.kc.shell_channel.get_msg(timeout=new_timeout)) - if shell_msg['parent_header'].get('msg_id') == msg_id: - try: - msg = await asyncio.wait_for(task_poll_output_msg, new_timeout) - except (asyncio.TimeoutError, Empty): - task_poll_kernel_alive.cancel() - raise CellExecutionError("Timeout waiting for IOPub output") - self.log.debug("Get _async_poll_for_reply:\n%s", msg) - - return msg if msg != None else "" - else: - if new_timeout is not None: - new_timeout = max(0, deadline - monotonic()) - except Empty: - self.log.debug("Empty _async_poll_for_reply:\n%s", msg_id) - task_poll_kernel_alive.cancel() - await self._async_check_alive() - await self._async_handle_timeout() - - # ------------------------------------------------------------------------------------- - # adapted from nbclient.client.async_execute_cell - async def _async_execute_code_snippet(self, code): - assert self.kc is not None - - self.log.debug("Executing cell:\n%s", code) - - parent_msg_id = await self._ensure_async(self.kc.execute(code, stop_on_error=not self.allow_errors)) - - task_poll_kernel_alive = asyncio.ensure_future(self._async_check_alive()) - - task_poll_output_msg = asyncio.ensure_future(self._async_poll_output_msg_code(parent_msg_id, code)) - - task_poll_for_reply = asyncio.ensure_future( - self._async_poll_for_reply_code(parent_msg_id, self.timeout, task_poll_output_msg, task_poll_kernel_alive)) - - try: - msg = await task_poll_for_reply - except asyncio.CancelledError: - # can only be cancelled by task_poll_kernel_alive when the kernel is dead - task_poll_output_msg.cancel() - raise DeadKernelError("Kernel died") - except Exception as e: - # Best effort to cancel request if it hasn't been resolved - try: - # Check if the task_poll_output is doing the raising for us - if not isinstance(e, CellControlSignal): - task_poll_output_msg.cancel() - finally: - raise - - return msg - - # ------------------------------------------------------------------------------------- - async def async_execute_cell( - self, - cell: NotebookNode, - cell_index: int, - execution_count: t.Optional[int] = None, - store_history: bool = True, - ) -> NotebookNode: - """ - Executes a single code cell. - - To execute all cells see :meth:`execute`. - - Parameters - ---------- - cell : nbformat.NotebookNode - The cell which is currently being processed. - cell_index : int - The position of the cell within the notebook object. - execution_count : int - The execution count to be assigned to the cell (default: Use kernel response) - store_history : bool - Determines if history should be stored in the kernel (default: False). - Specific to ipython kernels, which can store command histories. - - Returns - ------- - output : dict - The execution output payload (or None for no output). - - Raises - ------ - CellExecutionError - If execution failed and should raise an exception, this will be raised - with defaults about the failure. - - Returns - ------- - cell : NotebookNode - The cell which was just processed. - """ - assert self.kc is not None - - await run_hook(self.on_cell_start, cell=cell, cell_index=cell_index) - - if cell.cell_type != 'code' or not cell.source.strip(): - self.log.debug("Skipping non-executing cell %s", cell_index) - return cell - - if self.skip_cells_with_tag in cell.metadata.get("tags", []): - self.log.debug("Skipping tagged cell %s", cell_index) - return cell - - if self.record_timing: # clear execution metadata prior to execution - cell['metadata']['execution'] = {} - - self.log.debug("Executing cell:\n%s", cell.source) - - cell_allows_errors = (not self.force_raise_errors) and ( - self.allow_errors or "raises-exception" in cell.metadata.get("tags", []) - ) - - await run_hook(self.on_cell_execute, cell=cell, cell_index=cell_index) - parent_msg_id = await _ensure_async( - self.kc.execute( - cell.source, store_history=store_history, stop_on_error=not cell_allows_errors - ) - ) - await run_hook(self.on_cell_complete, cell=cell, cell_index=cell_index) - # We launched a code cell to execute - self.code_cells_executed += 1 - exec_timeout = self._get_timeout(cell) - - cell.outputs = [] - self.clear_before_next_output = False - - task_poll_kernel_alive = asyncio.ensure_future(self._async_poll_kernel_alive()) - task_poll_output_msg = asyncio.ensure_future( - self._async_poll_output_msg_code(parent_msg_id, code) - ) - self.task_poll_for_reply = asyncio.ensure_future( - self._async_poll_for_reply_code( - parent_msg_id, exec_timeout, task_poll_output_msg, task_poll_kernel_alive - ) - ) - try: - exec_reply = await self.task_poll_for_reply - except asyncio.CancelledError: - # can only be cancelled by task_poll_kernel_alive when the kernel is dead - task_poll_output_msg.cancel() - raise DeadKernelError("Kernel died") - except Exception as e: - # Best effort to cancel request if it hasn't been resolved - try: - # Check if the task_poll_output is doing the raising for us - if not isinstance(e, CellControlSignal): - task_poll_output_msg.cancel() - finally: - raise - - if execution_count: - cell['execution_count'] = execution_count - await self._check_raise_for_error(cell, cell_index, exec_reply) - self.nb['cells'][cell_index] = cell - return cell - # ------------------------------------------------------------------------------------- - - -def timestamp(msg: Optional[Dict] = None) -> str: - if msg and 'header' in msg: # The test mocks don't provide a header, so tolerate that - msg_header = msg['header'] - if 'date' in msg_header and isinstance(msg_header['date'], datetime.datetime): - try: - # reformat datetime into expected format - formatted_time = datetime.datetime.strftime( - msg_header['date'], '%Y-%m-%dT%H:%M:%S.%fZ' - ) - if ( - formatted_time - ): # docs indicate strftime may return empty string, so let's catch that too - return formatted_time - except Exception: - pass # fallback to a local time - - return datetime.datetime.utcnow().isoformat() + 'Z' + def _execute_code_snippet(self, code): + self.log.debug("Executing code:\n%s", code) + self.kc.execute_interactive(code, output_hook = self._execute_code_snippet_output_hook) + res = self.execute_result + self.execute_result = None + self.log.debug("Result:\n%s", res) + return res + + def _execute_code_snippet_output_hook(self, msg: t.Dict[str, t.Any]) -> None: + msg_type = msg["header"]["msg_type"] + content = msg["content"] + if msg_type == "stream": + pass + #stream = getattr(sys, content["name"]) + #stream.write(content["text"]) + elif msg_type in ("display_data", "update_display_data", "execute_result"): + self.execute_result = self.sanitizer(content["data"]["text/plain"]) + elif msg_type == "error": + self.log.error("Runtime error from the kernel: \n%s\n%s\n%s", content['ename'], content['evalue'], content['traceback']) + raise CellExecutionError(content['traceback'], content['ename'], content['evalue']) + return + +## TODO: do we need this? commenting out for now; will add back in if it causes errors +#def timestamp(msg: Optional[Dict] = None) -> str: +# if msg and 'header' in msg: # The test mocks don't provide a header, so tolerate that +# msg_header = msg['header'] +# if 'date' in msg_header and isinstance(msg_header['date'], datetime.datetime): +# try: +# # reformat datetime into expected format +# formatted_time = datetime.datetime.strftime( +# msg_header['date'], '%Y-%m-%dT%H:%M:%S.%fZ' +# ) +# if ( +# formatted_time +# ): # docs indicate strftime may return empty string, so let's catch that too +# return formatted_time +# except Exception: +# pass # fallback to a local time +# +# return datetime.datetime.utcnow().isoformat() + 'Z' From f0140fc224d719be146623ee8fc0906d13d8d738 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Tue, 22 Aug 2023 18:58:10 -0700 Subject: [PATCH 06/46] store kernel name in resources --- nbgrader/preprocessors/instantiatetests.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index d21e61814..ffaea8b7b 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -116,7 +116,7 @@ def preprocess(self, nb, resources): if kernel_name not in self.sanitizers: raise ValueError(f"Kernel {kernel_name} has not been specified in InstantiateTests.sanitizers") self.log.debug(f"Found kernel {kernel_name}") - self.kernel_name = kernel_name + resources["kernel_name"] = kernel_name # load the template tests file self.log.debug('Loading template tests file') @@ -156,7 +156,7 @@ def preprocess_cell(self, cell, resources, index): is_grade_flag = utils.is_grade(cell) # get the comment string for this language - comment_str = self.comment_strs[self.kernel_name] + comment_str = self.comment_strs[resources["kernel_name"]] # split the code lines into separate strings lines = cell.source.split("\n") @@ -283,7 +283,7 @@ def _load_test_template_file(self, resources): or perhaps cannot be loaded, it will attempt to load the default_tests.yaml file with the course_directory """ self.log.debug('loading template tests.yml...') - self.log.debug(f'kernel_name: {self.kernel_name}') + self.log.debug(f'kernel_name: {resources["kernel_name"]}') try: with open(os.path.join(resources['metadata']['path'], self.autotest_filename), 'r') as tests_file: tests = yaml.safe_load(tests_file) @@ -313,7 +313,7 @@ def _load_test_template_file(self, resources): raise # get kernel specific data - tests = tests[self.kernel_name] + tests = tests[resources["kernel_name"]] # get the test templates self.test_templates_by_type = tests['templates'] From 924a26ddaf133ac99bc188beb0ef56456a5e346b Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Tue, 22 Aug 2023 19:16:33 -0700 Subject: [PATCH 07/46] minor ed --- nbgrader/tests/preprocessors/test_instantiatetests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nbgrader/tests/preprocessors/test_instantiatetests.py b/nbgrader/tests/preprocessors/test_instantiatetests.py index ac3328557..bed94506a 100644 --- a/nbgrader/tests/preprocessors/test_instantiatetests.py +++ b/nbgrader/tests/preprocessors/test_instantiatetests.py @@ -73,7 +73,7 @@ def test_warning_autotest_nongrade(self, preprocessor, caplog): } nb, resources = preprocessor.preprocess(nb, resources) - assert "Autotest region detected in a non-grade cell; " in caplog.text + assert "AutoTest region detected in a non-grade cell; " in caplog.text # test that an error is thrown when we have an AUTOTEST directive in a non-grade cell def test_error_autotest_nongrade(self, preprocessor, caplog): @@ -92,7 +92,7 @@ def test_error_autotest_nongrade(self, preprocessor, caplog): with pytest.raises(Exception): nb, resources = preprocessor.preprocess(nb, resources) - assert "Autotest region detected in a non-grade cell; " in caplog.text + assert "AutoTest region detected in a non-grade cell; " in caplog.text # test that invalid python statements in AUTOTEST directives cause errors def test_error_bad_autotest_code(self, preprocessor): From 9ea6577b476baaa5bc54d7f2433d521aea22afc5 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Wed, 23 Aug 2023 13:44:00 -0700 Subject: [PATCH 08/46] remove problem3.ipynb from the ui-tests --- nbgrader/tests/ui-tests/assignment_list.spec.ts | 5 +++++ nbgrader/tests/ui-tests/formgrader.spec.ts | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/nbgrader/tests/ui-tests/assignment_list.spec.ts b/nbgrader/tests/ui-tests/assignment_list.spec.ts index 87ae7d702..4a689d929 100644 --- a/nbgrader/tests/ui-tests/assignment_list.spec.ts +++ b/nbgrader/tests/ui-tests/assignment_list.spec.ts @@ -124,6 +124,11 @@ const addCourses = async (request: APIRequestContext, tmpPath: string) => { `${tmpPath}/source/Problem Set 1/problem2.ipynb`, `${tmpPath}/source/Problem Set 1/Problem 2.ipynb` ); + // don't run autotest in the ui tests + await contents.deleteFile( + `${tmpPath}/source/Problem Set 1/problem3.ipynb` + ); + await contents.createDirectory(`${tmpPath}/source/ps.01`); await contents.uploadFile( path.resolve(__dirname, "files", "empty.ipynb"), diff --git a/nbgrader/tests/ui-tests/formgrader.spec.ts b/nbgrader/tests/ui-tests/formgrader.spec.ts index 6f6b211a8..222a57da7 100644 --- a/nbgrader/tests/ui-tests/formgrader.spec.ts +++ b/nbgrader/tests/ui-tests/formgrader.spec.ts @@ -140,6 +140,10 @@ const addCourses = async (request: APIRequestContext, tmpPath: string) => { `${tmpPath}/source/Problem Set 1/problem2.ipynb`, `${tmpPath}/source/Problem Set 1/Problem 2.ipynb` ); + // don't run autotest in the ui tests + await contents.deleteFile( + `${tmpPath}/source/Problem Set 1/problem3.ipynb` + ); await contents.renameDirectory( `${tmpPath}/submitted/bitdiddle`, `${tmpPath}/submitted/Bitdiddle` From 5a521ac65693200f2b59a6efa492374399ddeb69 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Thu, 24 Aug 2023 20:50:09 -0700 Subject: [PATCH 09/46] removed commented out timestamp code from earlier snippet execution --- nbgrader/preprocessors/instantiatetests.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index ffaea8b7b..2089c5d48 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -418,22 +418,3 @@ def _execute_code_snippet_output_hook(self, msg: t.Dict[str, t.Any]) -> None: self.log.error("Runtime error from the kernel: \n%s\n%s\n%s", content['ename'], content['evalue'], content['traceback']) raise CellExecutionError(content['traceback'], content['ename'], content['evalue']) return - -## TODO: do we need this? commenting out for now; will add back in if it causes errors -#def timestamp(msg: Optional[Dict] = None) -> str: -# if msg and 'header' in msg: # The test mocks don't provide a header, so tolerate that -# msg_header = msg['header'] -# if 'date' in msg_header and isinstance(msg_header['date'], datetime.datetime): -# try: -# # reformat datetime into expected format -# formatted_time = datetime.datetime.strftime( -# msg_header['date'], '%Y-%m-%dT%H:%M:%S.%fZ' -# ) -# if ( -# formatted_time -# ): # docs indicate strftime may return empty string, so let's catch that too -# return formatted_time -# except Exception: -# pass # fallback to a local time -# -# return datetime.datetime.utcnow().isoformat() + 'Z' From a8ab39f36b60b977e6e4e881dc3bd30c48170087 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Mon, 28 Aug 2023 12:08:14 -0700 Subject: [PATCH 10/46] tests.yml -> autotests.yml --- nbgrader/apps/quickstartapp.py | 6 +++--- .../user_guide/{tests.yml => autotests.yml} | 0 nbgrader/preprocessors/instantiatetests.py | 18 +++++++++--------- .../apps/files/{tests.yml => autotests.yml} | 0 nbgrader/tests/apps/test_nbgrader_autograde.py | 8 ++++---- .../apps/test_nbgrader_generate_assignment.py | 16 ++++++++-------- .../apps/test_nbgrader_generate_feedback.py | 2 +- 7 files changed, 25 insertions(+), 25 deletions(-) rename nbgrader/docs/source/user_guide/{tests.yml => autotests.yml} (100%) rename nbgrader/tests/apps/files/{tests.yml => autotests.yml} (100%) diff --git a/nbgrader/apps/quickstartapp.py b/nbgrader/apps/quickstartapp.py index 0af1460e4..36f8b528f 100644 --- a/nbgrader/apps/quickstartapp.py +++ b/nbgrader/apps/quickstartapp.py @@ -122,10 +122,10 @@ def start(self): ignore_html = shutil.ignore_patterns("*.html") shutil.copytree(example, os.path.join(course_path, "source"), ignore=ignore_html) - # copying the tests.yml file to the course directory + # copying the autotests.yml file to the course directory tests_file_path = os.path.abspath(os.path.join( - os.path.dirname(__file__), '..', 'docs', 'source', 'user_guide', 'tests.yml')) - shutil.copyfile(tests_file_path, os.path.join(course_path, 'tests.yml')) + os.path.dirname(__file__), '..', 'docs', 'source', 'user_guide', 'autotests.yml')) + shutil.copyfile(tests_file_path, os.path.join(course_path, 'autotests.yml')) # create the config file self.log.info("Generating example config file...") diff --git a/nbgrader/docs/source/user_guide/tests.yml b/nbgrader/docs/source/user_guide/autotests.yml similarity index 100% rename from nbgrader/docs/source/user_guide/tests.yml rename to nbgrader/docs/source/user_guide/autotests.yml diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index 2089c5d48..22857e598 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -33,7 +33,7 @@ class InstantiateTests(NbGraderPreprocessor): tests = None autotest_filename = Unicode( - "tests.yml", + "autotests.yml", help="The filename where automatic testing code is stored" ).tag(config=True) @@ -279,10 +279,10 @@ def preprocess_cell(self, cell, resources, index): # ------------------------------------------------------------------------------------- def _load_test_template_file(self, resources): """ - attempts to load the tests.yml file within the assignment directory. In case such file is not found + attempts to load the autotests.yml file within the assignment directory. In case such file is not found or perhaps cannot be loaded, it will attempt to load the default_tests.yaml file with the course_directory """ - self.log.debug('loading template tests.yml...') + self.log.debug('loading template autotests.yml...') self.log.debug(f'kernel_name: {resources["kernel_name"]}') try: with open(os.path.join(resources['metadata']['path'], self.autotest_filename), 'r') as tests_file: @@ -292,7 +292,7 @@ def _load_test_template_file(self, resources): except FileNotFoundError: # if there is no tests file, just load a default tests dict self.log.warning( - 'No tests.yml file found in the assignment directory. Loading the default tests.yml file in the course root directory') + 'No autotests.yml file found in the assignment directory. Loading the default autotests.yml file in the course root directory') # tests = {} try: with open(os.path.join(self.autotest_filename), 'r') as tests_file: @@ -300,15 +300,15 @@ def _load_test_template_file(self, resources): except FileNotFoundError: # if there is no tests file, just create a default empty tests dict self.log.warning( - 'No tests.yml file found. If AUTOTESTS appears in testing cells, an error will be thrown.') + 'No autotests.yml file found. If AUTOTESTS appears in testing cells, an error will be thrown.') tests = {} except yaml.parser.ParserError as e: - self.log.error('tests.yml contains invalid YAML code.') + self.log.error('autotests.yml contains invalid YAML code.') self.log.error(e.msg) raise except yaml.parser.ParserError as e: - self.log.error('tests.yml contains invalid YAML code.') + self.log.error('autotests.yml contains invalid YAML code.') self.log.error(e.msg) raise @@ -348,13 +348,13 @@ def _instantiate_tests(self, snippet, salt=None): try: tests = self.test_templates_by_type.get(dispatch_result, self.test_templates_by_type['default']) except KeyError: - self.log.error('tests.yml must contain a top-level "default" key with corresponding test code') + self.log.error('autotests.yml must contain a top-level "default" key with corresponding test code') raise try: test_templs = [t['test'] for t in tests] fail_msgs = [t['fail'] for t in tests] except KeyError: - self.log.error('each type in tests.yml must have a list of dictionaries with a "test" and "fail" key') + self.log.error('each type in autotests.yml must have a list of dictionaries with a "test" and "fail" key') self.log.error('the "test" item should store the test template code, ' 'and the "fail" item should store a failure message') raise diff --git a/nbgrader/tests/apps/files/tests.yml b/nbgrader/tests/apps/files/autotests.yml similarity index 100% rename from nbgrader/tests/apps/files/tests.yml rename to nbgrader/tests/apps/files/autotests.yml diff --git a/nbgrader/tests/apps/test_nbgrader_autograde.py b/nbgrader/tests/apps/test_nbgrader_autograde.py index 1f00939f8..7bd3eacdc 100644 --- a/nbgrader/tests/apps/test_nbgrader_autograde.py +++ b/nbgrader/tests/apps/test_nbgrader_autograde.py @@ -102,7 +102,7 @@ def test_grade_autotest(self, db, course_dir): run_nbgrader(["db", "student", "add", "bar", "--db", db]) self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "source", "ps1", "p1.ipynb")) - self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + self._copy_file(join("files", "autotests.yml"), join(course_dir, "autotests.yml")) run_nbgrader(["generate_assignment", "ps1", "--db", db]) self._copy_file(join("files", "autotest-simple-unchanged.ipynb"), join(course_dir, "submitted", "foo", "ps1", "p1.ipynb")) @@ -132,7 +132,7 @@ def test_grade_hashed_autotest(self, db, course_dir): run_nbgrader(["db", "student", "add", "bar", "--db", db]) self._copy_file(join("files", "autotest-hashed.ipynb"), join(course_dir, "source", "ps1", "p1.ipynb")) - self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + self._copy_file(join("files", "autotests.yml"), join(course_dir, "autotests.yml")) run_nbgrader(["generate_assignment", "ps1", "--db", db]) self._copy_file(join("files", "autotest-hashed-unchanged.ipynb"), join(course_dir, "submitted", "foo", "ps1", "p1.ipynb")) @@ -162,7 +162,7 @@ def test_grade_complex_autotest(self, db, course_dir): run_nbgrader(["db", "student", "add", "bar", "--db", db]) self._copy_file(join("files", "autotest-multi.ipynb"), join(course_dir, "source", "ps1", "p1.ipynb")) - self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + self._copy_file(join("files", "autotests.yml"), join(course_dir, "autotests.yml")) run_nbgrader(["generate_assignment", "ps1", "--db", db]) self._copy_file(join("files", "autotest-multi-unchanged.ipynb"), join(course_dir, "submitted", "foo", "ps1", "p1.ipynb")) @@ -944,7 +944,7 @@ def test_hidden_tests_autotest(self, db, course_dir): fh.write("""c.ClearSolutions.code_stub=dict(python="# YOUR CODE HERE")""") self._copy_file(join("files", "autotest-hidden.ipynb"), join(course_dir, "source", "ps1", "p1.ipynb")) - self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + self._copy_file(join("files", "autotests.yml"), join(course_dir, "autotests.yml")) run_nbgrader(["generate_assignment", "ps1", "--db", db]) self._copy_file(join("files", "autotest-hidden-unchanged.ipynb"), join(course_dir, "submitted", "foo", "ps1", "p1.ipynb")) diff --git a/nbgrader/tests/apps/test_nbgrader_generate_assignment.py b/nbgrader/tests/apps/test_nbgrader_generate_assignment.py index 444f1f08f..cb93d0f30 100644 --- a/nbgrader/tests/apps/test_nbgrader_generate_assignment.py +++ b/nbgrader/tests/apps/test_nbgrader_generate_assignment.py @@ -50,11 +50,11 @@ def test_single_file(self, course_dir, temp_cwd): def test_autotests_simple(self, course_dir, temp_cwd): """Can a notebook with simple autotests be generated with a default yaml location, and is autotest code removed?""" self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "source", "ps1", "foo.ipynb")) - self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + self._copy_file(join("files", "autotests.yml"), join(course_dir, "autotests.yml")) run_nbgrader(["db", "assignment", "add", "ps1"]) run_nbgrader(["generate_assignment", "ps1"]) assert os.path.isfile(join(course_dir, "release", "ps1", "foo.ipynb")) - assert not os.path.isfile(join(course_dir, "release", "ps1", "tests.yml")) + assert not os.path.isfile(join(course_dir, "release", "ps1", "autotests.yml")) foo = self._file_contents(join(course_dir, "release", "ps1", "foo.ipynb")) assert "AUTOTEST" not in foo @@ -62,11 +62,11 @@ def test_autotests_simple(self, course_dir, temp_cwd): def test_autotests_simple(self, course_dir, temp_cwd): """Can a notebook with simple autotests be generated with an assignment-specific yaml, and is autotest code removed?""" self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "source", "ps1", "foo.ipynb")) - self._copy_file(join("files", "tests.yml"), join(course_dir, "source", "ps1", "tests.yml")) + self._copy_file(join("files", "autotests.yml"), join(course_dir, "source", "ps1", "autotests.yml")) run_nbgrader(["db", "assignment", "add", "ps1"]) run_nbgrader(["generate_assignment", "ps1"]) assert os.path.isfile(join(course_dir, "release", "ps1", "foo.ipynb")) - assert os.path.isfile(join(course_dir, "release", "ps1", "tests.yml")) + assert os.path.isfile(join(course_dir, "release", "ps1", "autotests.yml")) foo = self._file_contents(join(course_dir, "release", "ps1", "foo.ipynb")) assert "AUTOTEST" not in foo @@ -81,7 +81,7 @@ def test_autotests_needs_yaml(self, course_dir, temp_cwd): def test_autotests_fancy(self, course_dir, temp_cwd): """Can a more complicated autotests notebook be generated, and is autotest code removed?""" self._copy_file(join("files", "autotest-multi.ipynb"), join(course_dir, "source", "ps1", "foo.ipynb")) - self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + self._copy_file(join("files", "autotests.yml"), join(course_dir, "autotests.yml")) run_nbgrader(["db", "assignment", "add", "ps1"]) run_nbgrader(["generate_assignment", "ps1"]) assert os.path.isfile(join(course_dir, "release", "ps1", "foo.ipynb")) @@ -92,7 +92,7 @@ def test_autotests_fancy(self, course_dir, temp_cwd): def test_autotests_hidden(self, course_dir, temp_cwd): """Can a notebook with hidden autotest be generated, and is autotest/hidden sections removed?""" self._copy_file(join("files", "autotest-hidden.ipynb"), join(course_dir, "source", "ps1", "foo.ipynb")) - self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + self._copy_file(join("files", "autotests.yml"), join(course_dir, "autotests.yml")) run_nbgrader(["db", "assignment", "add", "ps1"]) run_nbgrader(["generate_assignment", "ps1"]) assert os.path.isfile(join(course_dir, "release", "ps1", "foo.ipynb")) @@ -104,7 +104,7 @@ def test_autotests_hidden(self, course_dir, temp_cwd): def test_autotests_hashed(self, course_dir, temp_cwd): """Can a notebook with hashed autotests be generated, and is hashed autotest code removed?""" self._copy_file(join("files", "autotest-hashed.ipynb"), join(course_dir, "source", "ps1", "foo.ipynb")) - self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + self._copy_file(join("files", "autotests.yml"), join(course_dir, "autotests.yml")) run_nbgrader(["db", "assignment", "add", "ps1"]) run_nbgrader(["generate_assignment", "ps1"]) assert os.path.isfile(join(course_dir, "release", "ps1", "foo.ipynb")) @@ -117,7 +117,7 @@ def test_generate_source_with_tests_flag(self, course_dir, temp_cwd): """Does setting the flag --source_with_tests also create a notebook with solution and tests in the source_with_tests directory""" self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "source", "ps1", "foo.ipynb")) - self._copy_file(join("files", "tests.yml"), join(course_dir, "source", "ps1", "tests.yml")) + self._copy_file(join("files", "autotests.yml"), join(course_dir, "source", "ps1", "autotests.yml")) run_nbgrader(["db", "assignment", "add", "ps1"]) run_nbgrader(["generate_assignment", "ps1", "--source_with_tests"]) assert os.path.isfile(join(course_dir, "release", "ps1", "foo.ipynb")) diff --git a/nbgrader/tests/apps/test_nbgrader_generate_feedback.py b/nbgrader/tests/apps/test_nbgrader_generate_feedback.py index 6ff20a70b..cb76d3e13 100644 --- a/nbgrader/tests/apps/test_nbgrader_generate_feedback.py +++ b/nbgrader/tests/apps/test_nbgrader_generate_feedback.py @@ -333,7 +333,7 @@ def test_autotests(self, course_dir): self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "source", "ps1", "p1.ipynb")) self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "source", "ps1", "p2.ipynb")) - self._copy_file(join("files", "tests.yml"), join(course_dir, "tests.yml")) + self._copy_file(join("files", "autotests.yml"), join(course_dir, "autotests.yml")) run_nbgrader(["generate_assignment", "ps1"]) self._copy_file(join("files", "autotest-simple.ipynb"), join(course_dir, "submitted", "foo", "ps1", "p1.ipynb")) From 50e40c672c7292fb771647fdd710708ec7a7fb13 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Mon, 28 Aug 2023 12:16:39 -0700 Subject: [PATCH 11/46] logging string interpolation --- nbgrader/preprocessors/instantiatetests.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index 22857e598..53e133823 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -115,7 +115,7 @@ def preprocess(self, nb, resources): raise ValueError(f"Kernel {kernel_name} has not been specified in InstantiateTests.comment_strs") if kernel_name not in self.sanitizers: raise ValueError(f"Kernel {kernel_name} has not been specified in InstantiateTests.sanitizers") - self.log.debug(f"Found kernel {kernel_name}") + self.log.debug("Found kernel %s", kernel_name) resources["kernel_name"] = kernel_name # load the template tests file @@ -124,10 +124,10 @@ def preprocess(self, nb, resources): self.global_tests_loaded = True # set up the sanitizer - self.log.debug('Setting sanitizer for kernel {kernel_name}') + self.log.debug('Setting sanitizer for kernel %s', kernel_name) self.sanitizer = self.sanitizers[kernel_name] #start the kernel - self.log.debug('Starting client for kernel {kernel_name}') + self.log.debug('Starting client for kernel %s', kernel_name) km, self.kc = start_new_kernel(kernel_name = kernel_name) # run the preprocessor @@ -214,7 +214,7 @@ def preprocess_cell(self, cell, resources, index): # appears in the line before the self.autotest_delimiter token use_hash = (self.hashed_delimiter in line[:line.find(self.autotest_delimiter)]) if use_hash: - self.log.debug('Hashing delimiter found, using template: ' + self.hash_template) + self.log.debug('Hashing delimiter found, using template: %s', self.hash_template) else: self.log.debug('Hashing delimiter not found') @@ -233,18 +233,18 @@ def preprocess_cell(self, cell, resources, index): # generate the test for each snippet for snippet in snippets: - self.log.debug('Running autotest generation for snippet ' + snippet) + self.log.debug('Running autotest generation for snippet %s', snippet) # create a random salt for this test if use_hash: salt = secrets.token_hex(8) - self.log.debug('Using salt: ' + salt) + self.log.debug('Using salt: %s', salt) else: salt = None # get the normalized(/hashed) template tests for this code snippet self.log.debug( - 'Instantiating normalized' + ('/hashed ' if use_hash else ' ') + 'test templates based on type') + 'Instantiating normalized%s test templates based on type', ' & hashed' if use_hash else '') instantiated_tests, test_values, fail_messages = self._instantiate_tests(snippet, salt) # add all the lines to the cell @@ -253,7 +253,7 @@ def preprocess_cell(self, cell, resources, index): for i in range(len(instantiated_tests)): check_code = template.render(snippet=instantiated_tests[i], value=test_values[i], message=fail_messages[i]) - self.log.debug('Test: ' + check_code) + self.log.debug('Test: %s', check_code) new_lines.append(check_code) # add an empty line after this block of test code @@ -283,7 +283,7 @@ def _load_test_template_file(self, resources): or perhaps cannot be loaded, it will attempt to load the default_tests.yaml file with the course_directory """ self.log.debug('loading template autotests.yml...') - self.log.debug(f'kernel_name: {resources["kernel_name"]}') + self.log.debug('kernel_name: %s', resources["kernel_name"]) try: with open(os.path.join(resources['metadata']['path'], self.autotest_filename), 'r') as tests_file: tests = yaml.safe_load(tests_file) @@ -342,7 +342,7 @@ def _instantiate_tests(self, snippet, salt=None): template = j2.Environment(loader=j2.BaseLoader).from_string(self.dispatch_template) dispatch_code = template.render(snippet=snippet) dispatch_result = self._execute_code_snippet(dispatch_code) - self.log.debug('Dispatch result returned by kernel: ', dispatch_result) + self.log.debug('Dispatch result returned by kernel: %s', dispatch_result) # get the test code; if the type isn't in our dict, just default to 'default' # if default isn't in the tests code, this will throw an error try: From be7f998d432ba960371086eb51bc1614f62c6d77 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Mon, 28 Aug 2023 12:26:49 -0700 Subject: [PATCH 12/46] optional success code (and fix execute success code bug) --- nbgrader/preprocessors/instantiatetests.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index 53e133823..c13894501 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -259,15 +259,15 @@ def preprocess_cell(self, cell, resources, index): # add an empty line after this block of test code new_lines.append('') + # add the final success code and execute it + if is_grade_flag and self.global_tests_loaded and (self.autotest_delimiter in cell.source) and (self.success_code is not None): + new_lines.append(self.success_code) + non_autotest_code_lines.append(self.success_code) + # run the trailing non-autotest lines, if any remain if len(non_autotest_code_lines) > 0: self._execute_code_snippet("\n".join(non_autotest_code_lines)) - # add the final success message - if is_grade_flag and self.global_tests_loaded: - if self.autotest_delimiter in cell.source: - new_lines.append(self.success_code) - # replace the cell source cell.source = "\n".join(new_lines) @@ -322,7 +322,7 @@ def _load_test_template_file(self, resources): self.dispatch_template = tests['dispatch'] # get the success message template - self.success_code = tests['success'] + self.success_code = tests.get('success', None) # get the hash code template self.hash_template = tests['hash'] From ec6354585c98d11358b79acb4cc2d3044a80475d Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Mon, 28 Aug 2023 12:31:42 -0700 Subject: [PATCH 13/46] reraise filenotfound error when find autotest directive but no autotests.yml file --- nbgrader/preprocessors/instantiatetests.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index c13894501..3f6d619ab 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -293,15 +293,13 @@ def _load_test_template_file(self, resources): # if there is no tests file, just load a default tests dict self.log.warning( 'No autotests.yml file found in the assignment directory. Loading the default autotests.yml file in the course root directory') - # tests = {} try: with open(os.path.join(self.autotest_filename), 'r') as tests_file: tests = yaml.safe_load(tests_file) except FileNotFoundError: # if there is no tests file, just create a default empty tests dict - self.log.warning( - 'No autotests.yml file found. If AUTOTESTS appears in testing cells, an error will be thrown.') - tests = {} + self.log.error('No autotests.yml file found, but there were autotest directives found in the notebook. ') + raise except yaml.parser.ParserError as e: self.log.error('autotests.yml contains invalid YAML code.') self.log.error(e.msg) From e8c3d7ee05dc211eb8108aa5349e4c53117a257d Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Mon, 28 Aug 2023 12:37:57 -0700 Subject: [PATCH 14/46] hash optional; raise error if not set --- nbgrader/preprocessors/instantiatetests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index 3f6d619ab..fc02f8422 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -214,6 +214,8 @@ def preprocess_cell(self, cell, resources, index): # appears in the line before the self.autotest_delimiter token use_hash = (self.hashed_delimiter in line[:line.find(self.autotest_delimiter)]) if use_hash: + if self.hash_template is None: + raise ValueError('Found a hashing delimiter, but the hash property has not been set in autotests.yml') self.log.debug('Hashing delimiter found, using template: %s', self.hash_template) else: self.log.debug('Hashing delimiter not found') @@ -323,7 +325,7 @@ def _load_test_template_file(self, resources): self.success_code = tests.get('success', None) # get the hash code template - self.hash_template = tests['hash'] + self.hash_template = tests.get('hash', None) # get the hash code template self.check_template = tests['check'] From 2ceab614183fe12c2fcf8dbbd0120f2b5860d85c Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Mon, 28 Aug 2023 13:25:06 -0700 Subject: [PATCH 15/46] minor comment rephrasing --- nbgrader/preprocessors/instantiatetests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index fc02f8422..7cb9a4d05 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -292,14 +292,14 @@ def _load_test_template_file(self, resources): self.log.debug(tests) except FileNotFoundError: - # if there is no tests file, just load a default tests dict + # if there is no tests file, try to load default tests dict self.log.warning( 'No autotests.yml file found in the assignment directory. Loading the default autotests.yml file in the course root directory') try: with open(os.path.join(self.autotest_filename), 'r') as tests_file: tests = yaml.safe_load(tests_file) except FileNotFoundError: - # if there is no tests file, just create a default empty tests dict + # if there is not even a default tests file, re-raise the FileNotFound error self.log.error('No autotests.yml file found, but there were autotest directives found in the notebook. ') raise except yaml.parser.ParserError as e: From c53a31909f21743086d1b5e3199e1e45fce92ec4 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Tue, 29 Aug 2023 09:03:59 -0700 Subject: [PATCH 16/46] better if statement formatting Co-authored-by: Nicolas Brichet <32258950+brichet@users.noreply.github.com> --- nbgrader/preprocessors/instantiatetests.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index 7cb9a4d05..1957d551d 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -262,7 +262,12 @@ def preprocess_cell(self, cell, resources, index): new_lines.append('') # add the final success code and execute it - if is_grade_flag and self.global_tests_loaded and (self.autotest_delimiter in cell.source) and (self.success_code is not None): + if ( + is_grade_flag + and self.global_tests_loaded + and (self.autotest_delimiter in cell.source) + and (self.success_code is not None) + ): new_lines.append(self.success_code) non_autotest_code_lines.append(self.success_code) From 52ae394b9b49d83dcbce4fb066db8f419082603b Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Tue, 29 Aug 2023 09:04:20 -0700 Subject: [PATCH 17/46] remove unnecessary spaces in start new kernel arg Co-authored-by: Nicolas Brichet <32258950+brichet@users.noreply.github.com> --- nbgrader/preprocessors/instantiatetests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index 1957d551d..0aa0c98a9 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -128,7 +128,7 @@ def preprocess(self, nb, resources): self.sanitizer = self.sanitizers[kernel_name] #start the kernel self.log.debug('Starting client for kernel %s', kernel_name) - km, self.kc = start_new_kernel(kernel_name = kernel_name) + km, self.kc = start_new_kernel(kernel_name=kernel_name) # run the preprocessor self.log.debug('Running InstantiateTests preprocessor') From 577c733f451e64d3fbac055c13b4413a205b0fea Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Tue, 29 Aug 2023 09:08:42 -0700 Subject: [PATCH 18/46] clear outputs from test notebook --- .../apps/files/autotest-multi-unchanged.ipynb | 34 ++++--------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/nbgrader/tests/apps/files/autotest-multi-unchanged.ipynb b/nbgrader/tests/apps/files/autotest-multi-unchanged.ipynb index 212fee372..6090389a7 100644 --- a/nbgrader/tests/apps/files/autotest-multi-unchanged.ipynb +++ b/nbgrader/tests/apps/files/autotest-multi-unchanged.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": { "deletable": false, "nbgrader": { @@ -23,7 +23,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "deletable": false, "editable": false, @@ -39,15 +39,7 @@ "solution": false } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Success!\n" - ] - } - ], + "outputs": [], "source": [ "# the basic tests from the simple notebook\n", "from hashlib import sha1\n", @@ -69,7 +61,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": { "deletable": false, "editable": false, @@ -86,19 +78,7 @@ "task": false } }, - "outputs": [ - { - "ename": "AssertionError", - "evalue": "value of str(a) is not correct", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m/tmp/ipykernel_645570/554255095.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtype\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m\"\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"type of str(a) is not str. str(a) should be an str\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m\"1\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"length of str(a) is not correct\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 7\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlower\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m\"5\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"value of str(a) is not correct\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 8\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m\"5\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"correct string value of str(a) but incorrect case of letters\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mAssertionError\u001b[0m: value of str(a) is not correct" - ] - } - ], + "outputs": [], "source": [ "# multiple expressions per line\n", "from hashlib import sha1\n", @@ -269,9 +249,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.10.12" } }, "nbformat": 4, - "nbformat_minor": 1 + "nbformat_minor": 4 } From 702d10da13a5d5082e44006cf76b562ea9db0c5a Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Tue, 29 Aug 2023 09:19:31 -0700 Subject: [PATCH 19/46] added jinja to pyproject --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 46213205a..5d90abf06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,8 @@ dependencies = [ "rapidfuzz>=1.8", "requests>=2.26", "sqlalchemy>=1.4,<3", - "PyYAML>=6.0" + "PyYAML>=6.0", + "Jinja2>=3.0" ] version = "0.9.0a1" From da8c972bb83fa13a67b95493b4b21a1f77b2fd99 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Tue, 29 Aug 2023 11:21:10 -0700 Subject: [PATCH 20/46] initial commit of advanced topics docs, minor polish on creating assignments docs --- nbgrader/docs/source/user_guide/advanced.rst | 20 +++++++++++++++++++ .../creating_and_grading_assignments.ipynb | 19 ++++++++++-------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/nbgrader/docs/source/user_guide/advanced.rst b/nbgrader/docs/source/user_guide/advanced.rst index 502c0d393..b2b686865 100644 --- a/nbgrader/docs/source/user_guide/advanced.rst +++ b/nbgrader/docs/source/user_guide/advanced.rst @@ -194,3 +194,23 @@ containerization system. For details on using ``envkernel`` with singularity, see the `README `_ of ``envkernel``. + +.. _customizing-autotests: + +Automatic test code generation +--------------------------------------- + +.. versionadded:: 0.9.0 + +.. seealso:: + + :ref:`autograder-tests-cell-automatic-test-code` + General introduction to automatic test code generation. + + +nbgrader now supports generating test code automatically +using ``### AUTOTEST`` and ``### HASHED AUTOTEST`` statements. + +TODO +- autotest.yml syntax +- using ``--source-with-tests`` diff --git a/nbgrader/docs/source/user_guide/creating_and_grading_assignments.ipynb b/nbgrader/docs/source/user_guide/creating_and_grading_assignments.ipynb index 7c473c592..336a07cbb 100644 --- a/nbgrader/docs/source/user_guide/creating_and_grading_assignments.ipynb +++ b/nbgrader/docs/source/user_guide/creating_and_grading_assignments.ipynb @@ -426,7 +426,7 @@ " (see :ref:`autograde-assignments`)." ] }, - { + { "cell_type": "raw", "metadata": {}, "source": [ @@ -451,16 +451,15 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Tests in \"Autograder tests\" cells can be automatically and dynamically generated through the use of a special syntax such as ``### AUTOTEST`` and ``### HASHED AUTOTEST``, for example:\n", - "\n", - "![autograder tests autotest tests](images/autograder_tests_autotest_jlab.png)\n" + "Tests in \"Autograder tests\" cells can be automatically and dynamically generated through the use of the special syntax ``### AUTOTEST`` and ``### HASHED AUTOTEST``. This syntax allows you to specify only the objects you want to test, rather than having to write the test code yourself manually; `nbgrader` will generate the test code for you. For example,\n", + "![autograder tests autotest syntax](images/autograder_tests_autotest_jlab.png)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In the release version, the above autotest statements get converted to the following test code that the students see:\n", + "In this example, the instructor wants to test that the returned value of `squares(1)`, `squares(2)`, and `squares(3)` lines up with the value from the source copy. In the release copy, the above autotest statements will be converted to the following test code that the students see:\n", "![autograder tests autotest tests](images/autograder_tests_autogenerated_tests_jlab.png)" ] }, @@ -468,15 +467,19 @@ "cell_type": "raw", "metadata": {}, "source": [ - "When creating the release version (see :ref:`assign-and-release-an-assignment`), the autotest lines (lines starting with the special syntax) will transform into automatically generated test cases (i.e., assert statements). The value of the expression(s) following the special syntax will be evaluated in the solution version to generate test cases that are checked in the student version. If this special syntax is not used, then the contents of the cell will remain as is.\n", + "When creating the release version (see :ref:`assign-and-release-an-assignment`), the autotest lines (lines starting with the special syntax) will transform into automatically generated test cases (i.e., assert statements). The value of the expression(s) following the special syntax will be evaluated in the solution version to generate test cases that are checked in the student version. If this special syntax is not used, then the contents of the cell will remain as-is.\n", "\n", ".. note::\n", "\n", - " Lines starting with ### AUTOTEST will generate test code where the answer is visible to students. To generate test code where the answers are *hashed* (not viewable by students), begin the line with the syntax ### HASHED AUTOTEST instead.\n", + " Lines starting with ``### AUTOTEST`` will generate test code where the answer is visible to students. In the example above, the tests for ``squares(1)`` and ``squares(2)`` can be examined by students to see the answer. To generate test code that students can run, but where the answers are not viewable by students (they are *hashed*), begin the line with the syntax ``### HASHED AUTOTEST`` instead. You can also make `### AUTOTEST` and `### HASHED AUTOTEST` statements hidden and not runnable by students by wrapping them in ``### BEGIN HIDDEN TESTS`` and ``### END HIDDEN TESTS`` as in :ref:`autograder-tests-cell-hidden-tests`\n", " \n", ".. note:: \n", "\n", - " You can put multiple expressions to be tested on a single ### AUTOTEST line (or ### HASHED AUTOTEST line), separated by semicolons." + " You can put multiple expressions to be tested on a single ``### AUTOTEST`` line (or ``### HASHED AUTOTEST`` line), separated by semicolons.\n", + "\n", + ".. note::\n", + "\n", + " You can follow the ``### AUTOTEST`` or ``### HASHED AUTOTEST`` syntax with any valid Python expression. Test code will be automatically generated based on the return type of that expression. See :ref:`customizing-autotests` for more technical details about how ``### AUTOTEST`` and ``### HASHED AUTOTEST`` statements are converted into test code, and how to customize this process." ] }, { From db9b4102063ada12cf53c51c35465d15c1971dec Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Tue, 29 Aug 2023 14:33:28 -0700 Subject: [PATCH 21/46] done advanced docs section on autotest --- nbgrader/docs/source/user_guide/advanced.rst | 143 ++++++++++++++++++- 1 file changed, 140 insertions(+), 3 deletions(-) diff --git a/nbgrader/docs/source/user_guide/advanced.rst b/nbgrader/docs/source/user_guide/advanced.rst index b2b686865..715cab537 100644 --- a/nbgrader/docs/source/user_guide/advanced.rst +++ b/nbgrader/docs/source/user_guide/advanced.rst @@ -210,7 +210,144 @@ Automatic test code generation nbgrader now supports generating test code automatically using ``### AUTOTEST`` and ``### HASHED AUTOTEST`` statements. +In this section, you can find more detail on how this works and +how to customize the test generation process. +Suppose you ask students to create a ``foo`` function that adds 5 to +an integer. In the source copy of the notebook, you might write something like + +.. code:: python + + ### BEGIN SOLUTION + def foo(x): + return x + 5 + ### END SOLUTION + +In a test cell, you would normally then write test code manually to probe various aspects of the solution. +For example, you might check that the function increments 3 to 8 properly, and that the type +of the output is an integer. + +.. code:: python + + assert isinstance(foo(3), int), "incrementing an int by 5 should return an int" + assert foo(3) == 8, "3+5 should be 8" + +nbgrader now provides functionality to automate this process. Instead of writing tests explicitly, +you can instead specify *what you want to test*, and let nbgrader decide *how to test it* automatically. + +.. code:: python + + ### AUTOTEST foo(3) + +This directive indicates that you want to check ``foo(3)`` in the student's notebook, and make sure it +aligns with the value of ``foo(3)`` in the current source copy. You can write any valid expression (in the +language of your notebook) after the ``### AUTOTEST`` directive. For example, you could write + +.. code:: python + + ### AUTOTEST (foo(3) - 5 == 3) + +to generate test code for the expression ``foo(3)-5==3`` (i.e., a boolean value), and make sure that evaluating +the student's copy of this expression has a result that aligns with the source version (i.e., ``True``). You can write multiple +``### AUTOTEST`` directives in one cell. You can also separate multiple expressions on one line with semicolons: + +.. code:: python + + ### AUTOTEST foo(3); foo(4); foo(5) != 8 + +These directives will insert code into student notebooks where the solution is available in plaintext. If you want to +obfuscate the answers in the student copy, you should instead use a ``### HASHED AUTOTEST``, which will produce +a student notebook where the answers are hashed and not viewable by students. + +When you generate an assignment containing ``### AUTOTEST`` (or ``### HASHED AUTOTEST``) statements, nbgrader looks for a file +named ``autotests.yml`` that contains instructions on how to generate test code. It first looks +in the assignment directory itself (in case you want to specify special tests for just that assignment), and if it is +not found there, nbgrader searches in the course root directory. +The ``autotests.yml`` file is a `YAML `__ file that looks something like this: + +.. code:: yaml + + python3: + setup: "from hashlib import sha1" + hash: 'sha1({{snippet}}.encode("utf-8")+b"{{salt}}").hexdigest()' + dispatch: "type({{snippet}})" + normalize: "str({{snippet}})" + check: 'assert {{snippet}} == """{{value}}""", """{{message}}"""' + success: "print('Success!')" + + templates: + default: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not correct" + + - test: "{{snippet}}" + fail: "value of {{snippet}} is not correct" + + int: + - test: "type({{snippet}})" + fail: "type of {{snippet}} is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()" + + - test: "{{snippet}}" + fail: "value of {{snippet}} is not correct" + +The outermost level in the YAML file (the example shows an entry for ``python3``) specifies which kernel the configuration applies to. ``autotests.yml`` can +have separate sections for multiple kernels / languages. The ``autotests.yml`` file uses `Jinja templates `__ to +specify snippets of code that will be executed/inserted into Jupyter notebooks in the process of generating the assignment. You should familiarize yourself +with the basics of Jinja templates before proceeding. For each kernel, there are a few configuration settings possible: + +- **dispatch:** When you write ``### AUTOTEST foo(3)``, nbgrader needs to know how to test ``foo(3)``. It does so by executing ``foo(3)``, then checking its *type*, + and then running tests corresponding to that type in the ``autotests.yml`` file. Specifically, when generating an assignment, nbgrader substitutes the ``{{snippet}}`` template + variable with the expression ``foo(3)``, and then evaluates the dispatch code based on that. In this case, nbgrader runs ``type(foo(3))``, which will + return ``int``, so nbgrader will know to test ``foo(3)`` using tests for integer variables. +- **templates:** Once nbgrader determines the type of the expression ``foo(3)``, it will look for that type in the list of templates for the kernel. In this case, + it will find the ``int`` type in the list (it will use the **default** if the type is not found). Each type will have associated with it a + list of **test**/**fail** template pairs, which tell nbgrader what tests to run + and what messages to print in the event of a failure. Once again, ``{{snippet}}`` will be replaced by the ``foo(3)`` expression. In ``autotests.yml`` above, the + ``int`` type has two tests: one that checks type of the expression, and one that checks its value. In this case, the student notebook will have + two tests: one that checks the value of ``type(foo(3))``, and one that checks the value of ``foo(3)``. +- **normalize:** For each test code expression (for example, ``type(foo(3))`` as mentioned previously), nbgrader will execute code using the corresponding + Jupyter kernel, which will respond with a result in the form of a *string*. So nbgrader now knows that if it runs ``type(foo(3))`` at this + point in the notebook, and converts the output to a string (i.e., *normalizes it*), it should obtain ``"int"``. However, nbgrader does not know how to convert output to a string; that + depends on the kernel! So the normalize code template tells nbgrader how to convert an expression to a string. In the ``autotests.yml`` example above, the + normalize template suggests that nbgrader should try to compare ``str(type(foo(3)))`` to ``"int"``. +- **check:** This is the code template that will be inserted into the student notebook to run each test. The template has three variables. ``{{snippet}}`` is the normalized + test code. The ``{{value}}`` is the evaluated version of that test code, based on the source notebook. The ``{{message}}`` is + text that will be printed in the event of a test failure. In the example above, the check code template tells nbgrader to insert an ``assert`` statement to run the test. +- **hash (optional):** This is a code template that is responsible for hashing (i.e., obfuscating) the answers in the student notebok. The template has two variables. + ``{{snippet}}`` represents the expression that will be hashed, and ``{{salt}}`` is used for nbgrader to insert a `salt `__ + prior to hashing. The salt helps avoid students being able to identify hashes from common question types. For example, a true/false question has only two possible answers; + without a salt, students would be able to recognize the hashes of ``True`` and ``False`` in their notebooks. By adding a salt, nbgrader makes the hashed version of the answer + different for each question, preventing identifying answers based on their hashes. +- **setup (optional):** This is a code template that will be run at the beginning of all test cells containing ``### AUTOTEST`` or ``### HASHED AUTOTEST`` directives. It is often used to import + special packages that only the test code requires. In the example above, the setup code is used to import the ``sha1`` function from ``hashlib``, which is necessary + for hashed test generation. +- **success (optional):** This is a code template that will be added to the end of all test cells containing ``### AUTOTEST`` or ``### HASHED AUTOTEST`` directives. In the + generated student version of the notebook, + this code will run if all the tests pass. In the example ``autotests.yml`` file above, the success code is used to run ``print('Success!')``, i.e., simply print a message to + indicate that all tests in the cell passed. + +.. note:: + + For assignments with ``### AUTOTEST`` and ``### HASHED AUTOTEST`` directives, it is often handy + to have an editable copy of the assignment with solutions *and* test code inserted. You can + use ``nbgrader generate_assignment --source_with_tests`` to generate this version of an assignment, + which will appear in the ``source_with_tests/`` folder in the course repository. + +.. warning:: + + The default ``autotests.yml`` test templates file included with the repository has tests for many + common data types (``int``, ``dict``, ``list``, ``float``, etc). It also has a ``default`` test template + that it will try to apply to any types that do not have specified tests. If you want to automatically + generate your own tests for custom types, you will need to implement those test templates in ``autotests.yml``. That being said, custom + object types often have standard Python types as class attributes. Sometimes an easier option is to use nbgrader to test these + attributes automatically instead. For example, if ``obj`` is a complicated type with no specific test template available, + but ``obj`` has an ``int`` attribute ``x``, you could consider testing that attribute directly, e.g., ``### AUTOTEST obj.x``. + +.. warning:: + + The InstantiateTests preprocessor in nbgrader is responsible for generating test code from ``### AUTOTEST`` + directives and the ``autotests.yml`` file. It has some configuration parameters not yet mentioned here. + The most important of these is the ``InstantiateTests.sanitizers`` dictionary, which tells nbgrader how to + clean up the string output from each kind of Jupyter kernel before using it in the process of generating tests. We have + implemented sanitizers for popular kernels in nbgrader already, but you might need to add your own. + -TODO -- autotest.yml syntax -- using ``--source-with-tests`` From a881d2ba4dae736940eb7eecb595ef13eea02718 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Tue, 29 Aug 2023 15:59:43 -0700 Subject: [PATCH 22/46] moved problem3 to it's own problem set ps1_autotest --- .gitignore | 4 --- nbgrader/apps/quickstartapp.py | 26 ++++++++++++++----- .../managing_assignment_files.ipynb | 12 ++++----- .../tests/ui-tests/assignment_list.spec.ts | 4 --- nbgrader/tests/ui-tests/formgrader.spec.ts | 4 --- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 7da289b32..3cecd390f 100644 --- a/.gitignore +++ b/.gitignore @@ -84,18 +84,14 @@ nbgrader/docs/source/user_guide/managing_assignment_files_manually.rst nbgrader/docs/source/user_guide/managing_the_database.rst nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/problem1.html nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/problem2.html -nbgrader/docs/source/user_guide/autograded/bitdiddle/ps1/problem3.html nbgrader/docs/source/user_guide/autograded/hacker/ps1/problem1.html nbgrader/docs/source/user_guide/autograded/hacker/ps1/problem2.html -nbgrader/docs/source/user_guide/autograded/hacker/ps1/problem3.html nbgrader/docs/source/user_guide/downloaded/ps1/archive/ps1_hacker_attempt_2016-01-30-20-30-10_problem1.html nbgrader/docs/source/user_guide/release/ps1/problem1.html nbgrader/docs/source/user_guide/release/ps1/problem2.html -nbgrader/docs/source/user_guide/release/ps1/problem3.html nbgrader/docs/source/user_guide/source/header.html nbgrader/docs/source/user_guide/source/ps1/problem1.html nbgrader/docs/source/user_guide/source/ps1/problem2.html -nbgrader/docs/source/user_guide/source/ps1/problem3.html # components stuff node_modules diff --git a/nbgrader/apps/quickstartapp.py b/nbgrader/apps/quickstartapp.py index 36f8b528f..06b6824ff 100644 --- a/nbgrader/apps/quickstartapp.py +++ b/nbgrader/apps/quickstartapp.py @@ -40,6 +40,15 @@ """ ) ), + 'autotest': ( + {'QuickStartApp': {'autotest': True}}, + dedent( + """ + Create notebook assignments that have examples of automatic test generation via + ### AUTOTEST and ### HASHED AUTOTEST statements. + """ + ) + ), } class QuickStartApp(NbGrader): @@ -73,6 +82,8 @@ class QuickStartApp(NbGrader): force = Bool(False, help="Whether to overwrite existing files").tag(config=True) + autotest = Bool(False, help="Whether to use automatic test generation in example files").tag(config=True) + @default("classes") def _classes_default(self): classes = super(QuickStartApp, self)._classes_default() @@ -119,13 +130,14 @@ def start(self): self.log.info("Copying example from the user guide...") example = os.path.abspath(os.path.join( os.path.dirname(__file__), '..', 'docs', 'source', 'user_guide', 'source')) - ignore_html = shutil.ignore_patterns("*.html") - shutil.copytree(example, os.path.join(course_path, "source"), ignore=ignore_html) - - # copying the autotests.yml file to the course directory - tests_file_path = os.path.abspath(os.path.join( - os.path.dirname(__file__), '..', 'docs', 'source', 'user_guide', 'autotests.yml')) - shutil.copyfile(tests_file_path, os.path.join(course_path, 'autotests.yml')) + if self.autotest: + tests_file_path = os.path.abspath(os.path.join( + os.path.dirname(__file__), '..', 'docs', 'source', 'user_guide', 'autotests.yml')) + shutil.copyfile(tests_file_path, os.path.join(course_path, 'autotests.yml')) + ignored_files = shutil.ignore_patterns("*.html", "ps2", "ps1") + else: + ignored_files = shutil.ignore_patterns("*.html", "ps2", "autotests.yml", "ps1_autotest") + shutil.copytree(example, os.path.join(course_path, "source"), ignore=ignored_files) # create the config file self.log.info("Generating example config file...") diff --git a/nbgrader/docs/source/user_guide/managing_assignment_files.ipynb b/nbgrader/docs/source/user_guide/managing_assignment_files.ipynb index 498a6190a..8c75210be 100644 --- a/nbgrader/docs/source/user_guide/managing_assignment_files.ipynb +++ b/nbgrader/docs/source/user_guide/managing_assignment_files.ipynb @@ -465,8 +465,8 @@ "total ##\n", "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] jupyter.png\n", "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] problem1.ipynb\n", - "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] problem2.ipynb\n", - "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] problem3.ipynb\n" + "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] problem1_autotest.ipynb\n", + "-rw-r--r-- 1 nb_user nb_group [size] [date] [time] problem2.ipynb\n" ] } ], @@ -817,12 +817,12 @@ "[SubmitApp | WARNING] Possible missing notebooks and/or extra notebooks submitted for assignment ps1:\n", " Expected:\n", " \tproblem1.ipynb: MISSING\n", + " \tproblem1_autotest.ipynb: FOUND\n", " \tproblem2.ipynb: FOUND\n", - " \tproblem3.ipynb: FOUND\n", " Submitted:\n", " \tmyproblem1.ipynb: EXTRA\n", + " \tproblem1_autotest.ipynb: OK\n", " \tproblem2.ipynb: OK\n", - " \tproblem3.ipynb: OK\n", "[SubmitApp | INFO] Submitted as: example_course ps1 [timestamp] UTC\n" ] } @@ -898,12 +898,12 @@ "[SubmitApp | CRITICAL] Assignment ps1 not submitted. There are missing notebooks for the submission:\n", " Expected:\n", " \tproblem1.ipynb: MISSING\n", + " \tproblem1_autotest.ipynb: FOUND\n", " \tproblem2.ipynb: FOUND\n", - " \tproblem3.ipynb: FOUND\n", " Submitted:\n", " \tmyproblem1.ipynb: EXTRA\n", + " \tproblem1_autotest.ipynb: OK\n", " \tproblem2.ipynb: OK\n", - " \tproblem3.ipynb: OK\n", "[SubmitApp | ERROR] nbgrader submit failed\n" ] } diff --git a/nbgrader/tests/ui-tests/assignment_list.spec.ts b/nbgrader/tests/ui-tests/assignment_list.spec.ts index 4a689d929..b54d4ca83 100644 --- a/nbgrader/tests/ui-tests/assignment_list.spec.ts +++ b/nbgrader/tests/ui-tests/assignment_list.spec.ts @@ -124,10 +124,6 @@ const addCourses = async (request: APIRequestContext, tmpPath: string) => { `${tmpPath}/source/Problem Set 1/problem2.ipynb`, `${tmpPath}/source/Problem Set 1/Problem 2.ipynb` ); - // don't run autotest in the ui tests - await contents.deleteFile( - `${tmpPath}/source/Problem Set 1/problem3.ipynb` - ); await contents.createDirectory(`${tmpPath}/source/ps.01`); await contents.uploadFile( diff --git a/nbgrader/tests/ui-tests/formgrader.spec.ts b/nbgrader/tests/ui-tests/formgrader.spec.ts index 222a57da7..6f6b211a8 100644 --- a/nbgrader/tests/ui-tests/formgrader.spec.ts +++ b/nbgrader/tests/ui-tests/formgrader.spec.ts @@ -140,10 +140,6 @@ const addCourses = async (request: APIRequestContext, tmpPath: string) => { `${tmpPath}/source/Problem Set 1/problem2.ipynb`, `${tmpPath}/source/Problem Set 1/Problem 2.ipynb` ); - // don't run autotest in the ui tests - await contents.deleteFile( - `${tmpPath}/source/Problem Set 1/problem3.ipynb` - ); await contents.renameDirectory( `${tmpPath}/submitted/bitdiddle`, `${tmpPath}/submitted/Bitdiddle` From 1af6da29255625e1783337e91348131a7ef1ed34 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Tue, 29 Aug 2023 16:00:02 -0700 Subject: [PATCH 23/46] moved problem3 to its own ps1_autotest assignment --- .../source/ps1_autotest/jupyter.png | Bin 0 -> 5733 bytes .../source/ps1_autotest/problem1.ipynb | 399 ++++++++++++++++++ .../source/ps1_autotest/problem2.ipynb | 79 ++++ 3 files changed, 478 insertions(+) create mode 100644 nbgrader/docs/source/user_guide/source/ps1_autotest/jupyter.png create mode 100644 nbgrader/docs/source/user_guide/source/ps1_autotest/problem1.ipynb create mode 100644 nbgrader/docs/source/user_guide/source/ps1_autotest/problem2.ipynb diff --git a/nbgrader/docs/source/user_guide/source/ps1_autotest/jupyter.png b/nbgrader/docs/source/user_guide/source/ps1_autotest/jupyter.png new file mode 100644 index 0000000000000000000000000000000000000000..201fc09ce423a6e83c74829d25b711f65ba47f1a GIT binary patch literal 5733 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 SF7+@s1{7peq$?y%g8u^szt&R# literal 0 HcmV?d00001 diff --git a/nbgrader/docs/source/user_guide/source/ps1_autotest/problem1.ipynb b/nbgrader/docs/source/user_guide/source/ps1_autotest/problem1.ipynb new file mode 100644 index 000000000..16d713402 --- /dev/null +++ b/nbgrader/docs/source/user_guide/source/ps1_autotest/problem1.ipynb @@ -0,0 +1,399 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "jupyter", + "locked": true, + "schema_version": 3, + "solution": false + } + }, + "source": [ + "For this problem set, we'll be using the Jupyter notebook:\n", + "\n", + "![](jupyter.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part A (2 points)\n", + "\n", + "Write a function that returns a list of numbers, such that $x_i=i^2$, for $1\\leq i \\leq n$. Make sure it handles the case where $n<1$ by raising a `ValueError`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "squares", + "locked": false, + "schema_version": 3, + "solution": true + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def squares(n):\n", + " \"\"\"Compute the squares of numbers from 1 to n, such that the \n", + " ith element of the returned list equals i^2.\n", + " \n", + " \"\"\"\n", + " ### BEGIN SOLUTION\n", + " if n < 1:\n", + " raise ValueError(\"n must be greater than or equal to 1\")\n", + " return [i ** 2 for i in range(1, n + 1)]\n", + " ### END SOLUTION" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Your function should print `[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]` for $n=10$. Check that it does:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "squares(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "correct_squares", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "\"\"\"Check that squares returns the correct output for several inputs\"\"\"\n", + "### AUTOTEST squares(1); squares(2)\n", + "### HASHED AUTOTEST squares(3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "squares_invalid_input", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": false + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "\"\"\"Check that squares raises an error for invalid inputs\"\"\"\n", + "def test_func_throws(func, ErrorType):\n", + " try:\n", + " func()\n", + " except ErrorType:\n", + " return True\n", + " else:\n", + " print('Did not raise right type of error!')\n", + " return False\n", + " \n", + "### AUTOTEST test_func_throws(lambda : squares(0), ValueError)\n", + "### AUTOTEST test_func_throws(lambda : squares(-4), ValueError);\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Part B (1 point)\n", + "\n", + "Using your `squares` function, write a function that computes the sum of the squares of the numbers from 1 to $n$. Your function should call the `squares` function -- it should NOT reimplement its functionality." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "sum_of_squares", + "locked": false, + "schema_version": 3, + "solution": true + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def sum_of_squares(n):\n", + " \"\"\"Compute the sum of the squares of numbers from 1 to n.\"\"\"\n", + " ### BEGIN SOLUTION\n", + " return sum(squares(n))\n", + " ### END SOLUTION" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The sum of squares from 1 to 10 should be 385. Verify that this is the answer you get:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "sum_of_squares(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "correct_sum_of_squares", + "locked": false, + "points": 0.5, + "schema_version": 3, + "solution": false + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "\"\"\"Check that sum_of_squares returns the correct answer for various inputs.\"\"\"\n", + "### AUTOTEST sum_of_squares(1)\n", + "### AUTOTEST sum_of_squares(2); sum_of_squares(10) \n", + "### AUTOTEST sum_of_squares(11) \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "sum_of_squares_uses_squares", + "locked": false, + "points": 0.5, + "schema_version": 3, + "solution": false + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "\"\"\"Check that sum_of_squares relies on squares.\"\"\"\n", + "\n", + "orig_squares = squares\n", + "del squares\n", + "\n", + "### AUTOTEST test_func_throws(lambda : sum_of_squares(1), NameError)\n", + "\n", + "squares = orig_squares\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part C (1 point)\n", + "\n", + "Using LaTeX math notation, write out the equation that is implemented by your `sum_of_squares` function." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "sum_of_squares_equation", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": true + } + }, + "source": [ + "$\\sum_{i=1}^n i^2$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part D (2 points)\n", + "\n", + "Find a usecase for your `sum_of_squares` function and implement that usecase in the cell below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "sum_of_squares_application", + "locked": false, + "points": 2, + "schema_version": 3, + "solution": true + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "def pyramidal_number(n):\n", + " \"\"\"Returns the n^th pyramidal number\"\"\"\n", + " return sum_of_squares(n)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "cell-938593c4a215c6cc", + "locked": true, + "points": 4, + "schema_version": 3, + "solution": false, + "task": true + } + }, + "source": [ + "---\n", + "## Part E (4 points)\n", + "\n", + "State the formulae for an arithmetic and geometric sum and verify them numerically for an example of your choice." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part F (1 points)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": false, + "grade_id": "cell-d3df8cd59fd0eb74", + "locked": false, + "schema_version": 3, + "solution": true, + "task": false + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "my_dictionary = {\n", + " 'one' : 1,\n", + " 'two' : 2,\n", + " 'three' : 3\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "cell-6e9ff83aa5dfaf17", + "locked": true, + "points": 0, + "schema_version": 3, + "solution": false, + "task": false + }, + "vscode": { + "languageId": "python" + } + }, + "outputs": [], + "source": [ + "### AUTOTEST my_dictionary\n", + "### AUTOTEST my_dictionary[\"one\"]" + ] + } + ], + "metadata": { + "celltoolbar": "Create Assignment", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.12" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/nbgrader/docs/source/user_guide/source/ps1_autotest/problem2.ipynb b/nbgrader/docs/source/user_guide/source/ps1_autotest/problem2.ipynb new file mode 100644 index 000000000..a8c653699 --- /dev/null +++ b/nbgrader/docs/source/user_guide/source/ps1_autotest/problem2.ipynb @@ -0,0 +1,79 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Consider the following piece of code:\n", + "\n", + "```python\n", + "def f(x):\n", + " if x == 0 or x == 1:\n", + " return x\n", + " return f(x - 1) + f(x - 2)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part A (1 point)\n", + "\n", + "Describe, in words, what this code does, and how it does it." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "part-a", + "locked": false, + "points": 1, + "schema_version": 3, + "solution": true + } + }, + "source": [ + "This function computes the fibonnaci sequence using recursion. The base cases are $x=0$ and $x=1$, in which case the function will return 0 or 1, respectively. In all other cases, the function will call itself to find the $x-1$ and $x-2$ fibonnaci numbers, and then add them together." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "## Part B (2 points)\n", + "\n", + "For what inputs will this function not behave as expected? What will happen?" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbgrader": { + "grade": true, + "grade_id": "part-b", + "locked": false, + "points": 2, + "schema_version": 3, + "solution": true + } + }, + "source": [ + "The function will not work correctly for inputs less than zero. Such inputs will result in an infinite recursion, as the function will keep subtracting one but never reach a base case that stops it." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python", + "language": "python", + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} From 0ca7287b916f21081e18d0433cd60b8173432e72 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Tue, 29 Aug 2023 16:46:32 -0700 Subject: [PATCH 24/46] remove problem3 from ps1 --- .../user_guide/source/ps1/problem3.ipynb | 399 ------------------ 1 file changed, 399 deletions(-) delete mode 100644 nbgrader/docs/source/user_guide/source/ps1/problem3.ipynb diff --git a/nbgrader/docs/source/user_guide/source/ps1/problem3.ipynb b/nbgrader/docs/source/user_guide/source/ps1/problem3.ipynb deleted file mode 100644 index 16d713402..000000000 --- a/nbgrader/docs/source/user_guide/source/ps1/problem3.ipynb +++ /dev/null @@ -1,399 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "nbgrader": { - "grade": false, - "grade_id": "jupyter", - "locked": true, - "schema_version": 3, - "solution": false - } - }, - "source": [ - "For this problem set, we'll be using the Jupyter notebook:\n", - "\n", - "![](jupyter.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "## Part A (2 points)\n", - "\n", - "Write a function that returns a list of numbers, such that $x_i=i^2$, for $1\\leq i \\leq n$. Make sure it handles the case where $n<1$ by raising a `ValueError`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": false, - "grade_id": "squares", - "locked": false, - "schema_version": 3, - "solution": true - }, - "vscode": { - "languageId": "python" - } - }, - "outputs": [], - "source": [ - "def squares(n):\n", - " \"\"\"Compute the squares of numbers from 1 to n, such that the \n", - " ith element of the returned list equals i^2.\n", - " \n", - " \"\"\"\n", - " ### BEGIN SOLUTION\n", - " if n < 1:\n", - " raise ValueError(\"n must be greater than or equal to 1\")\n", - " return [i ** 2 for i in range(1, n + 1)]\n", - " ### END SOLUTION" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Your function should print `[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]` for $n=10$. Check that it does:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "vscode": { - "languageId": "python" - } - }, - "outputs": [], - "source": [ - "squares(10)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "correct_squares", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": false - }, - "vscode": { - "languageId": "python" - } - }, - "outputs": [], - "source": [ - "\"\"\"Check that squares returns the correct output for several inputs\"\"\"\n", - "### AUTOTEST squares(1); squares(2)\n", - "### HASHED AUTOTEST squares(3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "squares_invalid_input", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": false - }, - "vscode": { - "languageId": "python" - } - }, - "outputs": [], - "source": [ - "\"\"\"Check that squares raises an error for invalid inputs\"\"\"\n", - "def test_func_throws(func, ErrorType):\n", - " try:\n", - " func()\n", - " except ErrorType:\n", - " return True\n", - " else:\n", - " print('Did not raise right type of error!')\n", - " return False\n", - " \n", - "### AUTOTEST test_func_throws(lambda : squares(0), ValueError)\n", - "### AUTOTEST test_func_throws(lambda : squares(-4), ValueError);\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "\n", - "## Part B (1 point)\n", - "\n", - "Using your `squares` function, write a function that computes the sum of the squares of the numbers from 1 to $n$. Your function should call the `squares` function -- it should NOT reimplement its functionality." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": false, - "grade_id": "sum_of_squares", - "locked": false, - "schema_version": 3, - "solution": true - }, - "vscode": { - "languageId": "python" - } - }, - "outputs": [], - "source": [ - "def sum_of_squares(n):\n", - " \"\"\"Compute the sum of the squares of numbers from 1 to n.\"\"\"\n", - " ### BEGIN SOLUTION\n", - " return sum(squares(n))\n", - " ### END SOLUTION" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The sum of squares from 1 to 10 should be 385. Verify that this is the answer you get:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "vscode": { - "languageId": "python" - } - }, - "outputs": [], - "source": [ - "sum_of_squares(10)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "correct_sum_of_squares", - "locked": false, - "points": 0.5, - "schema_version": 3, - "solution": false - }, - "vscode": { - "languageId": "python" - } - }, - "outputs": [], - "source": [ - "\"\"\"Check that sum_of_squares returns the correct answer for various inputs.\"\"\"\n", - "### AUTOTEST sum_of_squares(1)\n", - "### AUTOTEST sum_of_squares(2); sum_of_squares(10) \n", - "### AUTOTEST sum_of_squares(11) \n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "sum_of_squares_uses_squares", - "locked": false, - "points": 0.5, - "schema_version": 3, - "solution": false - }, - "vscode": { - "languageId": "python" - } - }, - "outputs": [], - "source": [ - "\"\"\"Check that sum_of_squares relies on squares.\"\"\"\n", - "\n", - "orig_squares = squares\n", - "del squares\n", - "\n", - "### AUTOTEST test_func_throws(lambda : sum_of_squares(1), NameError)\n", - "\n", - "squares = orig_squares\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "## Part C (1 point)\n", - "\n", - "Using LaTeX math notation, write out the equation that is implemented by your `sum_of_squares` function." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "sum_of_squares_equation", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": true - } - }, - "source": [ - "$\\sum_{i=1}^n i^2$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "## Part D (2 points)\n", - "\n", - "Find a usecase for your `sum_of_squares` function and implement that usecase in the cell below." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "sum_of_squares_application", - "locked": false, - "points": 2, - "schema_version": 3, - "solution": true - }, - "vscode": { - "languageId": "python" - } - }, - "outputs": [], - "source": [ - "def pyramidal_number(n):\n", - " \"\"\"Returns the n^th pyramidal number\"\"\"\n", - " return sum_of_squares(n)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbgrader": { - "grade": false, - "grade_id": "cell-938593c4a215c6cc", - "locked": true, - "points": 4, - "schema_version": 3, - "solution": false, - "task": true - } - }, - "source": [ - "---\n", - "## Part E (4 points)\n", - "\n", - "State the formulae for an arithmetic and geometric sum and verify them numerically for an example of your choice." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "## Part F (1 points)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": false, - "grade_id": "cell-d3df8cd59fd0eb74", - "locked": false, - "schema_version": 3, - "solution": true, - "task": false - }, - "vscode": { - "languageId": "python" - } - }, - "outputs": [], - "source": [ - "my_dictionary = {\n", - " 'one' : 1,\n", - " 'two' : 2,\n", - " 'three' : 3\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "cell-6e9ff83aa5dfaf17", - "locked": true, - "points": 0, - "schema_version": 3, - "solution": false, - "task": false - }, - "vscode": { - "languageId": "python" - } - }, - "outputs": [], - "source": [ - "### AUTOTEST my_dictionary\n", - "### AUTOTEST my_dictionary[\"one\"]" - ] - } - ], - "metadata": { - "celltoolbar": "Create Assignment", - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.12" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} From cf1d60557948da68ed4bd8f8dabf6cccfe4cbd88 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Tue, 29 Aug 2023 17:11:47 -0700 Subject: [PATCH 25/46] ignore autotest assignment files; make quickstart rename ps1_autotest -> ps1; add quickstart test for autotest --- .gitignore | 3 +++ nbgrader/apps/quickstartapp.py | 6 +++-- .../tests/apps/test_nbgrader_quickstart.py | 25 +++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 3cecd390f..4943e0362 100644 --- a/.gitignore +++ b/.gitignore @@ -92,6 +92,9 @@ nbgrader/docs/source/user_guide/release/ps1/problem2.html nbgrader/docs/source/user_guide/source/header.html nbgrader/docs/source/user_guide/source/ps1/problem1.html nbgrader/docs/source/user_guide/source/ps1/problem2.html +nbgrader/docs/source/user_guide/source/ps1_autotest/problem1.html +nbgrader/docs/source/user_guide/source/ps1_autotest/problem2.html +nbgrader/docs/source/user_guide/source/ps2/problem.html # components stuff node_modules diff --git a/nbgrader/apps/quickstartapp.py b/nbgrader/apps/quickstartapp.py index 06b6824ff..0bca02f8a 100644 --- a/nbgrader/apps/quickstartapp.py +++ b/nbgrader/apps/quickstartapp.py @@ -126,7 +126,7 @@ def start(self): if not os.path.isdir(course_path): os.mkdir(course_path) - # populating it with an example + # populate it with an example self.log.info("Copying example from the user guide...") example = os.path.abspath(os.path.join( os.path.dirname(__file__), '..', 'docs', 'source', 'user_guide', 'source')) @@ -135,9 +135,11 @@ def start(self): os.path.dirname(__file__), '..', 'docs', 'source', 'user_guide', 'autotests.yml')) shutil.copyfile(tests_file_path, os.path.join(course_path, 'autotests.yml')) ignored_files = shutil.ignore_patterns("*.html", "ps2", "ps1") + shutil.copytree(example, os.path.join(course_path, "source"), ignore=ignored_files) + os.rename(os.path.join(course_path, "source", "ps1_autotest"), os.path.join(course_path, "source", "ps1")) else: ignored_files = shutil.ignore_patterns("*.html", "ps2", "autotests.yml", "ps1_autotest") - shutil.copytree(example, os.path.join(course_path, "source"), ignore=ignored_files) + shutil.copytree(example, os.path.join(course_path, "source"), ignore=ignored_files) # create the config file self.log.info("Generating example config file...") diff --git a/nbgrader/tests/apps/test_nbgrader_quickstart.py b/nbgrader/tests/apps/test_nbgrader_quickstart.py index 57c67934b..6ab100310 100644 --- a/nbgrader/tests/apps/test_nbgrader_quickstart.py +++ b/nbgrader/tests/apps/test_nbgrader_quickstart.py @@ -117,3 +117,28 @@ def test_quickstart_f(self): # nbgrader generate_assignment should work run_nbgrader(["generate_assignment", "ps1"]) + + def test_quickstart_autotest(self): + """Is the quickstart example with autotests properly generated?""" + + run_nbgrader(["quickstart", "example", "--autotest"]) + + # it should fail if it already exists + run_nbgrader(["quickstart", "example", "--autotest"], retcode=1) + + # it should succeed if --force is given + os.remove(os.path.join("example", "nbgrader_config.py")) + run_nbgrader(["quickstart", "example", "--force", "--autotest"]) + assert os.path.exists(os.path.join("example", "nbgrader_config.py")) + assert os.path.exists(os.path.join("example", "autotests.yml")) + + # nbgrader validate should work + os.chdir("example") + for nb in os.listdir(os.path.join("source", "ps1")): + if not nb.endswith(".ipynb"): + continue + output = run_nbgrader(["validate", os.path.join("source", "ps1", nb)], stdout=True) + assert output.strip() == "Success! Your notebook passes all the tests." + + # nbgrader generate_assignment should work + run_nbgrader(["generate_assignment", "ps1"]) From 4b76b89da8b82eb4dc937c298a95a6d5784d501a Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Tue, 29 Aug 2023 18:08:26 -0700 Subject: [PATCH 26/46] remove ps2 R assignment; check for autotest directive in quickstart tests --- .gitignore | 1 - nbgrader/apps/quickstartapp.py | 4 +- .../source/user_guide/source/ps2/jupyter.png | Bin 5733 -> 0 bytes .../user_guide/source/ps2/problem.ipynb | 241 ------------------ .../tests/apps/test_nbgrader_quickstart.py | 18 ++ 5 files changed, 20 insertions(+), 244 deletions(-) delete mode 100644 nbgrader/docs/source/user_guide/source/ps2/jupyter.png delete mode 100644 nbgrader/docs/source/user_guide/source/ps2/problem.ipynb diff --git a/.gitignore b/.gitignore index 4943e0362..c04f56b69 100644 --- a/.gitignore +++ b/.gitignore @@ -94,7 +94,6 @@ nbgrader/docs/source/user_guide/source/ps1/problem1.html nbgrader/docs/source/user_guide/source/ps1/problem2.html nbgrader/docs/source/user_guide/source/ps1_autotest/problem1.html nbgrader/docs/source/user_guide/source/ps1_autotest/problem2.html -nbgrader/docs/source/user_guide/source/ps2/problem.html # components stuff node_modules diff --git a/nbgrader/apps/quickstartapp.py b/nbgrader/apps/quickstartapp.py index 0bca02f8a..c477f3091 100644 --- a/nbgrader/apps/quickstartapp.py +++ b/nbgrader/apps/quickstartapp.py @@ -134,11 +134,11 @@ def start(self): tests_file_path = os.path.abspath(os.path.join( os.path.dirname(__file__), '..', 'docs', 'source', 'user_guide', 'autotests.yml')) shutil.copyfile(tests_file_path, os.path.join(course_path, 'autotests.yml')) - ignored_files = shutil.ignore_patterns("*.html", "ps2", "ps1") + ignored_files = shutil.ignore_patterns("*.html", "ps1") shutil.copytree(example, os.path.join(course_path, "source"), ignore=ignored_files) os.rename(os.path.join(course_path, "source", "ps1_autotest"), os.path.join(course_path, "source", "ps1")) else: - ignored_files = shutil.ignore_patterns("*.html", "ps2", "autotests.yml", "ps1_autotest") + ignored_files = shutil.ignore_patterns("*.html", "autotests.yml", "ps1_autotest") shutil.copytree(example, os.path.join(course_path, "source"), ignore=ignored_files) # create the config file diff --git a/nbgrader/docs/source/user_guide/source/ps2/jupyter.png b/nbgrader/docs/source/user_guide/source/ps2/jupyter.png deleted file mode 100644 index 201fc09ce423a6e83c74829d25b711f65ba47f1a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5733 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 SF7+@s1{7peq$?y%g8u^szt&R# diff --git a/nbgrader/docs/source/user_guide/source/ps2/problem.ipynb b/nbgrader/docs/source/user_guide/source/ps2/problem.ipynb deleted file mode 100644 index 2286cd1eb..000000000 --- a/nbgrader/docs/source/user_guide/source/ps2/problem.ipynb +++ /dev/null @@ -1,241 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "nbgrader": { - "grade": false, - "grade_id": "jupyter", - "locked": true, - "schema_version": 3, - "solution": false - } - }, - "source": [ - "For this problem set, we'll be using the Jupyter notebook:\n", - "\n", - "![](jupyter.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "## Part A (2 points)\n", - "\n", - "Write a function that returns a vector of numbers, such that $x_i=i^2$, for $1\\leq i \\leq n$. Make sure it handles the case where $n<1$ by using `stop(\"n must be greater than or equal to 1\")`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": false, - "grade_id": "squares", - "locked": false, - "schema_version": 3, - "solution": true - } - }, - "outputs": [], - "source": [ - "squares <- function(n){\n", - " ### BEGIN SOLUTION\n", - " if (n < 1){\n", - " stop(\"n must be greater than or equal to 1\")\n", - " }\n", - " ret <- c()\n", - " for (i in 1:n){\n", - " ret <- append(ret, i**2)\n", - " }\n", - " return(ret)\n", - " ### END SOLUTION\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Your function should print `[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]` for $n=10$. Check that it does:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "squares(10)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "correct_squares", - "locked": false, - "points": 2, - "schema_version": 3, - "solution": false - } - }, - "outputs": [], - "source": [ - "### AUTOTEST squares(1); squares(2); squares(15)\n", - "### AUTOTEST squares(10)\n", - "### AUTOTEST squares(11)\n", - "### AUTOTEST 3\n", - "### AUTOTEST squares(3)\n", - "### HASHED AUTOTEST squares(3)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "\n", - "## Part B (1 point)\n", - "\n", - "Using your `squares` function, write a function that computes the sum of the squares of the numbers from 1 to $n$. Your function should call the `squares` function -- it should NOT reimplement its functionality." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": false, - "grade_id": "sum_of_squares", - "locked": false, - "schema_version": 3, - "solution": true - } - }, - "outputs": [], - "source": [ - "sum_of_squares <- function(n) {\n", - " ### BEGIN SOLUTION\n", - " return(sum(squares(n)))\n", - " ### END SOLUTION\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The sum of squares from 1 to 10 should be 385. Verify that this is the answer you get:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sum_of_squares(10)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "correct_sum_of_squares", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": false - } - }, - "outputs": [], - "source": [ - "### AUTOTEST sum_of_squares(1)\n", - "### AUTOTEST sum_of_squares(2); sum_of_squares(10) \n", - "### AUTOTEST sum_of_squares(11) \n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "## Part C (1 point)\n", - "\n", - "Using LaTeX math notation, write out the equation that is implemented by your `sum_of_squares` function." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "sum_of_squares_equation", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": true - } - }, - "source": [ - "$\\sum_{i=1}^n i^2$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "## Part D (2 points)\n", - "\n", - "Find a usecase for your `sum_of_squares` function and implement that usecase in the cell below." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "sum_of_squares_application", - "locked": false, - "points": 2, - "schema_version": 3, - "solution": true - } - }, - "outputs": [], - "source": [ - "pyramidal_number <- function(n){\n", - " return(sum_of_squares(n))\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "## Part E (4 points)\n", - "\n", - "State the formulae for an arithmetic and geometric sum and verify them numerically for an example of your choice." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "R", - "language": "R", - "name": "ir" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/nbgrader/tests/apps/test_nbgrader_quickstart.py b/nbgrader/tests/apps/test_nbgrader_quickstart.py index 6ab100310..577e096b1 100644 --- a/nbgrader/tests/apps/test_nbgrader_quickstart.py +++ b/nbgrader/tests/apps/test_nbgrader_quickstart.py @@ -39,6 +39,13 @@ def test_quickstart(self, fake_home_dir): # nbgrader generate_assignment should work run_nbgrader(["generate_assignment", "ps1"]) + # there should be no autotests in any notebook in ps1 + for nb in os.listdir(os.path.join("source", "ps1")): + if not nb.endswith(".ipynb"): + continue + with open(os.path.join("source", "ps1", nb), 'r') as f: + assert "AUTOTEST" not in f.read() + def test_quickstart_overwrite_course_folder_if_structure_not_present(self): """Is the quickstart example properly generated?""" @@ -129,6 +136,8 @@ def test_quickstart_autotest(self): # it should succeed if --force is given os.remove(os.path.join("example", "nbgrader_config.py")) run_nbgrader(["quickstart", "example", "--force", "--autotest"]) + + # ensure both autotests.yml and nbgrader_config.py are in the course root dir assert os.path.exists(os.path.join("example", "nbgrader_config.py")) assert os.path.exists(os.path.join("example", "autotests.yml")) @@ -142,3 +151,12 @@ def test_quickstart_autotest(self): # nbgrader generate_assignment should work run_nbgrader(["generate_assignment", "ps1"]) + + # there should be autotests in at least one notebook in ps1 + found_autotest = False + for nb in os.listdir(os.path.join("source", "ps1")): + if not nb.endswith(".ipynb"): + continue + with open(os.path.join("source", "ps1", nb), 'r') as f: + found_autotest = found_autotest or ("AUTOTEST" in f.read()) + assert found_autotest From 2702e2c47a22dd42874bf64e73e49dbd4a4e93d9 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Tue, 29 Aug 2023 18:24:05 -0700 Subject: [PATCH 27/46] minor code tags syntax fix in docs --- .../source/user_guide/creating_and_grading_assignments.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbgrader/docs/source/user_guide/creating_and_grading_assignments.ipynb b/nbgrader/docs/source/user_guide/creating_and_grading_assignments.ipynb index 336a07cbb..8e3256b67 100644 --- a/nbgrader/docs/source/user_guide/creating_and_grading_assignments.ipynb +++ b/nbgrader/docs/source/user_guide/creating_and_grading_assignments.ipynb @@ -471,7 +471,7 @@ "\n", ".. note::\n", "\n", - " Lines starting with ``### AUTOTEST`` will generate test code where the answer is visible to students. In the example above, the tests for ``squares(1)`` and ``squares(2)`` can be examined by students to see the answer. To generate test code that students can run, but where the answers are not viewable by students (they are *hashed*), begin the line with the syntax ``### HASHED AUTOTEST`` instead. You can also make `### AUTOTEST` and `### HASHED AUTOTEST` statements hidden and not runnable by students by wrapping them in ``### BEGIN HIDDEN TESTS`` and ``### END HIDDEN TESTS`` as in :ref:`autograder-tests-cell-hidden-tests`\n", + " Lines starting with ``### AUTOTEST`` will generate test code where the answer is visible to students. In the example above, the tests for ``squares(1)`` and ``squares(2)`` can be examined by students to see the answer. To generate test code that students can run, but where the answers are not viewable by students (they are *hashed*), begin the line with the syntax ``### HASHED AUTOTEST`` instead. You can also make ``### AUTOTEST`` and ``### HASHED AUTOTEST`` statements hidden and not runnable by students by wrapping them in ``### BEGIN HIDDEN TESTS`` and ``### END HIDDEN TESTS`` as in :ref:`autograder-tests-cell-hidden-tests`\n", " \n", ".. note:: \n", "\n", From 696406547c38ffd50fcbd2ac1a6b443474aad24f Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Wed, 30 Aug 2023 18:26:56 -0700 Subject: [PATCH 28/46] set kernel working dir to notebook resources path --- nbgrader/preprocessors/instantiatetests.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index 0aa0c98a9..b975a2f54 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -118,6 +118,9 @@ def preprocess(self, nb, resources): self.log.debug("Found kernel %s", kernel_name) resources["kernel_name"] = kernel_name + # get the resources path from the notebook + resources_path = resources.get('metadata', {}).get('path', None) + # load the template tests file self.log.debug('Loading template tests file') self._load_test_template_file(resources) @@ -126,9 +129,9 @@ def preprocess(self, nb, resources): # set up the sanitizer self.log.debug('Setting sanitizer for kernel %s', kernel_name) self.sanitizer = self.sanitizers[kernel_name] - #start the kernel - self.log.debug('Starting client for kernel %s', kernel_name) - km, self.kc = start_new_kernel(kernel_name=kernel_name) + #start the kernel with the specified kernel and in the local path of the notebook + self.log.debug('Starting client for kernel %s at path %s', kernel_name, resources_path if resources_path is not None else '') + km, self.kc = start_new_kernel(kernel_name=kernel_name, cwd=resources_path) # run the preprocessor self.log.debug('Running InstantiateTests preprocessor') @@ -263,9 +266,9 @@ def preprocess_cell(self, cell, resources, index): # add the final success code and execute it if ( - is_grade_flag - and self.global_tests_loaded - and (self.autotest_delimiter in cell.source) + is_grade_flag + and self.global_tests_loaded + and (self.autotest_delimiter in cell.source) and (self.success_code is not None) ): new_lines.append(self.success_code) From d271f9cd36b6990f21b7bcae6ea34d46c59b980d Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Wed, 30 Aug 2023 23:58:13 -0700 Subject: [PATCH 29/46] Convert to deterministic salt computed using cell source/index --- nbgrader/preprocessors/instantiatetests.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index b975a2f54..3a3cfa960 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -5,9 +5,9 @@ from .. import utils from traitlets import Bool, List, Integer, Unicode, Dict, Callable from textwrap import dedent -import secrets import asyncio import inspect +import hashlib import typing as t from nbformat import NotebookNode from queue import Empty @@ -161,6 +161,11 @@ def preprocess_cell(self, cell, resources, index): # get the comment string for this language comment_str = self.comment_strs[resources["kernel_name"]] + # seed the salt generator for this cell + # avoid actual random seeds so that release versions are consistent across + # calls to nbgrader generate_assignment + salt_int = int(hashlib.sha256((cell.source+str(index)).encode('utf-8')).hexdigest(), 16) % 10**6 + # split the code lines into separate strings lines = cell.source.split("\n") @@ -240,9 +245,10 @@ def preprocess_cell(self, cell, resources, index): for snippet in snippets: self.log.debug('Running autotest generation for snippet %s', snippet) - # create a random salt for this test + # create a salt for this test if use_hash: - salt = secrets.token_hex(8) + salt_int += 1 + salt = hex(salt_int) self.log.debug('Using salt: %s', salt) else: salt = None From c58646d752dfcffb5085487f409a8af3939de71f Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Wed, 30 Aug 2023 23:59:39 -0700 Subject: [PATCH 30/46] minor ed --- nbgrader/preprocessors/instantiatetests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index 3a3cfa960..6e85e1fdd 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -248,7 +248,7 @@ def preprocess_cell(self, cell, resources, index): # create a salt for this test if use_hash: salt_int += 1 - salt = hex(salt_int) + salt = hex(salt_int)[2:] self.log.debug('Using salt: %s', salt) else: salt = None From cc5acbfade7870c51a60a00a1379c40ac7425d30 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Thu, 31 Aug 2023 10:19:54 -0700 Subject: [PATCH 31/46] remove extra jinja from pyproject Co-authored-by: Nicolas Brichet <32258950+brichet@users.noreply.github.com> --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5d90abf06..a5c1c9156 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,6 @@ dependencies = [ "requests>=2.26", "sqlalchemy>=1.4,<3", "PyYAML>=6.0", - "Jinja2>=3.0" ] version = "0.9.0a1" From 2dffa50717ae2944be25db5e660cfce98279ba50 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Thu, 31 Aug 2023 10:33:51 -0700 Subject: [PATCH 32/46] added tests for kernel workingdir, release consistency --- nbgrader/tests/__init__.py | 8 +++ .../preprocessors/test_instantiatetests.py | 60 ++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/nbgrader/tests/__init__.py b/nbgrader/tests/__init__.py index 82992b1ba..112345965 100644 --- a/nbgrader/tests/__init__.py +++ b/nbgrader/tests/__init__.py @@ -266,3 +266,11 @@ def create_autotest_test_cell(): """ cell = new_code_cell(source=source) return cell + +def create_file_loader_cell(filename): + source = f""" + with open({filename}, 'r') as f: + tmp = f.read() + """ + cell = create_regular_cell(source, 'code') + return cell diff --git a/nbgrader/tests/preprocessors/test_instantiatetests.py b/nbgrader/tests/preprocessors/test_instantiatetests.py index bed94506a..6286ddea5 100644 --- a/nbgrader/tests/preprocessors/test_instantiatetests.py +++ b/nbgrader/tests/preprocessors/test_instantiatetests.py @@ -3,7 +3,7 @@ from textwrap import dedent from ...preprocessors import InstantiateTests from .base import BaseTestPreprocessor -from .. import create_code_cell, create_text_cell, create_autotest_solution_cell, create_autotest_test_cell +from .. import create_code_cell, create_text_cell, create_autotest_solution_cell, create_autotest_test_cell, create_file_loader_cell from nbformat.v4 import new_notebook from nbclient.client import NotebookClient @@ -39,6 +39,7 @@ def test_has_comment_strs(self, preprocessor): assert 'python3' in preprocessor.comment_strs.keys() assert 'ir' in preprocessor.comment_strs.keys() + # test that autotest generates assert statements def test_replace_autotest_code(self, preprocessor): sol_cell = create_autotest_solution_cell() test_cell = create_autotest_test_cell() @@ -55,6 +56,63 @@ def test_replace_autotest_code(self, preprocessor): nb, resources = preprocessor.preprocess(nb, resources) assert 'assert' in nb['cells'][1]['source'] + # test that autotest generates consistent output given the same input + def test_consistent_release_version(self, preprocessor): + sol_cell = create_autotest_solution_cell() + test_cell = create_autotest_test_cell() + test_cell.metadata['nbgrader'] = {'grade': True} + + # create and process first notebook + nb1 = new_notebook() + nb1.metadata['kernelspec'] = { + "name": "python3" + } + nb1.cells.append(sol_cell) + nb1.cells.append(test_cell) + resources1 = { + 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} + } + nb1, resources1 = preprocessor.preprocess(nb1, resources1) + + # create and process second notebook + nb2 = new_notebook() + nb2.metadata['kernelspec'] = { + "name": "python3" + } + nb2.cells.append(sol_cell) + nb2.cells.append(test_cell) + resources2 = { + 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} + } + nb2, resources2 = preprocessor.preprocess(nb2, resources2) + assert nb1['cells'][1]['source'] == nb2['cells'][1]['source'] + + # test that autotest starts a kernel that uses the `path` metadata as working directory + def test_kernel_workingdir(self, preprocessor, caplog): + sol_cell = create_autotest_solution_cell() + test_cell = create_autotest_test_cell() + load_cell = create_file_loader_cell('grades.csv') + test_cell.metadata['nbgrader'] = {'grade': True} + nb = new_notebook() + nb.metadata['kernelspec'] = { + "name": "python3" + } + nb.cells.append(sol_cell) + nb.cells.append(test_cell) + nb.cells.append(load_cell) + # with the right path, the kernel should load the file + resources = { + 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} + } + nb, resources = preprocessor.preprocess(nb, resources) + # without the right path, the kernel should report an error + resources = { + 'metadata': {'path': 'nbgrader/docs/source/user_guide/source/'} + } + with pytest.raises(Exception): + nb, resources = preprocessor.preprocess(nb, resources) + assert "FileNotFoundError" in caplog.text + # test that a warning is thrown when we set enforce_metadata = False and have an AUTOTEST directive in a # non-grade cell def test_warning_autotest_nongrade(self, preprocessor, caplog): From 91eeaa08cf578b9637201bdb3b487b9e664d98cf Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Thu, 31 Aug 2023 10:47:56 -0700 Subject: [PATCH 33/46] minor quotes bugfix in workdir kernel test --- nbgrader/tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbgrader/tests/__init__.py b/nbgrader/tests/__init__.py index 112345965..5239d4cb6 100644 --- a/nbgrader/tests/__init__.py +++ b/nbgrader/tests/__init__.py @@ -269,7 +269,7 @@ def create_autotest_test_cell(): def create_file_loader_cell(filename): source = f""" - with open({filename}, 'r') as f: + with open('{filename}', 'r') as f: tmp = f.read() """ cell = create_regular_cell(source, 'code') From abb35d030e59e5a7be7915cc087ad8f6909ed1ab Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Thu, 31 Aug 2023 11:05:48 -0700 Subject: [PATCH 34/46] refresh nb between tests for workdir kernel test --- nbgrader/tests/preprocessors/test_instantiatetests.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/nbgrader/tests/preprocessors/test_instantiatetests.py b/nbgrader/tests/preprocessors/test_instantiatetests.py index 6286ddea5..ff34ec69d 100644 --- a/nbgrader/tests/preprocessors/test_instantiatetests.py +++ b/nbgrader/tests/preprocessors/test_instantiatetests.py @@ -93,6 +93,8 @@ def test_kernel_workingdir(self, preprocessor, caplog): test_cell = create_autotest_test_cell() load_cell = create_file_loader_cell('grades.csv') test_cell.metadata['nbgrader'] = {'grade': True} + + # with the right path, the kernel should load the file nb = new_notebook() nb.metadata['kernelspec'] = { "name": "python3" @@ -100,12 +102,19 @@ def test_kernel_workingdir(self, preprocessor, caplog): nb.cells.append(sol_cell) nb.cells.append(test_cell) nb.cells.append(load_cell) - # with the right path, the kernel should load the file resources = { 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} } nb, resources = preprocessor.preprocess(nb, resources) + # without the right path, the kernel should report an error + nb = new_notebook() + nb.metadata['kernelspec'] = { + "name": "python3" + } + nb.cells.append(sol_cell) + nb.cells.append(test_cell) + nb.cells.append(load_cell) resources = { 'metadata': {'path': 'nbgrader/docs/source/user_guide/source/'} } From 45ca48e40b082625f96b8b9a39714a7bff3845c7 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Thu, 31 Aug 2023 11:49:44 -0700 Subject: [PATCH 35/46] fail test to see existing log --- nbgrader/tests/preprocessors/test_instantiatetests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nbgrader/tests/preprocessors/test_instantiatetests.py b/nbgrader/tests/preprocessors/test_instantiatetests.py index ff34ec69d..2d29376b8 100644 --- a/nbgrader/tests/preprocessors/test_instantiatetests.py +++ b/nbgrader/tests/preprocessors/test_instantiatetests.py @@ -140,6 +140,7 @@ def test_warning_autotest_nongrade(self, preprocessor, caplog): } nb, resources = preprocessor.preprocess(nb, resources) + assert False, caplog.text assert "AutoTest region detected in a non-grade cell; " in caplog.text # test that an error is thrown when we have an AUTOTEST directive in a non-grade cell From 7d45d163b83f97467fa6036a09d5a5e73ec27702 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Thu, 31 Aug 2023 12:11:15 -0700 Subject: [PATCH 36/46] trying to provoke failure --- .../preprocessors/test_instantiatetests.py | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/nbgrader/tests/preprocessors/test_instantiatetests.py b/nbgrader/tests/preprocessors/test_instantiatetests.py index 2d29376b8..08712cf0a 100644 --- a/nbgrader/tests/preprocessors/test_instantiatetests.py +++ b/nbgrader/tests/preprocessors/test_instantiatetests.py @@ -87,40 +87,40 @@ def test_consistent_release_version(self, preprocessor): nb2, resources2 = preprocessor.preprocess(nb2, resources2) assert nb1['cells'][1]['source'] == nb2['cells'][1]['source'] - # test that autotest starts a kernel that uses the `path` metadata as working directory - def test_kernel_workingdir(self, preprocessor, caplog): - sol_cell = create_autotest_solution_cell() - test_cell = create_autotest_test_cell() - load_cell = create_file_loader_cell('grades.csv') - test_cell.metadata['nbgrader'] = {'grade': True} + ## test that autotest starts a kernel that uses the `path` metadata as working directory + #def test_kernel_workingdir(self, preprocessor, caplog): + # sol_cell = create_autotest_solution_cell() + # test_cell = create_autotest_test_cell() + # load_cell = create_file_loader_cell('grades.csv') + # test_cell.metadata['nbgrader'] = {'grade': True} - # with the right path, the kernel should load the file - nb = new_notebook() - nb.metadata['kernelspec'] = { - "name": "python3" - } - nb.cells.append(sol_cell) - nb.cells.append(test_cell) - nb.cells.append(load_cell) - resources = { - 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} - } - nb, resources = preprocessor.preprocess(nb, resources) + # # with the right path, the kernel should load the file + # nb = new_notebook() + # nb.metadata['kernelspec'] = { + # "name": "python3" + # } + # nb.cells.append(sol_cell) + # nb.cells.append(test_cell) + # nb.cells.append(load_cell) + # resources = { + # 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} + # } + # nb, resources = preprocessor.preprocess(nb, resources) - # without the right path, the kernel should report an error - nb = new_notebook() - nb.metadata['kernelspec'] = { - "name": "python3" - } - nb.cells.append(sol_cell) - nb.cells.append(test_cell) - nb.cells.append(load_cell) - resources = { - 'metadata': {'path': 'nbgrader/docs/source/user_guide/source/'} - } - with pytest.raises(Exception): - nb, resources = preprocessor.preprocess(nb, resources) - assert "FileNotFoundError" in caplog.text + # # without the right path, the kernel should report an error + # nb = new_notebook() + # nb.metadata['kernelspec'] = { + # "name": "python3" + # } + # nb.cells.append(sol_cell) + # nb.cells.append(test_cell) + # nb.cells.append(load_cell) + # resources = { + # 'metadata': {'path': 'nbgrader/docs/source/user_guide/source/'} + # } + # with pytest.raises(Exception): + # nb, resources = preprocessor.preprocess(nb, resources) + # assert "FileNotFoundError" in caplog.text # test that a warning is thrown when we set enforce_metadata = False and have an AUTOTEST directive in a # non-grade cell From 037141d11cec5c8a50a0b99b7e03e9860ea33dfb Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Thu, 31 Aug 2023 12:30:56 -0700 Subject: [PATCH 37/46] try error --- nbgrader/tests/preprocessors/test_instantiatetests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbgrader/tests/preprocessors/test_instantiatetests.py b/nbgrader/tests/preprocessors/test_instantiatetests.py index 08712cf0a..cbba850f0 100644 --- a/nbgrader/tests/preprocessors/test_instantiatetests.py +++ b/nbgrader/tests/preprocessors/test_instantiatetests.py @@ -140,7 +140,6 @@ def test_warning_autotest_nongrade(self, preprocessor, caplog): } nb, resources = preprocessor.preprocess(nb, resources) - assert False, caplog.text assert "AutoTest region detected in a non-grade cell; " in caplog.text # test that an error is thrown when we have an AUTOTEST directive in a non-grade cell @@ -160,6 +159,7 @@ def test_error_autotest_nongrade(self, preprocessor, caplog): with pytest.raises(Exception): nb, resources = preprocessor.preprocess(nb, resources) + assert False, caplog.text assert "AutoTest region detected in a non-grade cell; " in caplog.text # test that invalid python statements in AUTOTEST directives cause errors From 91b5789f71a625bb35df8cbbe2d2ad5f4085894f Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Thu, 31 Aug 2023 12:47:01 -0700 Subject: [PATCH 38/46] trying to figure out workdir test --- nbgrader/preprocessors/instantiatetests.py | 10 ++- .../preprocessors/test_instantiatetests.py | 65 +++++++++---------- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index 6e85e1fdd..5578df4a0 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -133,10 +133,16 @@ def preprocess(self, nb, resources): self.log.debug('Starting client for kernel %s at path %s', kernel_name, resources_path if resources_path is not None else '') km, self.kc = start_new_kernel(kernel_name=kernel_name, cwd=resources_path) + self.log.error('here is the notebook before') + self.log.error(nb) + # run the preprocessor self.log.debug('Running InstantiateTests preprocessor') nb, resources = super(InstantiateTests, self).preprocess(nb, resources) + self.log.error('here is the notebook after') + self.log.error(nb) + # shut down and cleanup the kernel self.log.debug('Shutting down / cleaning up kernel') km.shutdown_kernel() @@ -161,11 +167,11 @@ def preprocess_cell(self, cell, resources, index): # get the comment string for this language comment_str = self.comment_strs[resources["kernel_name"]] - # seed the salt generator for this cell + # seed the salt generator for this cell # avoid actual random seeds so that release versions are consistent across # calls to nbgrader generate_assignment salt_int = int(hashlib.sha256((cell.source+str(index)).encode('utf-8')).hexdigest(), 16) % 10**6 - + # split the code lines into separate strings lines = cell.source.split("\n") diff --git a/nbgrader/tests/preprocessors/test_instantiatetests.py b/nbgrader/tests/preprocessors/test_instantiatetests.py index cbba850f0..ff34ec69d 100644 --- a/nbgrader/tests/preprocessors/test_instantiatetests.py +++ b/nbgrader/tests/preprocessors/test_instantiatetests.py @@ -87,40 +87,40 @@ def test_consistent_release_version(self, preprocessor): nb2, resources2 = preprocessor.preprocess(nb2, resources2) assert nb1['cells'][1]['source'] == nb2['cells'][1]['source'] - ## test that autotest starts a kernel that uses the `path` metadata as working directory - #def test_kernel_workingdir(self, preprocessor, caplog): - # sol_cell = create_autotest_solution_cell() - # test_cell = create_autotest_test_cell() - # load_cell = create_file_loader_cell('grades.csv') - # test_cell.metadata['nbgrader'] = {'grade': True} + # test that autotest starts a kernel that uses the `path` metadata as working directory + def test_kernel_workingdir(self, preprocessor, caplog): + sol_cell = create_autotest_solution_cell() + test_cell = create_autotest_test_cell() + load_cell = create_file_loader_cell('grades.csv') + test_cell.metadata['nbgrader'] = {'grade': True} - # # with the right path, the kernel should load the file - # nb = new_notebook() - # nb.metadata['kernelspec'] = { - # "name": "python3" - # } - # nb.cells.append(sol_cell) - # nb.cells.append(test_cell) - # nb.cells.append(load_cell) - # resources = { - # 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} - # } - # nb, resources = preprocessor.preprocess(nb, resources) + # with the right path, the kernel should load the file + nb = new_notebook() + nb.metadata['kernelspec'] = { + "name": "python3" + } + nb.cells.append(sol_cell) + nb.cells.append(test_cell) + nb.cells.append(load_cell) + resources = { + 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} + } + nb, resources = preprocessor.preprocess(nb, resources) - # # without the right path, the kernel should report an error - # nb = new_notebook() - # nb.metadata['kernelspec'] = { - # "name": "python3" - # } - # nb.cells.append(sol_cell) - # nb.cells.append(test_cell) - # nb.cells.append(load_cell) - # resources = { - # 'metadata': {'path': 'nbgrader/docs/source/user_guide/source/'} - # } - # with pytest.raises(Exception): - # nb, resources = preprocessor.preprocess(nb, resources) - # assert "FileNotFoundError" in caplog.text + # without the right path, the kernel should report an error + nb = new_notebook() + nb.metadata['kernelspec'] = { + "name": "python3" + } + nb.cells.append(sol_cell) + nb.cells.append(test_cell) + nb.cells.append(load_cell) + resources = { + 'metadata': {'path': 'nbgrader/docs/source/user_guide/source/'} + } + with pytest.raises(Exception): + nb, resources = preprocessor.preprocess(nb, resources) + assert "FileNotFoundError" in caplog.text # test that a warning is thrown when we set enforce_metadata = False and have an AUTOTEST directive in a # non-grade cell @@ -159,7 +159,6 @@ def test_error_autotest_nongrade(self, preprocessor, caplog): with pytest.raises(Exception): nb, resources = preprocessor.preprocess(nb, resources) - assert False, caplog.text assert "AutoTest region detected in a non-grade cell; " in caplog.text # test that invalid python statements in AUTOTEST directives cause errors From bc775265ae4bf40d1a1b6d31efc6f8e13501349d Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Thu, 31 Aug 2023 13:01:56 -0700 Subject: [PATCH 39/46] more error output --- nbgrader/preprocessors/instantiatetests.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index 5578df4a0..8bcd589fb 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -426,6 +426,8 @@ def _execute_code_snippet(self, code): return res def _execute_code_snippet_output_hook(self, msg: t.Dict[str, t.Any]) -> None: + self.log.error('message') + self.log.error(msg) msg_type = msg["header"]["msg_type"] content = msg["content"] if msg_type == "stream": From 644c116cbb2aa5196eb1036da0e3d85ac6768132 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Thu, 31 Aug 2023 13:30:28 -0700 Subject: [PATCH 40/46] more observability --- nbgrader/preprocessors/instantiatetests.py | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/nbgrader/preprocessors/instantiatetests.py b/nbgrader/preprocessors/instantiatetests.py index 8bcd589fb..65d04fcda 100644 --- a/nbgrader/preprocessors/instantiatetests.py +++ b/nbgrader/preprocessors/instantiatetests.py @@ -135,6 +135,18 @@ def preprocess(self, nb, resources): self.log.error('here is the notebook before') self.log.error(nb) + self.log.error('here is where we are') + self.log.error(os.getcwd()) + self.log.error('here is in our workdir') + self.log.error(os.listdir()) + self.log.error('here is where the kernel thinks it is') + self._execute_code_snippet("import os") + res = self._execute_code_snippet("os.getcwd()") + self.log.error(res) + self.log.error('here is what kernel sees in its workdir') + res = self._execute_code_snippet("os.listdir()") + self.log.error(res) + # run the preprocessor self.log.debug('Running InstantiateTests preprocessor') @@ -142,6 +154,17 @@ def preprocess(self, nb, resources): self.log.error('here is the notebook after') self.log.error(nb) + self.log.error('here is where we are') + self.log.error(os.getcwd()) + self.log.error('here is in our workdir') + self.log.error(os.listdir()) + self.log.error('here is where the kernel thinks it is') + self._execute_code_snippet("import os") + res = self._execute_code_snippet("os.getcwd()") + self.log.error(res) + self.log.error('here is what kernel sees in its workdir') + res = self._execute_code_snippet("os.listdir()") + self.log.error(res) # shut down and cleanup the kernel self.log.debug('Shutting down / cleaning up kernel') From ec86a9a2459a63621a551d7577d90b4c6a997e59 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Thu, 31 Aug 2023 13:47:21 -0700 Subject: [PATCH 41/46] test debugging --- .../preprocessors/test_instantiatetests.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/nbgrader/tests/preprocessors/test_instantiatetests.py b/nbgrader/tests/preprocessors/test_instantiatetests.py index ff34ec69d..a7134f228 100644 --- a/nbgrader/tests/preprocessors/test_instantiatetests.py +++ b/nbgrader/tests/preprocessors/test_instantiatetests.py @@ -94,18 +94,18 @@ def test_kernel_workingdir(self, preprocessor, caplog): load_cell = create_file_loader_cell('grades.csv') test_cell.metadata['nbgrader'] = {'grade': True} - # with the right path, the kernel should load the file - nb = new_notebook() - nb.metadata['kernelspec'] = { - "name": "python3" - } - nb.cells.append(sol_cell) - nb.cells.append(test_cell) - nb.cells.append(load_cell) - resources = { - 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} - } - nb, resources = preprocessor.preprocess(nb, resources) + ## with the right path, the kernel should load the file + #nb = new_notebook() + #nb.metadata['kernelspec'] = { + # "name": "python3" + #} + #nb.cells.append(sol_cell) + #nb.cells.append(test_cell) + #nb.cells.append(load_cell) + #resources = { + # 'metadata': {'path': 'nbgrader/docs/source/user_guide/'} + #} + #nb, resources = preprocessor.preprocess(nb, resources) # without the right path, the kernel should report an error nb = new_notebook() From 95174e9863ba468dedd6e28c02304aa0666f3552 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Thu, 31 Aug 2023 14:07:23 -0700 Subject: [PATCH 42/46] move autotests file --- nbgrader/tests/preprocessors/test_instantiatetests.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nbgrader/tests/preprocessors/test_instantiatetests.py b/nbgrader/tests/preprocessors/test_instantiatetests.py index a7134f228..d30ccca6e 100644 --- a/nbgrader/tests/preprocessors/test_instantiatetests.py +++ b/nbgrader/tests/preprocessors/test_instantiatetests.py @@ -1,4 +1,5 @@ import pytest +import shutil import os from textwrap import dedent from ...preprocessors import InstantiateTests @@ -118,8 +119,15 @@ def test_kernel_workingdir(self, preprocessor, caplog): resources = { 'metadata': {'path': 'nbgrader/docs/source/user_guide/source/'} } + # make sure autotest doesn't fail prior to running the + # preprocessor because it can't find autotests.yml + # we want it to fail because it can't find a resource file + shutil.copyfile('autotests.yml', 'source/autotests.yml') with pytest.raises(Exception): nb, resources = preprocessor.preprocess(nb, resources) + # remove the temporary resource + os.remove('source/autotests.yml') + assert "FileNotFoundError" in caplog.text # test that a warning is thrown when we set enforce_metadata = False and have an AUTOTEST directive in a From 39f6ac13a763ec619213fa1b92737819282fb6b9 Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Thu, 31 Aug 2023 14:22:40 -0700 Subject: [PATCH 43/46] remove tests to speed things up --- nbgrader/tests/api/__init__.py | 0 nbgrader/tests/api/test_gradebook.py | 1312 ----------------- nbgrader/tests/api/test_models.py | 1278 ---------------- nbgrader/tests/apps/__init__.py | 0 nbgrader/tests/apps/base.py | 71 - nbgrader/tests/apps/conftest.py | 134 -- nbgrader/tests/apps/files/__init__.py | 0 .../apps/files/autotest-hashed-changed.ipynb | 112 -- .../files/autotest-hashed-unchanged.ipynb | 110 -- .../tests/apps/files/autotest-hashed.ipynb | 82 -- .../files/autotest-hidden-changed-right.ipynb | 85 -- .../files/autotest-hidden-changed-wrong.ipynb | 85 -- .../files/autotest-hidden-unchanged.ipynb | 83 -- .../tests/apps/files/autotest-hidden.ipynb | 80 - .../apps/files/autotest-multi-changed.ipynb | 267 ---- .../apps/files/autotest-multi-unchanged.ipynb | 257 ---- .../tests/apps/files/autotest-multi.ipynb | 164 --- .../apps/files/autotest-simple-changed.ipynb | 92 -- .../files/autotest-simple-unchanged.ipynb | 90 -- .../tests/apps/files/autotest-simple.ipynb | 74 - nbgrader/tests/apps/files/autotests.yml | 310 ---- nbgrader/tests/apps/files/data.txt | 4 - nbgrader/tests/apps/files/gradebook.db | Bin 35840 -> 0 bytes .../files/infinite-loop-with-output.ipynb | 26 - nbgrader/tests/apps/files/infinite-loop.ipynb | 25 - nbgrader/tests/apps/files/jupyter.png | Bin 21090 -> 0 bytes nbgrader/tests/apps/files/myexporter.py | 6 - nbgrader/tests/apps/files/notebooks.zip | Bin 18018 -> 0 bytes .../tests/apps/files/open_relative_file.ipynb | 37 - nbgrader/tests/apps/files/side-effects.ipynb | 25 - .../tests/apps/files/submitted-changed.ipynb | 157 -- .../submitted-cheat-attempt-alternative.ipynb | 158 -- .../apps/files/submitted-cheat-attempt.ipynb | 158 -- .../files/submitted-grade-cell-changed.ipynb | 157 -- .../files/submitted-locked-cell-changed.ipynb | 158 -- .../apps/files/submitted-unchanged.ipynb | 157 -- .../tests/apps/files/test-hidden-tests.ipynb | 258 ---- .../files/test-no-metadata-autotest.ipynb | 225 --- .../tests/apps/files/test-no-metadata.ipynb | 229 --- .../tests/apps/files/test-v0-invalid.ipynb | 314 ---- nbgrader/tests/apps/files/test-v0.ipynb | 315 ---- nbgrader/tests/apps/files/test-v1.ipynb | 300 ---- nbgrader/tests/apps/files/test-v2.ipynb | 300 ---- .../tests/apps/files/test-with-output.ipynb | 322 ---- nbgrader/tests/apps/files/test.ipynb | 300 ---- nbgrader/tests/apps/files/timeout.ipynb | 70 - nbgrader/tests/apps/files/timestamp.txt | 1 - nbgrader/tests/apps/files/too-new.ipynb | 32 - .../validating-environment-variable.ipynb | 55 - .../apps/files/validation-zero-points.ipynb | 75 - nbgrader/tests/apps/test_api.py | 818 ---------- nbgrader/tests/apps/test_config.py | 17 - nbgrader/tests/apps/test_nbgrader.py | 38 - .../tests/apps/test_nbgrader_autograde.py | 1287 ---------------- nbgrader/tests/apps/test_nbgrader_collect.py | 197 --- nbgrader/tests/apps/test_nbgrader_db.py | 423 ------ nbgrader/tests/apps/test_nbgrader_export.py | 65 - .../apps/test_nbgrader_fetch_assignment.py | 115 -- .../tests/apps/test_nbgrader_fetchfeedback.py | 93 -- .../tests/apps/test_nbgrader_formgrade.py | 9 - .../apps/test_nbgrader_generate_assignment.py | 432 ------ .../apps/test_nbgrader_generate_config.py | 30 - .../apps/test_nbgrader_generate_feedback.py | 383 ----- .../apps/test_nbgrader_generate_solution.py | 142 -- nbgrader/tests/apps/test_nbgrader_list.py | 500 ------- .../tests/apps/test_nbgrader_quickstart.py | 162 -- .../apps/test_nbgrader_releaseassignment.py | 106 -- .../apps/test_nbgrader_releasefeedback.py | 138 -- nbgrader/tests/apps/test_nbgrader_submit.py | 251 ---- nbgrader/tests/apps/test_nbgrader_update.py | 114 -- nbgrader/tests/apps/test_nbgrader_validate.py | 189 --- .../tests/apps/test_nbgrader_zip_collect.py | 691 --------- .../preprocessors/test_checkcellmetadata.py | 103 -- .../preprocessors/test_clearhiddentests.py | 207 --- .../preprocessors/test_clearmarkscheme.py | 184 --- .../preprocessors/test_clearsolutions.py | 238 --- .../preprocessors/test_computechecksums.py | 119 -- .../preprocessors/test_deduplicateids.py | 53 - .../tests/preprocessors/test_getgrades.py | 133 -- .../tests/preprocessors/test_headerfooter.py | 48 - .../preprocessors/test_instantiatetests.py | 4 +- .../tests/preprocessors/test_limitoutput.py | 38 - .../tests/preprocessors/test_lockcells.py | 170 --- .../preprocessors/test_overwritecells.py | 267 ---- .../preprocessors/test_overwritekernelspec.py | 59 - .../preprocessors/test_saveautogrades.py | 216 --- .../tests/preprocessors/test_savecells.py | 311 ---- 87 files changed, 2 insertions(+), 16898 deletions(-) delete mode 100644 nbgrader/tests/api/__init__.py delete mode 100644 nbgrader/tests/api/test_gradebook.py delete mode 100644 nbgrader/tests/api/test_models.py delete mode 100644 nbgrader/tests/apps/__init__.py delete mode 100644 nbgrader/tests/apps/base.py delete mode 100644 nbgrader/tests/apps/conftest.py delete mode 100644 nbgrader/tests/apps/files/__init__.py delete mode 100644 nbgrader/tests/apps/files/autotest-hashed-changed.ipynb delete mode 100644 nbgrader/tests/apps/files/autotest-hashed-unchanged.ipynb delete mode 100644 nbgrader/tests/apps/files/autotest-hashed.ipynb delete mode 100644 nbgrader/tests/apps/files/autotest-hidden-changed-right.ipynb delete mode 100644 nbgrader/tests/apps/files/autotest-hidden-changed-wrong.ipynb delete mode 100644 nbgrader/tests/apps/files/autotest-hidden-unchanged.ipynb delete mode 100644 nbgrader/tests/apps/files/autotest-hidden.ipynb delete mode 100644 nbgrader/tests/apps/files/autotest-multi-changed.ipynb delete mode 100644 nbgrader/tests/apps/files/autotest-multi-unchanged.ipynb delete mode 100644 nbgrader/tests/apps/files/autotest-multi.ipynb delete mode 100644 nbgrader/tests/apps/files/autotest-simple-changed.ipynb delete mode 100644 nbgrader/tests/apps/files/autotest-simple-unchanged.ipynb delete mode 100644 nbgrader/tests/apps/files/autotest-simple.ipynb delete mode 100644 nbgrader/tests/apps/files/autotests.yml delete mode 100644 nbgrader/tests/apps/files/data.txt delete mode 100644 nbgrader/tests/apps/files/gradebook.db delete mode 100644 nbgrader/tests/apps/files/infinite-loop-with-output.ipynb delete mode 100644 nbgrader/tests/apps/files/infinite-loop.ipynb delete mode 100644 nbgrader/tests/apps/files/jupyter.png delete mode 100644 nbgrader/tests/apps/files/myexporter.py delete mode 100644 nbgrader/tests/apps/files/notebooks.zip delete mode 100644 nbgrader/tests/apps/files/open_relative_file.ipynb delete mode 100644 nbgrader/tests/apps/files/side-effects.ipynb delete mode 100644 nbgrader/tests/apps/files/submitted-changed.ipynb delete mode 100644 nbgrader/tests/apps/files/submitted-cheat-attempt-alternative.ipynb delete mode 100644 nbgrader/tests/apps/files/submitted-cheat-attempt.ipynb delete mode 100644 nbgrader/tests/apps/files/submitted-grade-cell-changed.ipynb delete mode 100644 nbgrader/tests/apps/files/submitted-locked-cell-changed.ipynb delete mode 100644 nbgrader/tests/apps/files/submitted-unchanged.ipynb delete mode 100644 nbgrader/tests/apps/files/test-hidden-tests.ipynb delete mode 100644 nbgrader/tests/apps/files/test-no-metadata-autotest.ipynb delete mode 100644 nbgrader/tests/apps/files/test-no-metadata.ipynb delete mode 100644 nbgrader/tests/apps/files/test-v0-invalid.ipynb delete mode 100644 nbgrader/tests/apps/files/test-v0.ipynb delete mode 100644 nbgrader/tests/apps/files/test-v1.ipynb delete mode 100644 nbgrader/tests/apps/files/test-v2.ipynb delete mode 100644 nbgrader/tests/apps/files/test-with-output.ipynb delete mode 100644 nbgrader/tests/apps/files/test.ipynb delete mode 100644 nbgrader/tests/apps/files/timeout.ipynb delete mode 100644 nbgrader/tests/apps/files/timestamp.txt delete mode 100644 nbgrader/tests/apps/files/too-new.ipynb delete mode 100644 nbgrader/tests/apps/files/validating-environment-variable.ipynb delete mode 100644 nbgrader/tests/apps/files/validation-zero-points.ipynb delete mode 100644 nbgrader/tests/apps/test_api.py delete mode 100644 nbgrader/tests/apps/test_config.py delete mode 100644 nbgrader/tests/apps/test_nbgrader.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_autograde.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_collect.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_db.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_export.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_fetch_assignment.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_fetchfeedback.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_formgrade.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_generate_assignment.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_generate_config.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_generate_feedback.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_generate_solution.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_list.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_quickstart.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_releaseassignment.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_releasefeedback.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_submit.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_update.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_validate.py delete mode 100644 nbgrader/tests/apps/test_nbgrader_zip_collect.py delete mode 100644 nbgrader/tests/preprocessors/test_checkcellmetadata.py delete mode 100644 nbgrader/tests/preprocessors/test_clearhiddentests.py delete mode 100644 nbgrader/tests/preprocessors/test_clearmarkscheme.py delete mode 100644 nbgrader/tests/preprocessors/test_clearsolutions.py delete mode 100644 nbgrader/tests/preprocessors/test_computechecksums.py delete mode 100644 nbgrader/tests/preprocessors/test_deduplicateids.py delete mode 100644 nbgrader/tests/preprocessors/test_getgrades.py delete mode 100644 nbgrader/tests/preprocessors/test_headerfooter.py delete mode 100644 nbgrader/tests/preprocessors/test_limitoutput.py delete mode 100644 nbgrader/tests/preprocessors/test_lockcells.py delete mode 100644 nbgrader/tests/preprocessors/test_overwritecells.py delete mode 100644 nbgrader/tests/preprocessors/test_overwritekernelspec.py delete mode 100644 nbgrader/tests/preprocessors/test_saveautogrades.py delete mode 100644 nbgrader/tests/preprocessors/test_savecells.py diff --git a/nbgrader/tests/api/__init__.py b/nbgrader/tests/api/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/nbgrader/tests/api/test_gradebook.py b/nbgrader/tests/api/test_gradebook.py deleted file mode 100644 index 0b80ae262..000000000 --- a/nbgrader/tests/api/test_gradebook.py +++ /dev/null @@ -1,1312 +0,0 @@ -import pytest - -from datetime import datetime, timedelta -from ... import api -from ... import utils -from ...api import InvalidEntry, MissingEntry -from _pytest.fixtures import SubRequest -from nbgrader.api import Gradebook - - -@pytest.fixture -def gradebook(request: SubRequest) -> Gradebook: - gb = api.Gradebook("sqlite:///:memory:") - - def fin() -> None: - gb.close() - request.addfinalizer(fin) - return gb - - -@pytest.fixture -def assignment(gradebook: Gradebook) -> Gradebook: - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - gradebook.add_grade_cell('test1', 'p1', 'foo', max_score=1, cell_type='code') - gradebook.add_grade_cell('test2', 'p1', 'foo', max_score=2, cell_type='markdown') - gradebook.add_solution_cell('solution1', 'p1', 'foo') - gradebook.add_solution_cell('test2', 'p1', 'foo') - gradebook.add_source_cell('test1', 'p1', 'foo', cell_type='code') - gradebook.add_source_cell('test2', 'p1', 'foo', cell_type='markdown') - gradebook.add_source_cell('solution1', 'p1', 'foo', cell_type='code') - return gradebook - - -def makeAssignments(gb, na, nn, ns, grades=[1, 2, 10, 20, 100, 200]): - for si in range(ns): - sname = "s{0}".format(si + 1) - gb.add_student(sname) - for ia in range(na): - aname = 'a{0}'.format(ia + 1) - a = gb.add_assignment(aname) - for ni in range(nn): - nname = 'n{0}'.format(ni + 1) - n = gb.add_notebook(nname, aname) - gb.add_solution_cell('solution1', nname, aname) - gb.add_solution_cell('solution2', nname, aname) - gb.add_source_cell('source1', nname, aname, cell_type='code') - gb.add_source_cell('source2', nname, aname, cell_type='markdown') - gb.add_source_cell('solution1', nname, aname, cell_type='code') - gb.add_grade_cell('grade_code1', nname, aname, cell_type='code', max_score=2) - gb.add_grade_cell('grade_code2', nname, aname, cell_type='code', max_score=3) - gb.add_grade_cell('grade_written1', nname, aname, cell_type='markdown', max_score=20) - gb.add_grade_cell('grade_written2', nname, aname, cell_type='markdown', max_score=30) - gb.add_task_cell('task1', nname, aname, cell_type='markdown', max_score=200) - gb.add_task_cell('task2', nname, aname, cell_type='markdown', max_score=300) - for si in range(ns): - sname = "s{0}".format(si + 1) - sub = gb.add_submission(aname, sname) - sub.flagged = False - for ni in range(nn): - nname = 'n{0}'.format(ni + 1) - g1 = gb.find_grade("grade_code1", nname, aname, sname) - g2 = gb.find_grade("grade_code2", nname, aname, sname) - g3 = gb.find_grade("grade_written1", nname, aname, sname) - g4 = gb.find_grade("grade_written2", nname, aname, sname) - g5 = gb.find_grade("task1", nname, aname, sname) - g6 = gb.find_grade("task2", nname, aname, sname) - - (g1.manual_score, g2.manual_score, g3.manual_score, g4.manual_score, - g5.manual_score, g6.manual_score) = grades - gb.db.commit() - - return gb - - -@pytest.fixture -def FiveStudents(gradebook): - return makeAssignments(gradebook, 1, 1, 5) - - -@pytest.fixture -def FiveNotebooks(gradebook): - return makeAssignments(gradebook, 1, 5, 1) - - -@pytest.fixture -def FiveAssignments(gradebook): - return makeAssignments(gradebook, 5, 1, 1) - - -@pytest.fixture -def assignmentWithTask(gradebook: Gradebook) -> Gradebook: - for f in ['foo', 'foo2']: - gradebook.add_assignment(f) - for n in ['p1', 'p2']: - gradebook.add_notebook(n, f) - gradebook.add_solution_cell('solution1', n, f) - gradebook.add_solution_cell('test2', n, f) - gradebook.add_source_cell('test1', n, f, cell_type='code') - gradebook.add_source_cell('test2', n, f, cell_type='markdown') - gradebook.add_source_cell('solution1', n, f, cell_type='code') - gradebook.add_grade_cell('grade_code1', n, f, cell_type='code', max_score=1) - gradebook.add_grade_cell('grade_code2', n, f, cell_type='code', max_score=10) - gradebook.add_grade_cell('grade_written1', n, f, cell_type='markdown', max_score=1) - gradebook.add_grade_cell('grade_written2', n, f, cell_type='markdown', max_score=10) - gradebook.add_task_cell('task1', n, f, cell_type='markdown', max_score=2) - gradebook.add_task_cell('task2', n, f, cell_type='markdown', max_score=20) - - return gradebook - - -@pytest.fixture -def assignmentWithSubmissionNoMarks(assignmentWithTask: Gradebook) -> Gradebook: - assignmentWithTask.add_student('hacker123') - assignmentWithTask.add_student('bitdiddle') - assignmentWithTask.add_student('louisreasoner') - s1 = assignmentWithTask.add_submission('foo', 'hacker123') - s2 = assignmentWithTask.add_submission('foo', 'bitdiddle') - s1.flagged = True - s2.flagged = False - assignmentWithTask.db.commit() - return assignmentWithTask - -possiblegrades = [ - [0.5, 2, 3, 5, 1, 7, 2, 1], - [0.1, 4, 0.25, 1, 7, 0.0, 1, 1], - [0] * 8, - [2] * 8, - [0.25] * 8, -] - - -@pytest.fixture(params=possiblegrades) -def assignmentWithSubmissionWithMarks(assignmentWithSubmissionNoMarks: Gradebook, request: SubRequest) -> Gradebook: - a = assignmentWithSubmissionNoMarks - g1 = a.find_grade("grade_code1", "p1", "foo", "bitdiddle") - g2 = a.find_grade("grade_code2", "p1", "foo", "bitdiddle") - - g3 = a.find_grade("grade_written1", "p1", "foo", "hacker123") - g4 = a.find_grade("grade_written2", "p1", "foo", "hacker123") - - g5 = a.find_grade("task1", "p1", "foo", "bitdiddle") - g6 = a.find_grade("task2", "p1", "foo", "bitdiddle") - g7 = a.find_grade("task1", "p1", "foo", "hacker123") - g8 = a.find_grade("task2", "p1", "foo", "hacker123") - - (g1.manual_score, g2.manual_score, g3.manual_score, g4.manual_score, g5.manual_score, - g6.manual_score, g7.manual_score, g8.manual_score) = request.param - a.db.commit() - a.usedgrades = request.param - a.usedgrades_code = request.param[:2] - a.usedgrades_written = request.param[2:4] - a.usedgrades_task = request.param[4:] - - return a - - -@pytest.fixture -def assignmentManyStudents(assignmentWithTask, request): - a = assignmentWithTask - for s in range(50): - sname = 's{0}'.format(s) - a.add_student(sname) - sub = a.add_submission('foo', sname) - g1 = a.find_grade("grade_code1", "p1", "foo", sname) - g2 = a.find_grade("grade_written1", "p1", "foo", sname) - g3 = a.find_grade("task1", "p1", "foo", sname) - g4 = a.find_grade("task2", "p1", "foo", sname) - - ( - g1.manual_score, - g2.manual_score, - g3.manual_score, - g4.manual_score) = (1, 2, 3, 4) - a.db.commit() - - return a - - -@pytest.fixture -def assignmentTwoStudents(assignmentWithTask, request): - a = assignmentWithTask - for s in range(50): - sname = 's{0}'.format(s) - a.add_student(sname) - sub = a.add_submission('foo', sname) - g1 = a.find_grade("grade_code1", "p1", "foo", sname) - g2 = a.find_grade("grade_written1", "p1", "foo", sname) - g3 = a.find_grade("task1", "p1", "foo", sname) - g4 = a.find_grade("task2", "p1", "foo", sname) - - ( - g1.manual_score, - g2.manual_score, - g3.manual_score, - g4.manual_score) = (1, 2, 3, 4) - a.db.commit() - - return a - - -def test_init(gradebook: Gradebook) -> None: - assert gradebook.students == [] - assert gradebook.assignments == [] - - -# Test students - -def test_add_student(gradebook): - s = gradebook.add_student('12345') - assert s.id == '12345' - assert gradebook.students == [s] - - # try adding a duplicate student - with pytest.raises(InvalidEntry): - gradebook.add_student('12345') - - # try adding a student with arguments - s = gradebook.add_student('6789', last_name="Bar", first_name="Foo", email="foo@bar.com") - assert s.id == '6789' - assert s.last_name == "Bar" - assert s.first_name == "Foo" - assert s.email == "foo@bar.com" - - -def test_add_duplicate_student(gradebook): - # we also need this test because this will cause an IntegrityError - # under the hood rather than a FlushError - gradebook.add_student('12345') - with pytest.raises(InvalidEntry): - gradebook.add_student('12345') - - -def test_find_student(gradebook): - s1 = gradebook.add_student('12345') - assert gradebook.find_student('12345') == s1 - - s2 = gradebook.add_student('abcd') - assert gradebook.find_student('12345') == s1 - assert gradebook.find_student('abcd') == s2 - - -def test_find_nonexistant_student(gradebook): - with pytest.raises(MissingEntry): - gradebook.find_student('12345') - - -def test_remove_student(assignment): - assignment.add_student('hacker123') - assignment.add_submission('foo', 'hacker123') - - assignment.remove_student('hacker123') - - with pytest.raises(MissingEntry): - assignment.find_submission('foo', 'hacker123') - with pytest.raises(MissingEntry): - assignment.find_student('hacker123') - - -def test_update_or_create_student(gradebook): - # first test creating it - s1 = gradebook.update_or_create_student('hacker123') - assert gradebook.find_student('hacker123') == s1 - assert s1.first_name is None - - # now test finding/updating it - s2 = gradebook.update_or_create_student('hacker123', first_name='Alyssa') - assert s1 == s2 - assert s2.first_name == 'Alyssa' - - -# Test assignments - -def test_add_assignment(gradebook): - a = gradebook.add_assignment('foo') - assert a.name == 'foo' - assert gradebook.assignments == [a] - - # try adding a duplicate assignment - with pytest.raises(InvalidEntry): - gradebook.add_assignment('foo') - - # try adding an assignment with arguments - now = datetime.utcnow() - a = gradebook.add_assignment('bar', duedate=now) - assert a.name == 'bar' - assert a.duedate == now - - # try adding with a string timestamp - a = gradebook.add_assignment('baz', duedate=now.isoformat()) - assert a.name == 'baz' - assert a.duedate == now - - -def test_add_duplicate_assignment(gradebook): - gradebook.add_assignment('foo') - with pytest.raises(InvalidEntry): - gradebook.add_assignment('foo') - - -def test_find_assignment(gradebook): - a1 = gradebook.add_assignment('foo') - assert gradebook.find_assignment('foo') == a1 - - a2 = gradebook.add_assignment('bar') - assert gradebook.find_assignment('foo') == a1 - assert gradebook.find_assignment('bar') == a2 - - -def test_find_nonexistant_assignment(gradebook): - with pytest.raises(MissingEntry): - gradebook.find_assignment('foo') - - -def test_remove_assignment(assignment): - assignment.add_student('hacker123') - assignment.add_submission('foo', 'hacker123') - - notebooks = assignment.find_assignment('foo').notebooks - grade_cells = [x for nb in notebooks for x in nb.grade_cells] - solution_cells = [x for nb in notebooks for x in nb.solution_cells] - source_cells = [x for nb in notebooks for x in nb.source_cells] - - assignment.remove_assignment('foo') - - for nb in notebooks: - assert assignment.db.query(api.SubmittedNotebook).filter(api.SubmittedNotebook.id == nb.id).all() == [] - for grade_cell in grade_cells: - assert assignment.db.query(api.GradeCell).filter(api.GradeCell.id == grade_cell.id).all() == [] - for solution_cell in solution_cells: - assert assignment.db.query(api.SolutionCell).filter(api.SolutionCell.id == solution_cell.id).all() == [] - for source_cell in source_cells: - assert assignment.db.query(api.SourceCell).filter(api.SourceCell.id == source_cell.id).all() == [] - - with pytest.raises(MissingEntry): - assignment.find_assignment('foo') - - assert assignment.find_student('hacker123').submissions == [] - - -def test_update_or_create_assignment(gradebook): - # first test creating it - a1 = gradebook.update_or_create_assignment('foo') - assert gradebook.find_assignment('foo') == a1 - assert a1.duedate is None - - # now test finding/updating it - a2 = gradebook.update_or_create_assignment('foo', duedate="2015-02-02 14:58:23.948203 America/Los_Angeles") - assert a1 == a2 - assert a2.duedate == utils.parse_utc("2015-02-02 14:58:23.948203 America/Los_Angeles") - -# Test notebooks - - -def test_add_notebook(gradebook): - a = gradebook.add_assignment('foo') - n = gradebook.add_notebook('p1', 'foo') - assert n.name == 'p1' - assert n.assignment == a - assert a.notebooks == [n] - - # try adding a duplicate assignment - with pytest.raises(InvalidEntry): - gradebook.add_notebook('p1', 'foo') - - -def test_add_duplicate_notebook(gradebook): - # it should be ok to add a notebook with the same name, as long as - # it's for different assignments - gradebook.add_assignment('foo') - gradebook.add_assignment('bar') - n1 = gradebook.add_notebook('p1', 'foo') - n2 = gradebook.add_notebook('p1', 'bar') - assert n1.id != n2.id - - # but not ok to add a notebook with the same name for the same assignment - with pytest.raises(InvalidEntry): - gradebook.add_notebook('p1', 'foo') - - -def test_find_notebook(gradebook): - gradebook.add_assignment('foo') - n1 = gradebook.add_notebook('p1', 'foo') - assert gradebook.find_notebook('p1', 'foo') == n1 - - n2 = gradebook.add_notebook('p2', 'foo') - assert gradebook.find_notebook('p1', 'foo') == n1 - assert gradebook.find_notebook('p2', 'foo') == n2 - - -def test_find_nonexistant_notebook(gradebook: Gradebook) -> None: - # check that it doesn't find it when there is nothing in the db - with pytest.raises(MissingEntry): - gradebook.find_notebook('p1', 'foo') - - # check that it doesn't find it even if the assignment exists - gradebook.add_assignment('foo') - with pytest.raises(MissingEntry): - gradebook.find_notebook('p1', 'foo') - - -def test_update_or_create_notebook(gradebook): - # first test creating it - gradebook.add_assignment('foo') - n1 = gradebook.update_or_create_notebook('p1', 'foo') - assert gradebook.find_notebook('p1', 'foo') == n1 - - # now test finding/updating it - n2 = gradebook.update_or_create_notebook('p1', 'foo') - assert n1 == n2 - - -def test_remove_notebook(assignment): - assignment.add_student('hacker123') - assignment.add_submission('foo', 'hacker123') - - notebooks = assignment.find_assignment('foo').notebooks - - for nb in notebooks: - grade_cells = [x for x in nb.grade_cells] - solution_cells = [x for x in nb.solution_cells] - source_cells = [x for x in nb.source_cells] - - assignment.remove_notebook(nb.name, 'foo') - assert assignment.db.query(api.SubmittedNotebook).filter(api.SubmittedNotebook.id == nb.id).all() == [] - - for grade_cell in grade_cells: - assert assignment.db.query(api.GradeCell).filter(api.GradeCell.id == grade_cell.id).all() == [] - for solution_cell in solution_cells: - assert assignment.db.query(api.SolutionCell).filter(api.SolutionCell.id == solution_cell.id).all() == [] - for source_cell in source_cells: - assert assignment.db.query(api.SourceCell).filter(api.SourceCell.id == source_cell.id).all() == [] - - with pytest.raises(MissingEntry): - assignment.find_notebook(nb.name, 'foo') - - -def test_course_id_constructor(): - gb = api.Gradebook("sqlite:///:memory:") - assert gb.db.query(api.Course).first().id == "default_course" - -def test_course_id_multiple_assignments(): - course_one = "course-one" - course_two = "course-two" - - gb_one = api.Gradebook("sqlite:///:memory:", course_id=course_one) - gb_two = api.Gradebook("sqlite:///:memory:", course_id=course_two) - - assignment_one = gb_one.add_assignment('foo') - assignent_two = gb_two.add_assignment('bar') - - assert assignment_one.course_id == course_one - assert assignent_two.course_id == course_two - - assert len(gb_one.db.query(api.Course).all()) == 1 - assert gb_one.db.query(api.Course).first().id == course_one - assert gb_one.db.query(api.Assignment).first().course_id == course_one - assert gb_one.db.query(api.Assignment).first().course == gb_one.db.query(api.Course).first() - - assert len(gb_two.db.query(api.Course).all()) == 1 - assert gb_two.db.query(api.Course).first().id == course_two - assert gb_two.db.query(api.Assignment).first().course_id == course_two - assert gb_two.db.query(api.Assignment).first().course == gb_two.db.query(api.Course).first() - -# Test grade cells - -def test_add_grade_cell(gradebook): - gradebook.add_assignment('foo') - n = gradebook.add_notebook('p1', 'foo') - gc = gradebook.add_grade_cell('test1', 'p1', 'foo', max_score=2, cell_type='markdown') - assert gc.name == 'test1' - assert gc.max_score == 2 - assert gc.cell_type == 'markdown' - assert n.grade_cells == [gc] - assert gc.notebook == n - - -def test_add_grade_cell_with_args(gradebook): - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - gc = gradebook.add_grade_cell( - 'test1', 'p1', 'foo', - max_score=3, cell_type="code") - assert gc.name == 'test1' - assert gc.max_score == 3 - assert gc.cell_type == "code" - - -def test_create_invalid_grade_cell(gradebook): - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - with pytest.raises(InvalidEntry): - gradebook.add_grade_cell( - 'test1', 'p1', 'foo', - max_score=3, cell_type="something") - - -def test_add_duplicate_grade_cell(gradebook): - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - gradebook.add_grade_cell('test1', 'p1', 'foo', max_score=1, cell_type='code') - with pytest.raises(InvalidEntry): - gradebook.add_grade_cell('test1', 'p1', 'foo', max_score=2, cell_type='markdown') - - -def test_find_grade_cell(gradebook): - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - gc1 = gradebook.add_grade_cell('test1', 'p1', 'foo', max_score=1, cell_type='code') - assert gradebook.find_grade_cell('test1', 'p1', 'foo') == gc1 - - gc2 = gradebook.add_grade_cell('test2', 'p1', 'foo', max_score=2, cell_type='code') - assert gradebook.find_grade_cell('test1', 'p1', 'foo') == gc1 - assert gradebook.find_grade_cell('test2', 'p1', 'foo') == gc2 - - -def test_find_nonexistant_grade_cell(gradebook): - with pytest.raises(MissingEntry): - gradebook.find_grade_cell('test1', 'p1', 'foo') - - gradebook.add_assignment('foo') - with pytest.raises(MissingEntry): - gradebook.find_grade_cell('test1', 'p1', 'foo') - - gradebook.add_notebook('p1', 'foo') - with pytest.raises(MissingEntry): - gradebook.find_grade_cell('test1', 'p1', 'foo') - - -def test_update_or_create_grade_cell(gradebook): - # first test creating it - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - gc1 = gradebook.update_or_create_grade_cell('test1', 'p1', 'foo', max_score=2, cell_type='code') - assert gc1.max_score == 2 - assert gc1.cell_type == 'code' - assert gradebook.find_grade_cell('test1', 'p1', 'foo') == gc1 - - # now test finding/updating it - gc2 = gradebook.update_or_create_grade_cell('test1', 'p1', 'foo', max_score=3) - assert gc1 == gc2 - assert gc1.max_score == 3 - assert gc1.cell_type == 'code' - - -# Test solution cells - -def test_add_solution_cell(gradebook): - gradebook.add_assignment('foo') - n = gradebook.add_notebook('p1', 'foo') - sc = gradebook.add_solution_cell('test1', 'p1', 'foo') - assert sc.name == 'test1' - assert n.solution_cells == [sc] - assert sc.notebook == n - - -def test_add_duplicate_solution_cell(gradebook): - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - gradebook.add_solution_cell('test1', 'p1', 'foo') - with pytest.raises(InvalidEntry): - gradebook.add_solution_cell('test1', 'p1', 'foo') - - -def test_find_solution_cell(gradebook): - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - sc1 = gradebook.add_solution_cell('test1', 'p1', 'foo') - assert gradebook.find_solution_cell('test1', 'p1', 'foo') == sc1 - - sc2 = gradebook.add_solution_cell('test2', 'p1', 'foo') - assert gradebook.find_solution_cell('test1', 'p1', 'foo') == sc1 - assert gradebook.find_solution_cell('test2', 'p1', 'foo') == sc2 - - -def test_find_nonexistant_solution_cell(gradebook): - with pytest.raises(MissingEntry): - gradebook.find_solution_cell('test1', 'p1', 'foo') - - gradebook.add_assignment('foo') - with pytest.raises(MissingEntry): - gradebook.find_solution_cell('test1', 'p1', 'foo') - - gradebook.add_notebook('p1', 'foo') - with pytest.raises(MissingEntry): - gradebook.find_solution_cell('test1', 'p1', 'foo') - - -def test_update_or_create_solution_cell(gradebook: Gradebook) -> None: - # first test creating it - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - sc1 = gradebook.update_or_create_solution_cell('test1', 'p1', 'foo') - assert gradebook.find_solution_cell('test1', 'p1', 'foo') == sc1 - - # now test finding/updating it - sc2 = gradebook.update_or_create_solution_cell('test1', 'p1', 'foo') - assert sc1 == sc2 - - -# Test source cells - -def test_add_source_cell(gradebook): - gradebook.add_assignment('foo') - n = gradebook.add_notebook('p1', 'foo') - sc = gradebook.add_source_cell('test1', 'p1', 'foo', cell_type="code") - assert sc.name == 'test1' - assert sc.cell_type == 'code' - assert n.source_cells == [sc] - assert sc.notebook == n - - -def test_add_source_cell_with_args(gradebook): - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - sc = gradebook.add_source_cell( - 'test1', 'p1', 'foo', - source="blah blah blah", - cell_type="code", checksum="abcde") - assert sc.name == 'test1' - assert sc.source == "blah blah blah" - assert sc.cell_type == "code" - assert sc.checksum == "abcde" - - -def test_create_invalid_source_cell(gradebook): - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - with pytest.raises(InvalidEntry): - gradebook.add_source_cell( - 'test1', 'p1', 'foo', - source="blah blah blah", - cell_type="something", checksum="abcde") - - -def test_add_duplicate_source_cell(gradebook): - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - gradebook.add_source_cell('test1', 'p1', 'foo', cell_type="code") - with pytest.raises(InvalidEntry): - gradebook.add_source_cell('test1', 'p1', 'foo', cell_type="code") - - -def test_find_source_cell(gradebook): - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - sc1 = gradebook.add_source_cell('test1', 'p1', 'foo', cell_type="code") - assert gradebook.find_source_cell('test1', 'p1', 'foo') == sc1 - - sc2 = gradebook.add_source_cell('test2', 'p1', 'foo', cell_type="code") - assert gradebook.find_source_cell('test1', 'p1', 'foo') == sc1 - assert gradebook.find_source_cell('test2', 'p1', 'foo') == sc2 - - -def test_find_nonexistant_source_cell(gradebook): - with pytest.raises(MissingEntry): - gradebook.find_source_cell('test1', 'p1', 'foo') - - gradebook.add_assignment('foo') - with pytest.raises(MissingEntry): - gradebook.find_source_cell('test1', 'p1', 'foo') - - gradebook.add_notebook('p1', 'foo') - with pytest.raises(MissingEntry): - gradebook.find_source_cell('test1', 'p1', 'foo') - - -def test_update_or_create_source_cell(gradebook): - # first test creating it - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - sc1 = gradebook.update_or_create_source_cell('test1', 'p1', 'foo', cell_type='code') - assert sc1.cell_type == 'code' - assert gradebook.find_source_cell('test1', 'p1', 'foo') == sc1 - - # now test finding/updating it - assert sc1.checksum is None - sc2 = gradebook.update_or_create_source_cell('test1', 'p1', 'foo', checksum="123456") - assert sc1 == sc2 - assert sc1.cell_type == 'code' - assert sc1.checksum == "123456" - - -# Test submissions - -def test_add_submission(assignment): - assignment.add_student('hacker123') - assignment.add_student('bitdiddle') - s1 = assignment.add_submission('foo', 'hacker123') - s2 = assignment.add_submission('foo', 'bitdiddle') - - assert assignment.assignment_submissions('foo') == [s2, s1] - assert assignment.student_submissions('hacker123') == [s1] - assert assignment.student_submissions('bitdiddle') == [s2] - assert assignment.find_submission('foo', 'hacker123') == s1 - assert assignment.find_submission('foo', 'bitdiddle') == s2 - - -def test_add_duplicate_submission(assignment): - assignment.add_student('hacker123') - assignment.add_submission('foo', 'hacker123') - with pytest.raises(InvalidEntry): - assignment.add_submission('foo', 'hacker123') - - -def test_remove_submission(assignment): - assignment.add_student('hacker123') - assignment.add_submission('foo', 'hacker123') - - submission = assignment.find_submission('foo', 'hacker123') - notebooks = submission.notebooks - grades = [x for nb in notebooks for x in nb.grades] - comments = [x for nb in notebooks for x in nb.comments] - - assignment.remove_submission('foo', 'hacker123') - - for nb in notebooks: - assert assignment.db.query(api.SubmittedNotebook).filter(api.SubmittedNotebook.id == nb.id).all() == [] - for grade in grades: - assert assignment.db.query(api.Grade).filter(api.Grade.id == grade.id).all() == [] - for comment in comments: - assert assignment.db.query(api.Comment).filter(api.Comment.id == comment.id).all() == [] - - with pytest.raises(MissingEntry): - assignment.find_submission('foo', 'hacker123') - - -def test_update_or_create_submission(assignment): - assignment.add_student('hacker123') - s1 = assignment.update_or_create_submission('foo', 'hacker123') - assert s1.timestamp is None - - s2 = assignment.update_or_create_submission('foo', 'hacker123', timestamp="2015-02-02 14:58:23.948203 America/Los_Angeles") - assert s1 == s2 - assert s2.timestamp == utils.parse_utc("2015-02-02 14:58:23.948203 America/Los_Angeles") - - -def test_find_submission_notebook(assignment): - assignment.add_student('hacker123') - s = assignment.add_submission('foo', 'hacker123') - n1, = s.notebooks - - with pytest.raises(MissingEntry): - assignment.find_submission_notebook('p2', 'foo', 'hacker123') - - n2 = assignment.find_submission_notebook('p1', 'foo', 'hacker123') - assert n1 == n2 - - -def test_find_submission_notebook_by_id(assignment): - assignment.add_student('hacker123') - s = assignment.add_submission('foo', 'hacker123') - n1, = s.notebooks - - with pytest.raises(MissingEntry): - assignment.find_submission_notebook_by_id('12345') - - n2 = assignment.find_submission_notebook_by_id(n1.id) - assert n1 == n2 - - -def test_remove_submission_notebook(assignment): - assignment.add_student('hacker123') - assignment.add_submission('foo', 'hacker123') - - submission = assignment.find_submission('foo', 'hacker123') - notebooks = submission.notebooks - - for nb in notebooks: - grades = [x for x in nb.grades] - comments = [x for x in nb.comments] - - assignment.remove_submission_notebook(nb.name, 'foo', 'hacker123') - assert assignment.db.query(api.SubmittedNotebook).filter(api.SubmittedNotebook.id == nb.id).all() == [] - - for grade in grades: - assert assignment.db.query(api.Grade).filter(api.Grade.id == grade.id).all() == [] - for comment in comments: - assert assignment.db.query(api.Comment).filter(api.Comment.id == comment.id).all() == [] - - with pytest.raises(MissingEntry): - assignment.find_submission_notebook(nb.name, 'foo', 'hacker123') - - -def test_find_grade(assignment): - assignment.add_student('hacker123') - s = assignment.add_submission('foo', 'hacker123') - n1, = s.notebooks - grades = n1.grades - - for g1 in grades: - g2 = assignment.find_grade(g1.name, 'p1', 'foo', 'hacker123') - assert g1 == g2 - - with pytest.raises(MissingEntry): - assignment.find_grade('asdf', 'p1', 'foo', 'hacker123') - - -def test_find_grade_by_id(assignment): - assignment.add_student('hacker123') - s = assignment.add_submission('foo', 'hacker123') - n1, = s.notebooks - grades = n1.grades - - for g1 in grades: - g2 = assignment.find_grade_by_id(g1.id) - assert g1 == g2 - - with pytest.raises(MissingEntry): - assignment.find_grade_by_id('12345') - - -def test_find_comment(assignment): - assignment.add_student('hacker123') - s = assignment.add_submission('foo', 'hacker123') - n1, = s.notebooks - comments = n1.comments - - for c1 in comments: - c2 = assignment.find_comment(c1.name, 'p1', 'foo', 'hacker123') - assert c1 == c2 - - with pytest.raises(MissingEntry): - assignment.find_comment('asdf', 'p1', 'foo', 'hacker123') - - -def test_find_comment_by_id(assignment): - assignment.add_student('hacker123') - s = assignment.add_submission('foo', 'hacker123') - n1, = s.notebooks - comments = n1.comments - - for c1 in comments: - c2 = assignment.find_comment_by_id(c1.id) - assert c1 == c2 - - with pytest.raises(MissingEntry): - assignment.find_comment_by_id('12345') - - -# Test average scores - -def test_average_assignment_score(assignment): - assert assignment.average_assignment_score('foo') == 0.0 - assert assignment.average_assignment_code_score('foo') == 0.0 - assert assignment.average_assignment_written_score('foo') == 0.0 - - assignment.add_student('hacker123') - assignment.add_student('bitdiddle') - assignment.add_submission('foo', 'hacker123') - assignment.add_submission('foo', 'bitdiddle') - - assert assignment.average_assignment_score('foo') == 0.0 - assert assignment.average_assignment_code_score('foo') == 0.0 - assert assignment.average_assignment_written_score('foo') == 0.0 - - g1 = assignment.find_grade("test1", "p1", "foo", "hacker123") - g2 = assignment.find_grade("test2", "p1", "foo", "hacker123") - g3 = assignment.find_grade("test1", "p1", "foo", "bitdiddle") - g4 = assignment.find_grade("test2", "p1", "foo", "bitdiddle") - - g1.manual_score = 0.5 - g2.manual_score = 2 - g3.manual_score = 1 - g4.manual_score = 1 - assignment.db.commit() - - assert assignment.average_assignment_score('foo') == 2.25 - assert assignment.average_assignment_code_score('foo') == 0.75 - assert assignment.average_assignment_written_score('foo') == 1.5 - - -def test_average_notebook_score(assignment: Gradebook) -> None: - assert assignment.average_notebook_score('p1', 'foo') == 0 - assert assignment.average_notebook_code_score('p1', 'foo') == 0 - assert assignment.average_notebook_written_score('p1', 'foo') == 0 - - assignment.add_student('hacker123') - assignment.add_student('bitdiddle') - assignment.add_submission('foo', 'hacker123') - assignment.add_submission('foo', 'bitdiddle') - - assert assignment.average_notebook_score('p1', 'foo') == 0.0 - assert assignment.average_notebook_code_score('p1', 'foo') == 0.0 - assert assignment.average_notebook_written_score('p1', 'foo') == 0.0 - - g1 = assignment.find_grade("test1", "p1", "foo", "hacker123") - g2 = assignment.find_grade("test2", "p1", "foo", "hacker123") - g3 = assignment.find_grade("test1", "p1", "foo", "bitdiddle") - g4 = assignment.find_grade("test2", "p1", "foo", "bitdiddle") - - g1.manual_score = 0.5 - g2.manual_score = 2 - g3.manual_score = 1 - g4.manual_score = 1 - assignment.db.commit() - - assert assignment.average_notebook_score('p1', 'foo') == 2.25 - assert assignment.average_notebook_code_score('p1', 'foo') == 0.75 - assert assignment.average_notebook_written_score('p1', 'foo') == 1.5 - - -# Test mass dictionary queries - -def test_student_dicts(assignment): - assignment.add_student('hacker123') - assignment.add_student('bitdiddle') - assignment.add_student('louisreasoner') - assignment.add_submission('foo', 'hacker123') - assignment.add_submission('foo', 'bitdiddle') - - g1 = assignment.find_grade("test1", "p1", "foo", "hacker123") - g2 = assignment.find_grade("test2", "p1", "foo", "hacker123") - g3 = assignment.find_grade("test1", "p1", "foo", "bitdiddle") - g4 = assignment.find_grade("test2", "p1", "foo", "bitdiddle") - - g1.manual_score = 0.5 - g2.manual_score = 2 - g3.manual_score = 1 - g4.manual_score = 1 - assignment.db.commit() - - students = assignment.student_dicts() - a = sorted(students, key=lambda x: x["id"]) - b = sorted([x.to_dict() for x in assignment.students], key=lambda x: x["id"]) - assert a == b - - -def test_student_dicts_zero_points(gradebook): - gradebook.add_assignment("ps1") - s = gradebook.add_student("1234") - assert gradebook.student_dicts() == [s.to_dict()] - - -def test_notebook_submission_dicts(assignment): - assignment.add_student('hacker123') - assignment.add_student('bitdiddle') - s1 = assignment.add_submission('foo', 'hacker123') - s2 = assignment.add_submission('foo', 'bitdiddle') - s1.flagged = True - s2.flagged = False - - g1 = assignment.find_grade("test1", "p1", "foo", "hacker123") - g2 = assignment.find_grade("test2", "p1", "foo", "hacker123") - g3 = assignment.find_grade("test1", "p1", "foo", "bitdiddle") - g4 = assignment.find_grade("test2", "p1", "foo", "bitdiddle") - - g1.manual_score = 0.5 - g2.manual_score = 2 - g3.manual_score = 1 - g4.manual_score = 1 - assignment.db.commit() - - notebook = assignment.find_notebook("p1", "foo") - submissions = assignment.notebook_submission_dicts("p1", "foo") - a = sorted(submissions, key=lambda x: x["id"]) - b = sorted([x.to_dict() for x in notebook.submissions], key=lambda x: x["id"]) - assert a == b - - -def test_submission_dicts(assignment): - assignment.add_student('hacker123') - assignment.add_student('bitdiddle') - s1 = assignment.add_submission('foo', 'hacker123') - s2 = assignment.add_submission('foo', 'bitdiddle') - s1.flagged = True - s2.flagged = False - - g1 = assignment.find_grade("test1", "p1", "foo", "hacker123") - g2 = assignment.find_grade("test2", "p1", "foo", "hacker123") - g3 = assignment.find_grade("test1", "p1", "foo", "bitdiddle") - g4 = assignment.find_grade("test2", "p1", "foo", "bitdiddle") - - g1.manual_score = 0.5 - g2.manual_score = 2 - g3.manual_score = 1 - g4.manual_score = 1 - assignment.db.commit() - - a = sorted(assignment.submission_dicts("foo"), key=lambda x: x["id"]) - b = sorted([x.to_dict() for x in assignment.find_assignment("foo").submissions], key=lambda x: x["id"]) - assert a == b - - -def test_grant_extension(gradebook): - gradebook.add_assignment("ps1", duedate="2018-05-09 10:00:00") - gradebook.add_student("hacker123") - s1 = gradebook.add_submission("ps1", "hacker123") - assert s1.extension is None - assert s1.duedate == datetime(2018, 5, 9, 10, 0, 0) - - gradebook.grant_extension('ps1', 'hacker123', minutes=10) - assert s1.extension == timedelta(minutes=10) - assert s1.duedate == datetime(2018, 5, 9, 10, 10, 0) - - gradebook.grant_extension('ps1', 'hacker123', hours=1) - assert s1.extension == timedelta(hours=1) - assert s1.duedate == datetime(2018, 5, 9, 11, 0, 0) - - gradebook.grant_extension('ps1', 'hacker123', days=2) - assert s1.extension == timedelta(days=2) - assert s1.duedate == datetime(2018, 5, 11, 10, 0, 0) - - gradebook.grant_extension('ps1', 'hacker123', weeks=3) - assert s1.extension == timedelta(weeks=3) - assert s1.duedate == datetime(2018, 5, 30, 10, 0, 0) - - gradebook.grant_extension('ps1', 'hacker123') - assert s1.extension is None - assert s1.duedate == datetime(2018, 5, 9, 10, 0, 0) - - -# Test task cells - -def test_add_task_cell(gradebook): - gradebook.add_assignment('foo') - n = gradebook.add_notebook('p1', 'foo') - gc = gradebook.add_task_cell('test1', 'p1', 'foo', max_score=2, cell_type='markdown') - assert gc.name == 'test1' - assert gc.max_score == 2 - assert gc.cell_type == 'markdown' - assert n.task_cells == [gc] - assert gc.notebook == n - - -def test_add_task_cell_with_args(gradebook): - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - gc = gradebook.add_task_cell( - 'test1', 'p1', 'foo', - max_score=3, cell_type="code") - assert gc.name == 'test1' - assert gc.max_score == 3 - assert gc.cell_type == "code" - - -def test_create_invalid_task_cell(gradebook): - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - with pytest.raises(InvalidEntry): - gradebook.add_task_cell( - 'test1', 'p1', 'foo', - max_score=3, cell_type="something") - - -def test_add_duplicate_task_cell(gradebook): - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - gradebook.add_task_cell('test1', 'p1', 'foo', max_score=1, cell_type='code') - with pytest.raises(InvalidEntry): - gradebook.add_task_cell('test1', 'p1', 'foo', max_score=2, cell_type='markdown') - - -def test_find_task_cell(gradebook): - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - gc1 = gradebook.add_task_cell('test1', 'p1', 'foo', max_score=1, cell_type='code') - assert gradebook.find_task_cell('test1', 'p1', 'foo') == gc1 - - gc2 = gradebook.add_task_cell('test2', 'p1', 'foo', max_score=2, cell_type='code') - assert gradebook.find_task_cell('test1', 'p1', 'foo') == gc1 - assert gradebook.find_task_cell('test2', 'p1', 'foo') == gc2 - - -def test_find_nonexistant_task_cell(gradebook): - with pytest.raises(MissingEntry): - gradebook.find_task_cell('test1', 'p1', 'foo') - - gradebook.add_assignment('foo') - with pytest.raises(MissingEntry): - gradebook.find_task_cell('test1', 'p1', 'foo') - - gradebook.add_notebook('p1', 'foo') - with pytest.raises(MissingEntry): - gradebook.find_task_cell('test1', 'p1', 'foo') - - -def test_update_or_create_task_cell(gradebook): - # first test creating it - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - gc1 = gradebook.update_or_create_task_cell('test1', 'p1', 'foo', max_score=2, cell_type='markdown') - assert gc1.max_score == 2 - assert gc1.cell_type == 'markdown' - assert gradebook.find_task_cell('test1', 'p1', 'foo') == gc1 - - # now test finding/updating it - gc2 = gradebook.update_or_create_task_cell('test1', 'p1', 'foo', max_score=3) - assert gc1 == gc2 - assert gc1.max_score == 3 - assert gc1.cell_type == 'markdown' - - -def test_find_graded_cell(gradebook): - # first test creating it - gradebook.add_assignment('foo') - gradebook.add_assignment('foo2') - gradebook.add_notebook('p1', 'foo') - gradebook.add_notebook('p2', 'foo2') - gc1 = gradebook.update_or_create_task_cell('test1', 'p1', 'foo', max_score=2, cell_type='markdown') - assert gc1.max_score == 2 - assert gc1.cell_type == 'markdown' - assert gradebook.find_graded_cell('test1', 'p1', 'foo') == gc1 - gc2 = gradebook.update_or_create_grade_cell('test2', 'p2', 'foo2', max_score=2, cell_type='code') - assert gc2.max_score == 2 - assert gc2.cell_type == 'code' - assert gradebook.find_grade_cell('test2', 'p2', 'foo2') == gc2 - assert gradebook.find_graded_cell('test2', 'p2', 'foo2') == gc2 - - -def test_grade_cell_maxscore(gradebook): - # first test creating it - gradebook.add_assignment('foo') - gradebook.add_notebook('p1', 'foo') - gc1 = gradebook.update_or_create_task_cell('test1', 'p1', 'foo', max_score=1000, cell_type='markdown') - gc1a = gradebook.update_or_create_task_cell('test1a', 'p1', 'foo', max_score=3000, cell_type='markdown') - gc2 = gradebook.update_or_create_grade_cell('test2', 'p1', 'foo', max_score=5, cell_type='code') - gc3 = gradebook.update_or_create_grade_cell('test3', 'p1', 'foo', max_score=7, cell_type='code') - gc4 = gradebook.update_or_create_grade_cell('test4', 'p1', 'foo', max_score=13, cell_type='code') - gc5 = gradebook.update_or_create_grade_cell('test5', 'p1', 'foo', max_score=10, cell_type='code') - # assert gc2.max_score == 5 - n1 = gradebook.find_notebook('p1', 'foo') - assert n1.max_score_gradecell == 35 - assert n1.max_score_taskcell == 4000 - assert n1.max_score == 4035 - - -def test_grades_include_taskcells(assignmentWithSubmissionWithMarks: Gradebook) -> None: - s = assignmentWithSubmissionWithMarks.find_submission('foo', 'hacker123') - for n in s.notebooks: - grades = n.grades - assert len(grades) == 6 - - -# next 4 same as in normal tests, but with an assignment with tasks -def test_find_grade(assignmentWithSubmissionWithMarks): - s = assignmentWithSubmissionWithMarks.find_submission('foo', 'hacker123') - for n in s.notebooks: - grades = n.grades - for g1 in grades: - g2 = assignmentWithSubmissionWithMarks.find_grade(g1.name, n.name, 'foo', 'hacker123') - assert g1 == g2 - - with pytest.raises(MissingEntry): - assignmentWithSubmissionWithMarks.find_grade('asdf', 'p1', 'foo', 'hacker123') - - -def test_find_grade_by_id(assignmentWithSubmissionWithMarks): - s = assignmentWithSubmissionWithMarks.find_submission('foo', 'hacker123') - for n in s.notebooks: - grades = n.grades - - for g1 in grades: - g2 = assignmentWithSubmissionWithMarks.find_grade_by_id(g1.id) - assert g1 == g2 - - with pytest.raises(MissingEntry): - assignmentWithSubmissionWithMarks.find_grade_by_id('12345') - - -def test_find_comment(assignmentWithSubmissionWithMarks: Gradebook) -> None: - s = assignmentWithSubmissionWithMarks.find_submission('foo', 'hacker123') - for n in s.notebooks: - comments = n.comments - - for c1 in comments: - c2 = assignmentWithSubmissionWithMarks.find_comment(c1.name, n.name, 'foo', 'hacker123') - assert c1 == c2 - - with pytest.raises(MissingEntry): - assignmentWithSubmissionWithMarks.find_comment('asdf', n.name, 'foo', 'hacker123') - - -def test_find_comment_by_id(assignmentWithSubmissionWithMarks): - s = assignmentWithSubmissionWithMarks.find_submission('foo', 'hacker123') - for n in s.notebooks: - comments = n.comments - - for c1 in comments: - c2 = assignmentWithSubmissionWithMarks.find_comment_by_id(c1.id) - assert c1 == c2 - - with pytest.raises(MissingEntry): - assignmentWithSubmissionWithMarks.find_comment_by_id('12345') - - -def test_average_assignment_score_empty(assignment): - assert assignment.average_assignment_score('foo') == 0.0 - assert assignment.average_assignment_code_score('foo') == 0.0 - assert assignment.average_assignment_written_score('foo') == 0.0 - assert assignment.average_assignment_task_score('foo') == 0.0 - - -def test_average_assignment_no_score(assignmentWithSubmissionNoMarks): - assert assignmentWithSubmissionNoMarks.average_assignment_score('foo') == 0.0 - assert assignmentWithSubmissionNoMarks.average_assignment_code_score('foo') == 0.0 - assert assignmentWithSubmissionNoMarks.average_assignment_written_score('foo') == 0.0 - assert assignmentWithSubmissionNoMarks.average_assignment_task_score('foo') == 0.0 - - -def test_average_assignment_with_score(assignmentWithSubmissionWithMarks): - assert assignmentWithSubmissionWithMarks.average_assignment_score('foo') == sum(assignmentWithSubmissionWithMarks.usedgrades) / 2.0 - assert assignmentWithSubmissionWithMarks.average_assignment_code_score('foo') == sum(assignmentWithSubmissionWithMarks.usedgrades_code) / 2.0 - assert assignmentWithSubmissionWithMarks.average_assignment_written_score('foo') == sum(assignmentWithSubmissionWithMarks.usedgrades_written) / 2.0 - assert assignmentWithSubmissionWithMarks.average_assignment_task_score('foo') == sum(assignmentWithSubmissionWithMarks.usedgrades_task) / 2.0 - - -def test_average_notebook_score_empty(assignment): - assert assignment.average_notebook_score('p1', 'foo') == 0.0 - assert assignment.average_notebook_code_score('p1', 'foo') == 0.0 - assert assignment.average_notebook_written_score('p1', 'foo') == 0.0 - assert assignment.average_notebook_task_score('p1', 'foo') == 0.0 - - -def test_average_notebook_no_score(assignmentWithSubmissionNoMarks): - assert assignmentWithSubmissionNoMarks.average_notebook_score('p1', 'foo') == 0.0 - assert assignmentWithSubmissionNoMarks.average_notebook_code_score('p1', 'foo') == 0.0 - assert assignmentWithSubmissionNoMarks.average_notebook_written_score('p1', 'foo') == 0.0 - assert assignmentWithSubmissionNoMarks.average_notebook_task_score('p1', 'foo') == 0.0 - - -def test_average_notebook_with_score(assignmentWithSubmissionWithMarks: Gradebook) -> None: - assert assignmentWithSubmissionWithMarks.average_notebook_score('p1', 'foo') == sum(assignmentWithSubmissionWithMarks.usedgrades) / 2.0 - assert assignmentWithSubmissionWithMarks.average_notebook_code_score('p1', 'foo') == sum(assignmentWithSubmissionWithMarks.usedgrades_code) / 2.0 - assert assignmentWithSubmissionWithMarks.average_notebook_written_score('p1', 'foo') == sum(assignmentWithSubmissionWithMarks.usedgrades_written) / 2.0 - assert assignmentWithSubmissionWithMarks.average_notebook_task_score('p1', 'foo') == sum(assignmentWithSubmissionWithMarks.usedgrades_task) / 2.0 - - -def test_student_dicts(assignmentWithSubmissionWithMarks): - assign = assignmentWithSubmissionWithMarks - students = assign.student_dicts() - a = sorted(students, key=lambda x: x["id"]) - b = sorted([x.to_dict() for x in assign.students], key=lambda x: x["id"]) - assert a == b - - -def test_notebook_max_score(assignmentManyStudents): - assign = assignmentManyStudents - notebook = assign.find_notebook("p1", "foo") - assert notebook.max_score == 44 - - -def test_notebook_max_score_multiple_notebooks(FiveNotebooks): - assign = FiveNotebooks - notebook = assign.find_notebook("n1", "a1") - assert notebook.max_score == 555 - - -def test_submission_max_score(assignmentManyStudents): - assign = assignmentManyStudents - s = assign.find_submission('foo', 's1') - assert s.max_score == 88 - for n in s.notebooks: - assert n.max_score == 44 - - -def test_submission_max_score_multiple_notebooks(FiveNotebooks): - assign = FiveNotebooks - s = assign.find_submission('a1', 's1') - assert s.max_score == 5 * 555 - for n in s.notebooks: - assert n.max_score == 555 - - -def test_notebook_submission_dicts_multiple_students(FiveStudents): - assign = FiveStudents - notebook = assign.find_notebook("n1", "a1") - submissions = assign.notebook_submission_dicts("n1", "a1") - a = sorted(submissions, key=lambda x: x["id"]) - b = sorted([x.to_dict() for x in notebook.submissions], key=lambda x: x["id"]) - assert a == b - - -def test_notebook_submission_dicts_multiple_notebooks(FiveNotebooks): - assign = FiveNotebooks - notebook = assign.find_notebook("n1", "a1") - submissions = assign.notebook_submission_dicts("n1", "a1") - a = sorted(submissions, key=lambda x: x["id"]) - b = sorted([x.to_dict() for x in notebook.submissions], key=lambda x: x["id"]) - assert a == b - - -def test_notebook_submission_dicts_multiple_assignments(FiveAssignments): - assign = FiveAssignments - notebook = assign.find_notebook("n1", "a1") - submissions = assign.notebook_submission_dicts("n1", "a1") - a = sorted(submissions, key=lambda x: x["id"]) - b = sorted([x.to_dict() for x in notebook.submissions], key=lambda x: x["id"]) - assert a == b - - -def test_notebook_submission_dicts(assignmentWithSubmissionWithMarks): - assign = assignmentWithSubmissionWithMarks - notebook = assign.find_notebook("p1", "foo") - submissions = assign.notebook_submission_dicts("p1", "foo") - a = sorted(submissions, key=lambda x: x["id"]) - b = sorted([x.to_dict() for x in notebook.submissions], key=lambda x: x["id"]) - assert a == b - - -def test_submission_dicts_multiple_students(FiveStudents): - assign = FiveStudents - a = sorted(assign.submission_dicts("a1"), key=lambda x: x["id"]) - b = sorted([x.to_dict() for x in assign.find_assignment("a1").submissions], key=lambda x: x["id"]) - assert a == b - - -def test_submission_dicts_multiple_notebooks(FiveNotebooks): - assign = FiveNotebooks - a = sorted(assign.submission_dicts("a1"), key=lambda x: x["id"]) - b = sorted([x.to_dict() for x in assign.find_assignment("a1").submissions], key=lambda x: x["id"]) - assert a == b diff --git a/nbgrader/tests/api/test_models.py b/nbgrader/tests/api/test_models.py deleted file mode 100644 index a38e0f53c..000000000 --- a/nbgrader/tests/api/test_models.py +++ /dev/null @@ -1,1278 +0,0 @@ -import datetime -import pytest -import json - -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker, scoped_session -from sqlalchemy.sql import and_ - -from ... import api - - -@pytest.fixture -def db(request): - engine = create_engine("sqlite:///:memory:") - db = scoped_session(sessionmaker(autoflush=True, bind=engine)) - api.Base.query = db.query_property() - api.Base.metadata.create_all(bind=engine) - - def fin(): - db.remove() - engine.dispose() - request.addfinalizer(fin) - - return db - - -@pytest.fixture -def submissions(db): - now = datetime.datetime.utcnow() - a = api.Assignment(name='foo', duedate=now) - n = api.Notebook(name='blah', assignment=a) - gc1 = api.GradeCell(name='foo', max_score=10, notebook=n, cell_type="markdown") - gc2 = api.GradeCell(name='bar', max_score=5, notebook=n, cell_type="code") - sc = api.SolutionCell(name='foo', notebook=n) - api.SourceCell( - name='foo', cell_type='markdown', notebook=n, - source='waoiefjwoweifjw', checksum='12345', locked=True) - api.SourceCell( - name='bar', cell_type='code', notebook=n, - source='afejfwejfwe', checksum='567890', locked=False) - db.add(a) - db.commit() - - s = api.Student(id="12345", first_name='Jane', last_name='Doe', email='janedoe@nowhere', lms_user_id='230') - sa = api.SubmittedAssignment(assignment=a, student=s) - sn = api.SubmittedNotebook(assignment=sa, notebook=n) - g1a = api.Grade(cell=gc1, notebook=sn) - g2a = api.Grade(cell=gc2, notebook=sn) - ca = api.Comment(cell=sc, notebook=sn) - - db.add(s) - db.commit() - - s = api.Student(id="6789", first_name='John', last_name='Doe', email='johndoe@nowhere', lms_user_id='230') - sa = api.SubmittedAssignment(assignment=a, student=s) - sn = api.SubmittedNotebook(assignment=sa, notebook=n) - g1b = api.Grade(cell=gc1, notebook=sn) - g2b = api.Grade(cell=gc2, notebook=sn) - cb = api.Comment(cell=sc, notebook=sn) - - db.add(s) - db.commit() - - return db, (g1a, g2a, g1b, g2b), (ca, cb) - - -def test_create_assignment(db): - now = datetime.datetime.utcnow() - a = api.Assignment(name='foo', duedate=now) - db.add(a) - db.commit() - - assert a.id - assert a.name == 'foo' - assert a.duedate == now - assert a.notebooks == [] - assert a.submissions == [] - - assert a.max_score == 0 - assert a.max_code_score == 0 - assert a.max_written_score == 0 - assert a.num_submissions == 0 - - assert repr(a) == "Assignment" - - -def test_create_notebook(db): - now = datetime.datetime.utcnow() - a = api.Assignment(name='foo', duedate=now) - n = api.Notebook(name='blah', assignment=a) - db.add(a) - db.commit() - - assert n.id - assert n.name == 'blah' - assert n.assignment == a - assert n.grade_cells == [] - assert n.solution_cells == [] - assert n.source_cells == [] - assert n.submissions == [] - assert a.notebooks == [n] - - assert n.max_score == 0 - assert n.max_code_score == 0 - assert n.max_written_score == 0 - - assert repr(n) == "Notebook" - - -def test_create_grade_cell(db): - now = datetime.datetime.utcnow() - a = api.Assignment(name='foo', duedate=now) - n = api.Notebook(name='blah', assignment=a) - g = api.GradeCell(name='foo', max_score=10, notebook=n, cell_type="code") - db.add(a) - db.commit() - - assert g.id - assert g.name == 'foo' - assert g.max_score == 10 - assert g.cell_type == "code" - assert g.assignment == a - assert g.notebook == n - assert g.grades == [] - assert n.grade_cells == [g] - - assert n.max_score == 10 - assert n.max_code_score == 10 - assert n.max_written_score == 0 - - assert repr(g) == "GradeCell" - - -def test_create_solution_cell(db): - now = datetime.datetime.utcnow() - a = api.Assignment(name='foo', duedate=now) - n = api.Notebook(name='blah', assignment=a) - s = api.SolutionCell(name='foo', notebook=n) - db.add(a) - db.commit() - - assert s.id - assert s.name == 'foo' - assert s.assignment == a - assert s.notebook == n - assert s.comments == [] - assert n.solution_cells == [s] - - assert repr(s) == "SolutionCell" - - -def test_create_source_cell(db): - now = datetime.datetime.utcnow() - a = api.Assignment(name='foo', duedate=now) - n = api.Notebook(name='blah', assignment=a) - s = api.SourceCell( - name='foo', notebook=n, source="hello", - cell_type="code", checksum="12345") - db.add(a) - db.commit() - - assert s.id - assert s.name == 'foo' - assert not s.locked - assert s.cell_type == "code" - assert s.source == "hello" - assert s.checksum == "12345" - assert s.assignment == a - assert s.notebook == n - assert n.source_cells == [s] - - assert repr(s) == "SourceCell" - - -def test_create_student(db): - s = api.Student(id="12345", first_name='Jane', last_name='Doe', email='janedoe@nowhere') - db.add(s) - db.commit() - - assert s.id == "12345" - assert s.first_name == 'Jane' - assert s.last_name == 'Doe' - assert s.email == 'janedoe@nowhere' - assert s.submissions == [] - - assert s.score == 0 - assert s.max_score == 0 - - assert repr(s) == "Student<12345>" - - -def test_create_submitted_assignment(db): - a = api.Assignment(name='foo') - s = api.Student(id="12345", first_name='Jane', last_name='Doe', email='janedoe@nowhere') - sa = api.SubmittedAssignment(assignment=a, student=s) - db.add(sa) - db.commit() - - assert sa.id - assert sa.assignment == a - assert sa.student == s - assert sa.notebooks == [] - assert s.submissions == [sa] - assert a.submissions == [sa] - - assert sa.score == 0 - assert sa.max_score == 0 - assert sa.code_score == 0 - assert sa.max_code_score == 0 - assert sa.written_score == 0 - assert sa.max_written_score == 0 - assert not sa.needs_manual_grade - - assert sa.duedate is None - assert sa.timestamp is None - assert sa.extension is None - assert sa.total_seconds_late == 0 - - d = sa.to_dict() - assert d['id'] == sa.id - assert d['name'] == 'foo' - assert d['student'] == '12345' - assert d['timestamp'] == None - assert d['score'] == 0 - assert d['max_score'] == 0 - assert d['code_score'] == 0 - assert d['max_code_score'] == 0 - assert d['written_score'] == 0 - assert d['max_written_score'] == 0 - assert not d['needs_manual_grade'] - - assert repr(sa) == "SubmittedAssignment" - - -def test_submission_timestamp_ontime(db): - duedate = datetime.datetime.utcnow() - timestamp = duedate - datetime.timedelta(days=2) - - a = api.Assignment(name='foo', duedate=duedate) - s = api.Student(id="12345", first_name='Jane', last_name='Doe', email='janedoe@nowhere') - sa = api.SubmittedAssignment(assignment=a, student=s, timestamp=timestamp) - db.add(sa) - db.commit() - - assert sa.duedate == duedate - assert sa.timestamp == timestamp - assert sa.extension is None - assert sa.total_seconds_late == 0 - - -def test_submission_timestamp_late(db): - duedate = datetime.datetime.utcnow() - timestamp = duedate + datetime.timedelta(days=2) - - a = api.Assignment(name='foo', duedate=duedate) - s = api.Student(id="12345", first_name='Jane', last_name='Doe', email='janedoe@nowhere') - sa = api.SubmittedAssignment(assignment=a, student=s, timestamp=timestamp) - db.add(sa) - db.commit() - - assert sa.duedate == duedate - assert sa.timestamp == timestamp - assert sa.extension is None - assert sa.total_seconds_late == 172800 - - -def test_submission_timestamp_with_extension(db): - duedate = datetime.datetime.utcnow() - timestamp = duedate + datetime.timedelta(days=2) - extension = datetime.timedelta(days=3) - - a = api.Assignment(name='foo', duedate=duedate) - s = api.Student(id="12345", first_name='Jane', last_name='Doe', email='janedoe@nowhere') - sa = api.SubmittedAssignment(assignment=a, student=s, timestamp=timestamp, extension=extension) - db.add(sa) - db.commit() - - assert sa.duedate == (duedate + extension) - assert sa.timestamp == timestamp - assert sa.extension == extension - assert sa.total_seconds_late == 0 - - -def test_submission_timestamp_late_with_extension(db): - duedate = datetime.datetime.utcnow() - timestamp = duedate + datetime.timedelta(days=5) - extension = datetime.timedelta(days=3) - - a = api.Assignment(name='foo', duedate=duedate) - s = api.Student(id="12345", first_name='Jane', last_name='Doe', email='janedoe@nowhere') - sa = api.SubmittedAssignment(assignment=a, student=s, timestamp=timestamp, extension=extension) - db.add(sa) - db.commit() - - assert sa.duedate == (duedate + extension) - assert sa.timestamp == timestamp - assert sa.extension == extension - assert sa.total_seconds_late == 172800 - - -def test_create_submitted_notebook(db): - now = datetime.datetime.utcnow() - a = api.Assignment(name='foo', duedate=now) - n1 = api.Notebook(name='blah', assignment=a) - n2 = api.Notebook(name='blah2', assignment=a) - s = api.Student(id="12345", first_name='Jane', last_name='Doe', email='janedoe@nowhere') - sa = api.SubmittedAssignment(assignment=a, student=s) - sn1 = api.SubmittedNotebook(assignment=sa, notebook=n1, late_submission_penalty=5) - sn2 = api.SubmittedNotebook(assignment=sa, notebook=n2, late_submission_penalty=1) - db.add(sn1) - db.add(sn2) - db.commit() - - assert sn1.id - assert sn1.notebook == n1 - assert sn1.assignment == sa - assert sn1.grades == [] - assert sn1.comments == [] - assert sn1.student == s - assert sa.notebooks == [sn1, sn2] - assert n1.submissions == [sn1] - - assert sn1.score == 0 - assert sn1.max_score == 0 - assert sn1.code_score == 0 - assert sn1.max_code_score == 0 - assert sn1.written_score == 0 - assert sn1.max_written_score == 0 - assert sn1.late_submission_penalty == 5 - assert sn2.late_submission_penalty == 1 - assert sa.late_submission_penalty == 6 - assert not sn1.needs_manual_grade - - assert repr(sn1) == "SubmittedNotebook" - - -def test_create_code_grade(db): - now = datetime.datetime.utcnow() - a = api.Assignment(name='foo', duedate=now) - n = api.Notebook(name='blah', assignment=a) - gc = api.GradeCell(name='foo', max_score=10, notebook=n, cell_type="code") - s = api.Student(id="12345", first_name='Jane', last_name='Doe', email='janedoe@nowhere') - sa = api.SubmittedAssignment(assignment=a, student=s) - sn = api.SubmittedNotebook(assignment=sa, notebook=n) - g = api.Grade(cell=gc, notebook=sn, auto_score=5) - db.add(g) - db.commit() - - assert g.id - assert g.cell == gc - assert g.notebook == sn - assert g.auto_score == 5 - assert g.manual_score is None - assert g.assignment == sa - assert g.student == s - assert g.max_score == 10 - - assert g.needs_manual_grade - assert sn.needs_manual_grade - assert sa.needs_manual_grade - - assert g.score == 5 - assert sn.score == 5 - assert sn.code_score == 5 - assert sn.written_score == 0 - assert sa.score == 5 - assert sa.code_score == 5 - assert sa.written_score == 0 - assert s.score == 5 - - g.manual_score = 7.5 - db.commit() - - assert g.needs_manual_grade - assert sn.needs_manual_grade - assert sa.needs_manual_grade - - assert g.score == 7.5 - assert sn.score == 7.5 - assert sn.code_score == 7.5 - assert sn.written_score == 0 - assert sa.score == 7.5 - assert sa.code_score == 7.5 - assert sa.written_score == 0 - assert s.score == 7.5 - - g.needs_manual_grade = False - db.commit() - - assert not g.needs_manual_grade - assert not sn.needs_manual_grade - assert not sa.needs_manual_grade - - assert repr(g) == "Grade" - - -def test_create_written_grade(db): - now = datetime.datetime.utcnow() - a = api.Assignment(name='foo', duedate=now) - n = api.Notebook(name='blah', assignment=a) - gc = api.GradeCell(name='foo', max_score=10, notebook=n, cell_type="markdown") - s = api.Student(id="12345", first_name='Jane', last_name='Doe', email='janedoe@nowhere') - sa = api.SubmittedAssignment(assignment=a, student=s) - sn = api.SubmittedNotebook(assignment=sa, notebook=n) - g = api.Grade(cell=gc, notebook=sn) - db.add(g) - db.commit() - - assert g.id - assert g.cell == gc - assert g.notebook == sn - assert g.auto_score is None - assert g.manual_score is None - assert g.assignment == sa - assert g.student == s - assert g.max_score == 10 - - assert g.needs_manual_grade - assert sn.needs_manual_grade - assert sa.needs_manual_grade - - assert g.score == 0 - assert sn.score == 0 - assert sn.code_score == 0 - assert sn.written_score == 0 - assert sa.score == 0 - assert sa.code_score == 0 - assert sa.written_score == 0 - assert s.score == 0 - - g.manual_score = 7.5 - db.commit() - - assert g.needs_manual_grade - assert sn.needs_manual_grade - assert sa.needs_manual_grade - - assert g.score == 7.5 - assert sn.score == 7.5 - assert sn.code_score == 0 - assert sn.written_score == 7.5 - assert sa.score == 7.5 - assert sa.code_score == 0 - assert sa.written_score == 7.5 - assert s.score == 7.5 - - g.needs_manual_grade = False - db.commit() - - assert not g.needs_manual_grade - assert not sn.needs_manual_grade - assert not sa.needs_manual_grade - - assert repr(g) == "Grade" - - -def test_create_comment(db): - now = datetime.datetime.utcnow() - a = api.Assignment(name='foo', duedate=now) - n = api.Notebook(name='blah', assignment=a) - sc = api.SolutionCell(name='foo', notebook=n) - s = api.Student(id="12345", first_name='Jane', last_name='Doe', email='janedoe@nowhere') - sa = api.SubmittedAssignment(assignment=a, student=s) - sn = api.SubmittedNotebook(assignment=sa, notebook=n) - c = api.Comment(cell=sc, notebook=sn, auto_comment="something") - db.add(c) - db.commit() - - assert c.id - assert c.cell == sc - assert c.notebook == sn - assert c.comment == "something" - assert c.assignment == sa - assert c.student == s - - assert repr(c) == "Comment" - - -def test_query_needs_manual_grade_ungraded(submissions): - db = submissions[0] - - # do all the cells need grading? - a = db.query(api.Grade)\ - .filter(api.Grade.needs_manual_grade)\ - .order_by(api.Grade.id)\ - .all() - b = db.query(api.Grade)\ - .order_by(api.Grade.id)\ - .all() - assert a == b - - # do all the submitted notebooks need grading? - a = db.query(api.SubmittedNotebook)\ - .filter(api.SubmittedNotebook.needs_manual_grade)\ - .order_by(api.SubmittedNotebook.id)\ - .all() - b = db.query(api.SubmittedNotebook)\ - .order_by(api.SubmittedNotebook.id)\ - .all() - assert a == b - - # do all the notebooks need grading? - a = db.query(api.Notebook)\ - .filter(api.Notebook.needs_manual_grade)\ - .order_by(api.Notebook.id)\ - .all() - b = db.query(api.Notebook)\ - .order_by(api.Notebook.id)\ - .all() - assert a == b - - # do all the assignments need grading? - a = db.query(api.SubmittedAssignment)\ - .join(api.SubmittedNotebook).join(api.Grade)\ - .filter(api.SubmittedNotebook.needs_manual_grade)\ - .order_by(api.SubmittedAssignment.id)\ - .all() - b = db.query(api.SubmittedAssignment)\ - .order_by(api.SubmittedAssignment.id)\ - .all() - assert a == b - - -def test_query_needs_manual_grade_autograded(submissions): - db, grades, _ = submissions - - for grade in grades: - grade.auto_score = grade.max_score - db.commit() - - # do all the cells need grading? - a = db.query(api.Grade)\ - .filter(api.Grade.needs_manual_grade)\ - .order_by(api.Grade.id)\ - .all() - b = db.query(api.Grade)\ - .order_by(api.Grade.id)\ - .all() - assert a == b - - # do all the submitted notebooks need grading? - a = db.query(api.SubmittedNotebook)\ - .filter(api.SubmittedNotebook.needs_manual_grade)\ - .order_by(api.SubmittedNotebook.id)\ - .all() - b = db.query(api.SubmittedNotebook)\ - .order_by(api.SubmittedNotebook.id)\ - .all() - assert a == b - - # do all the notebooks need grading? - a = db.query(api.Notebook)\ - .filter(api.Notebook.needs_manual_grade)\ - .order_by(api.Notebook.id)\ - .all() - b = db.query(api.Notebook)\ - .order_by(api.Notebook.id)\ - .all() - assert a == b - - # do all the assignments need grading? - a = db.query(api.SubmittedAssignment)\ - .join(api.SubmittedNotebook).join(api.Grade)\ - .filter(api.SubmittedNotebook.needs_manual_grade)\ - .order_by(api.SubmittedAssignment.id)\ - .all() - b = db.query(api.SubmittedAssignment)\ - .order_by(api.SubmittedAssignment.id)\ - .all() - assert a == b - - for grade in grades: - grade.needs_manual_grade = False - db.commit() - - # do none of the cells need grading? - assert [] == db.query(api.Grade)\ - .filter(api.Grade.needs_manual_grade)\ - .all() - - # do none of the submitted notebooks need grading? - assert [] == db.query(api.SubmittedNotebook)\ - .filter(api.SubmittedNotebook.needs_manual_grade)\ - .all() - - # do none of the notebooks need grading? - assert [] == db.query(api.Notebook)\ - .filter(api.Notebook.needs_manual_grade)\ - .all() - - # do none of the assignments need grading? - assert [] == db.query(api.SubmittedAssignment)\ - .join(api.SubmittedNotebook).join(api.Grade)\ - .filter(api.SubmittedNotebook.needs_manual_grade)\ - .all() - - -def test_query_needs_manual_grade_manualgraded(submissions): - db, grades, _ = submissions - - for grade in grades: - grade.auto_score = None - grade.manual_score = grade.max_score / 2.0 - db.commit() - - # do all the cells need grading? - a = db.query(api.Grade)\ - .filter(api.Grade.needs_manual_grade)\ - .order_by(api.Grade.id)\ - .all() - b = db.query(api.Grade)\ - .order_by(api.Grade.id)\ - .all() - assert a == b - - # do all the submitted notebooks need grading? - a = db.query(api.SubmittedNotebook)\ - .filter(api.SubmittedNotebook.needs_manual_grade)\ - .order_by(api.SubmittedNotebook.id)\ - .all() - b = db.query(api.SubmittedNotebook)\ - .order_by(api.SubmittedNotebook.id)\ - .all() - assert a == b - - # do all the notebooks need grading? - a = db.query(api.Notebook)\ - .filter(api.Notebook.needs_manual_grade)\ - .order_by(api.Notebook.id)\ - .all() - b = db.query(api.Notebook)\ - .order_by(api.Notebook.id)\ - .all() - assert a == b - - # do all the assignments need grading? - a = db.query(api.SubmittedAssignment)\ - .join(api.SubmittedNotebook).join(api.Grade)\ - .filter(api.SubmittedNotebook.needs_manual_grade)\ - .order_by(api.SubmittedAssignment.id)\ - .all() - b = db.query(api.SubmittedAssignment)\ - .order_by(api.SubmittedAssignment.id)\ - .all() - assert a == b - - for grade in grades: - grade.needs_manual_grade = False - db.commit() - - # do none of the cells need grading? - assert [] == db.query(api.Grade)\ - .filter(api.Grade.needs_manual_grade)\ - .all() - - # do none of the submitted notebooks need grading? - assert [] == db.query(api.SubmittedNotebook)\ - .filter(api.SubmittedNotebook.needs_manual_grade)\ - .all() - - # do none of the notebooks need grading? - assert [] == db.query(api.Notebook)\ - .filter(api.Notebook.needs_manual_grade)\ - .all() - - # do none of the assignments need grading? - assert [] == db.query(api.SubmittedAssignment)\ - .join(api.SubmittedNotebook).join(api.Grade)\ - .filter(api.SubmittedNotebook.needs_manual_grade)\ - .all() - - -def test_query_max_score(submissions): - db = submissions[0] - - assert [5, 10] == sorted([x[1] for x in db.query( - api.GradeCell.id, api.GradeCell.max_score).group_by(api.GradeCell.id).all()]) - assert [5, 5, 10, 10] == sorted([x[1] for x in db.query( - api.Grade.id, api.Grade.max_score).group_by(api.Grade.id).all()]) - assert [15] == sorted([x[1] for x in db.query( - api.Notebook.id, api.Notebook.max_score).group_by(api.Notebook.id).all()]) - assert [15, 15] == sorted([x[1] for x in db.query( - api.SubmittedNotebook.id, api.SubmittedNotebook.max_score).group_by(api.SubmittedNotebook.id).all()]) - assert [15] == sorted([x[1] for x in db.query( - api.Assignment.id, api.Assignment.max_score).group_by(api.Assignment.id).all()]) - assert [15, 15] == sorted([x[1] for x in db.query( - api.SubmittedAssignment.id, api.SubmittedAssignment.max_score).group_by(api.SubmittedAssignment.id).all()]) - assert [15, 15] == sorted([x[1] for x in db.query - (api.Student.id, api.Student.max_score).group_by(api.Student.id).all()]) - - -def test_query_score_ungraded(submissions): - db = submissions[0] - - assert [x[0] for x in db.query(api.Grade.score).all()] == [0.0, 0.0, 0.0, 0.0] - assert [x[1] for x in db.query(api.SubmittedNotebook.id, api.SubmittedNotebook.score).all()] == [0.0, 0.0] - assert [x[1] for x in db.query(api.SubmittedAssignment.id, api.SubmittedAssignment.score).all()] == [0.0, 0.0] - assert [x[1] for x in db.query(api.Student.id, api.Student.score).all()] == [0.0, 0.0] - - -def test_query_comment_unchanged(submissions): - db = submissions[0] - - assert [x[0] for x in db.query(api.Comment.comment).all()] == [None, None] - - -def test_query_score_autograded(submissions): - db, grades, _ = submissions - - grades[0].auto_score = 10 - grades[1].auto_score = 0 - grades[2].auto_score = 5 - grades[3].auto_score = 2.5 - db.commit() - - assert sorted(x[0] for x in db.query(api.Grade.score).all()) == [0, 2.5, 5, 10] - assert sorted(x[1] for x in db.query(api.SubmittedNotebook.id, api.SubmittedNotebook.score).all()) == [7.5, 10] - assert sorted(x[1] for x in db.query(api.SubmittedAssignment.id, api.SubmittedAssignment.score).all()) == [7.5, 10] - assert sorted(x[1] for x in db.query(api.Student.id, api.Student.score).all()) == [7.5, 10] - - -def test_query_auto_comment(submissions): - db, _, comments = submissions - - comments[0].auto_comment = "foo" - comments[1].auto_comment = "bar" - db.commit() - - assert sorted(x[0] for x in db.query(api.Comment.comment).all()) == ["bar", "foo"] - - -def test_query_score_manualgraded(submissions): - db, grades, _ = submissions - - grades[0].auto_score = 10 - grades[1].auto_score = 0 - grades[2].auto_score = 5 - grades[3].auto_score = 2.5 - grades[0].manual_score = 4 - grades[1].manual_score = 1.5 - grades[2].manual_score = 9 - grades[3].manual_score = 3 - db.commit() - - assert sorted(x[0] for x in db.query(api.Grade.score).all()) == [1.5, 3, 4, 9] - assert sorted(x[1] for x in db.query(api.SubmittedNotebook.id, api.SubmittedNotebook.score).all()) == [5.5, 12] - assert sorted(x[1] for x in db.query(api.SubmittedAssignment.id, api.SubmittedAssignment.score).all()) == [5.5, 12] - assert sorted(x[1] for x in db.query(api.Student.id, api.Student.score).all()) == [5.5, 12] - - -def test_query_manual_comment(submissions): - db, _, comments = submissions - - comments[0].auto_comment = "foo" - comments[1].auto_comment = "bar" - comments[0].manual_comment = "baz" - comments[1].manual_comment = "quux" - db.commit() - - assert sorted(x[0] for x in db.query(api.Comment.comment).all()) == ["baz", "quux"] - - -def test_query_max_written_score(submissions): - db = submissions[0] - - assert [10] == sorted([x[1] for x in db.query(api.Notebook.id, api.Notebook.max_written_score).all()]) - assert [10, 10] == sorted([x[1] for x in db.query(api.SubmittedNotebook.id, api.SubmittedNotebook.max_written_score).all()]) - assert [10] == sorted([x[1] for x in db.query(api.Assignment.id, api.Assignment.max_written_score).all()]) - assert [10, 10] == sorted([x[1] for x in db.query(api.SubmittedAssignment.id, api.SubmittedAssignment.max_written_score).all()]) - - -def test_query_written_score_ungraded(submissions): - db = submissions[0] - - assert [0.0, 0.0] == [x[1] for x in db.query(api.SubmittedNotebook.id, api.SubmittedNotebook.written_score).all()] - assert [0.0, 0.0] == [x[1] for x in db.query(api.SubmittedAssignment.id, api.SubmittedAssignment.written_score).all()] - - -def test_query_written_score_autograded(submissions): - db, grades, _ = submissions - - grades[0].auto_score = 10 - grades[1].auto_score = 0 - grades[2].auto_score = 5 - grades[3].auto_score = 2.5 - db.commit() - - assert [5, 10] == sorted(x[1] for x in db.query(api.SubmittedNotebook.id, api.SubmittedNotebook.written_score).all()) - assert [5, 10] == sorted(x[1] for x in db.query(api.SubmittedAssignment.id, api.SubmittedAssignment.written_score).all()) - - -def test_query_written_score_manualgraded(submissions): - db, grades, _ = submissions - - grades[0].auto_score = 10 - grades[1].auto_score = 0 - grades[2].auto_score = 5 - grades[3].auto_score = 2.5 - grades[0].manual_score = 4 - grades[1].manual_score = 1.5 - grades[2].manual_score = 9 - grades[3].manual_score = 3 - db.commit() - - assert [4, 9] == sorted(x[1] for x in db.query(api.SubmittedNotebook.id, api.SubmittedNotebook.written_score).all()) - assert [4, 9] == sorted(x[1] for x in db.query(api.SubmittedAssignment.id, api.SubmittedAssignment.written_score).all()) - - -def test_query_max_code_score(submissions): - db = submissions[0] - - assert [5] == sorted([x[1] for x in db.query(api.Notebook.id, api.Notebook.max_code_score).all()]) - assert [5, 5] == sorted([x[1] for x in db.query(api.SubmittedNotebook.id, api.SubmittedNotebook.max_code_score).all()]) - assert [5] == sorted([x[1] for x in db.query(api.Assignment.id, api.Assignment.max_code_score).all()]) - assert [5, 5] == sorted([x[1] for x in db.query(api.SubmittedAssignment.id, api.SubmittedAssignment.max_code_score).all()]) - - -def test_query_code_score_ungraded(submissions): - db = submissions[0] - - assert [0.0, 0.0] == [x[1] for x in db.query(api.SubmittedNotebook.id, api.SubmittedNotebook.code_score).all()] - assert [0.0, 0.0] == [x[1] for x in db.query(api.SubmittedAssignment.id, api.SubmittedAssignment.code_score).all()] - - -def test_query_code_score_autograded(submissions): - db, grades, _ = submissions - - grades[0].auto_score = 10 - grades[1].auto_score = 0 - grades[2].auto_score = 5 - grades[3].auto_score = 2.5 - db.commit() - - assert [0, 2.5] == sorted(x[1] for x in db.query(api.SubmittedNotebook.id, api.SubmittedNotebook.code_score).all()) - assert [0, 2.5] == sorted(x[1] for x in db.query(api.SubmittedAssignment.id, api.SubmittedAssignment.code_score).all()) - - -def test_query_code_score_manualgraded(submissions): - db, grades, _ = submissions - - grades[0].auto_score = 10 - grades[1].auto_score = 0 - grades[2].auto_score = 5 - grades[3].auto_score = 2.5 - grades[0].manual_score = 4 - grades[1].manual_score = 1.5 - grades[2].manual_score = 9 - grades[3].manual_score = 3 - db.commit() - - assert [1.5, 3] == sorted(x[1] for x in db.query(api.SubmittedNotebook.id, api.SubmittedNotebook.code_score).all()) - assert [1.5, 3] == sorted(x[1] for x in db.query(api.SubmittedAssignment.id, api.SubmittedAssignment.code_score).all()) - - -def test_query_auto_score_extra_credit(submissions): - db, grades, _ = submissions - - grades[0].auto_score = 10 - grades[1].auto_score = 0 - grades[2].auto_score = 5 - grades[3].auto_score = 2.5 - - grades[0].extra_credit = 0.5 - grades[1].extra_credit = 0 - grades[2].extra_credit = 2.3 - grades[3].extra_credit = 1.1 - db.commit() - - assert sorted(x[0] for x in db.query(api.Grade.score).all()) == [0, 3.6, 7.3, 10.5] - assert sorted(x[1] for x in db.query(api.SubmittedNotebook.id, api.SubmittedNotebook.score).all()) == [10.5, 10.9] - assert sorted(x[1] for x in db.query(api.SubmittedAssignment.id, api.SubmittedAssignment.score).all()) == [10.5, 10.9] - assert sorted(x[1] for x in db.query(api.Student.id, api.Student.score).all()) == [10.5, 10.9] - - -def test_query_manual_score_extra_credit(submissions): - db, grades, _ = submissions - - grades[0].auto_score = 10 - grades[1].auto_score = 0 - grades[2].auto_score = 5 - grades[3].auto_score = 2.5 - - grades[0].manual_score = 4 - grades[1].manual_score = 1.5 - grades[2].manual_score = 9 - grades[3].manual_score = 3 - - grades[0].extra_credit = 0.5 - grades[1].extra_credit = 0 - grades[2].extra_credit = 2.3 - grades[3].extra_credit = 1.1 - db.commit() - - assert sorted(x[0] for x in db.query(api.Grade.score).all()) == [1.5, 4.1, 4.5, 11.3] - assert sorted(x[1] for x in db.query(api.SubmittedNotebook.id, api.SubmittedNotebook.score).all()) == [6, 15.4] - assert sorted(x[1] for x in db.query(api.SubmittedAssignment.id, api.SubmittedAssignment.score).all()) == [6, 15.4] - assert sorted(x[1] for x in db.query(api.Student.id, api.Student.score).all()) == [6, 15.4] - - -def test_query_num_submissions(submissions): - db = submissions[0] - - assert [2] == [x[0] for x in db.query(api.Assignment.num_submissions).all()] - assert [2] == [x[0] for x in db.query(api.Notebook.num_submissions).all()] - - -def test_student_max_score(db): - now = datetime.datetime.utcnow() - a = api.Assignment(name='foo', duedate=now) - n = api.Notebook(name='blah', assignment=a) - api.GradeCell(name='foo', max_score=10, notebook=n, cell_type="markdown") - api.GradeCell(name='bar', max_score=5, notebook=n, cell_type="code") - db.add(a) - db.commit() - - s = api.Student(id="12345", first_name='Jane', last_name='Doe', email='janedoe@nowhere') - db.add(s) - db.commit() - - assert s.max_score == 15 - - -def test_query_grade_cell_types(submissions): - db = submissions[0] - - a = db.query(api.Grade)\ - .filter(api.Grade.cell_type == "code")\ - .order_by(api.Grade.id)\ - .all() - b = db.query(api.Grade)\ - .join(api.GradeCell)\ - .filter(api.GradeCell.cell_type == "code")\ - .order_by(api.Grade.id)\ - .all() - assert a == b - - a = db.query(api.Grade)\ - .filter(api.Grade.cell_type == "markdown")\ - .order_by(api.Grade.id)\ - .all() - b = db.query(api.Grade)\ - .join(api.GradeCell)\ - .filter(api.GradeCell.cell_type == "markdown")\ - .order_by(api.Grade.id)\ - .all() - assert a == b - - -def test_query_failed_tests_failed(submissions): - db, grades, _ = submissions - - for grade in grades: - if grade.cell.cell_type == "code": - grade.auto_score = 0 - db.commit() - - # have all the cells failed? - a = db.query(api.Grade)\ - .filter(api.Grade.failed_tests)\ - .order_by(api.Grade.id)\ - .all() - b = db.query(api.Grade)\ - .filter(api.Grade.cell_type == "code")\ - .order_by(api.Grade.id)\ - .all() - assert a == b - - # have all the notebooks failed? - a = db.query(api.SubmittedNotebook)\ - .filter(api.SubmittedNotebook.failed_tests)\ - .order_by(api.SubmittedNotebook.id)\ - .all() - b = db.query(api.SubmittedNotebook)\ - .order_by(api.SubmittedNotebook.id)\ - .all() - - -def test_query_failed_tests_ok(submissions): - db, all_grades, _ = submissions - - for grade in all_grades: - if grade.cell.cell_type == "code": - grade.auto_score = grade.max_score - db.commit() - - # are all the grades ok? - assert [] == db.query(api.Grade)\ - .filter(api.Grade.failed_tests)\ - .all() - - # are all the notebooks ok? - assert [] == db.query(api.SubmittedNotebook)\ - .filter(api.SubmittedNotebook.failed_tests)\ - .all() - - -def test_assignment_to_dict(submissions): - db = submissions[0] - - a = db.query(api.Assignment).one() - ad = a.to_dict() - - assert set(ad.keys()) == { - 'id', 'name', 'duedate', 'num_submissions', 'max_score', - 'max_code_score', 'max_written_score', 'max_task_score'} - - assert ad['id'] == a.id - assert ad['name'] == "foo" - assert ad['duedate'] == a.duedate.isoformat() - assert ad['num_submissions'] == 2 - assert ad['max_score'] == 15 - assert ad['max_code_score'] == 5 - assert ad['max_written_score'] == 10 - - # make sure it can be JSONified - json.dumps(ad) - - -def test_notebook_to_dict(submissions): - db = submissions[0] - - a = db.query(api.Assignment).one() - n, = a.notebooks - nd = n.to_dict() - - assert set(nd.keys()) == { - 'id', 'name', 'num_submissions', 'max_score', 'max_code_score', - 'max_written_score', 'needs_manual_grade', 'max_task_score'} - - assert nd['id'] == n.id - assert nd['name'] == 'blah' - assert nd['num_submissions'] == 2 - assert nd['max_score'] == 15 - assert nd['max_code_score'] == 5 - assert nd['max_written_score'] == 10 - assert nd['needs_manual_grade'] - - # make sure it can be JSONified - json.dumps(nd) - - -def test_gradecell_to_dict(submissions): - db = submissions[0] - - gc1 = db.query(api.GradeCell).filter(api.GradeCell.name == 'foo').one() - gc2 = db.query(api.GradeCell).filter(api.GradeCell.name == 'bar').one() - - gc1d = gc1.to_dict() - gc2d = gc2.to_dict() - - assert set(gc1d.keys()) == set(gc2d.keys()) - assert set(gc1d.keys()) == { - 'id', 'name', 'max_score', 'cell_type', 'notebook', 'assignment'} - - assert gc1d['id'] == gc1.id - assert gc1d['name'] == 'foo' - assert gc1d['max_score'] == 10 - assert gc1d['cell_type'] == 'markdown' - assert gc1d['notebook'] == 'blah' - assert gc1d['assignment'] == 'foo' - - assert gc2d['id'] == gc2.id - assert gc2d['name'] == 'bar' - assert gc2d['max_score'] == 5 - assert gc2d['cell_type'] == 'code' - assert gc2d['notebook'] == 'blah' - assert gc2d['assignment'] == 'foo' - - # make sure it can be JSONified - json.dumps(gc1d) - json.dumps(gc2d) - - -def test_solutioncell_to_dict(submissions): - db = submissions[0] - - sc = db.query(api.SolutionCell).one() - scd = sc.to_dict() - - assert set(scd.keys()) == {'id', 'name', 'notebook', 'assignment'} - - assert scd['id'] == sc.id - assert scd['name'] == 'foo' - assert scd['notebook'] == 'blah' - assert scd['assignment'] == 'foo' - - # make sure it can be JSONified - json.dumps(scd) - - -def test_sourcecell_to_dict(submissions): - db = submissions[0] - - sc1 = db.query(api.SourceCell).filter(api.SourceCell.name == 'foo').one() - sc2 = db.query(api.SourceCell).filter(api.SourceCell.name == 'bar').one() - - sc1d = sc1.to_dict() - sc2d = sc2.to_dict() - - assert set(sc1d.keys()) == set(sc2d.keys()) - assert set(sc1d.keys()) == { - 'id', 'name', 'cell_type', 'source', 'checksum', 'locked', - 'notebook', 'assignment'} - - assert sc1d['id'] == sc1.id - assert sc1d['name'] == 'foo' - assert sc1d['cell_type'] == 'markdown' - assert sc1d['source'] == 'waoiefjwoweifjw' - assert sc1d['checksum'] == '12345' - assert sc1d['notebook'] == 'blah' - assert sc1d['assignment'] == 'foo' - assert sc1d['locked'] - - assert sc2d['id'] == sc2.id - assert sc2d['name'] == 'bar' - assert sc2d['cell_type'] == 'code' - assert sc2d['source'] == 'afejfwejfwe' - assert sc2d['checksum'] == '567890' - assert sc2d['notebook'] == 'blah' - assert sc2d['assignment'] == 'foo' - assert not sc2d['locked'] - - # make sure it can be JSONified - json.dumps(sc1d) - json.dumps(sc2d) - - -def test_student_to_dict(submissions): - db = submissions[0] - - s1 = db.query(api.Student).filter(api.Student.id == '12345').one() - s2 = db.query(api.Student).filter(api.Student.id == '6789').one() - - s1d = s1.to_dict() - s2d = s2.to_dict() - - assert set(s1d.keys()) == set(s2d.keys()) - assert set(s1d.keys()) == { - 'id', 'first_name', 'last_name', 'email', 'score', 'max_score', 'lms_user_id'} - - assert s1d['id'] == '12345' - assert s1d['first_name'] == 'Jane' - assert s1d['last_name'] == 'Doe' - assert s1d['email'] == 'janedoe@nowhere' - assert s1d['score'] == 0 - assert s1d['max_score'] == 15 - assert s1d['lms_user_id'] == '230' - - assert s2d['id'] == '6789' - assert s2d['first_name'] == 'John' - assert s2d['last_name'] == 'Doe' - assert s2d['email'] == 'johndoe@nowhere' - assert s2d['score'] == 0 - assert s2d['max_score'] == 15 - assert s2d['lms_user_id'] == '230' - - # make sure it can be JSONified - json.dumps(s1d) - json.dumps(s2d) - - -def test_submittedassignment_to_dict(submissions): - db = submissions[0] - - sa = db.query(api.SubmittedAssignment)\ - .join(api.Student)\ - .filter(api.Student.id == '12345')\ - .one() - - sad = sa.to_dict() - - assert set(sad.keys()) == { - 'id', 'name', 'student', 'timestamp', 'score', 'max_score', 'code_score', - 'max_code_score', 'written_score', 'max_written_score', - 'task_score', 'max_task_score', - 'needs_manual_grade', 'last_name', 'first_name'} - - assert sad['id'] == sa.id - assert sad['name'] == 'foo' - assert sad['student'] == '12345' - assert sad['last_name'] == 'Doe' - assert sad['first_name'] == 'Jane' - assert sad['timestamp'] is None - assert sad['score'] == 0 - assert sad['max_score'] == 15 - assert sad['code_score'] == 0 - assert sad['max_code_score'] == 5 - assert sad['written_score'] == 0 - assert sad['max_written_score'] == 10 - assert sad['needs_manual_grade'] - - # make sure it can be JSONified - json.dumps(sad) - - -def test_submittednotebook_to_dict(submissions): - db = submissions[0] - - sn = db.query(api.SubmittedNotebook)\ - .join(api.Notebook).join(api.SubmittedAssignment).join(api.Student)\ - .filter(and_( - api.Student.id == '12345', - api.Notebook.name == 'blah'))\ - .one() - - snd = sn.to_dict() - - assert set(snd.keys()) == { - 'id', 'name', 'student', 'last_name', 'first_name', - 'score', 'max_score', 'code_score', - 'max_code_score', 'written_score', 'max_written_score', - 'task_score', 'max_task_score', - 'needs_manual_grade', 'failed_tests', 'flagged'} - - assert snd['id'] == sn.id - assert snd['name'] == 'blah' - assert snd['student'] == '12345' - assert snd['last_name'] == 'Doe' - assert snd['first_name'] == 'Jane' - assert snd['score'] == 0 - assert snd['max_score'] == 15 - assert snd['code_score'] == 0 - assert snd['max_code_score'] == 5 - assert snd['written_score'] == 0 - assert snd['max_written_score'] == 10 - assert snd['needs_manual_grade'] - assert not snd['failed_tests'] - assert not snd['flagged'] - - # make sure it can be JSONified - json.dumps(snd) - - -def test_grade_to_dict(submissions): - _, grades, _ = submissions - - for g in grades: - gd = g.to_dict() - assert set(gd.keys()) == { - 'id', 'name', 'notebook', 'assignment', 'student', 'auto_score', - 'manual_score', 'max_score', 'needs_manual_grade', 'failed_tests', - 'cell_type', 'extra_credit'} - - assert gd['id'] == g.id - assert gd['name'] == g.name - assert gd['notebook'] == 'blah' - assert gd['assignment'] == 'foo' - assert gd['student'] == g.student.id - assert gd['auto_score'] is None - assert gd['manual_score'] is None - assert gd['extra_credit'] is None - assert gd['needs_manual_grade'] - assert not gd['failed_tests'] - assert gd['cell_type'] == g.cell_type - - # make sure it can be JSONified - json.dumps(gd) - - -def test_comment_to_dict(submissions): - _, _, comments = submissions - - for c in comments: - cd = c.to_dict() - assert set(cd.keys()) == { - 'id', 'name', 'notebook', 'assignment', 'student', 'auto_comment', - 'manual_comment'} - - assert cd['id'] == c.id - assert cd['name'] == c.name - assert cd['notebook'] == 'blah' - assert cd['assignment'] == 'foo' - assert cd['student'] == c.student.id - assert cd['auto_comment'] is None - assert cd['manual_comment'] is None - - # make sure it can be JSONified - json.dumps(cd) diff --git a/nbgrader/tests/apps/__init__.py b/nbgrader/tests/apps/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/nbgrader/tests/apps/base.py b/nbgrader/tests/apps/base.py deleted file mode 100644 index 9e5c6b8a2..000000000 --- a/nbgrader/tests/apps/base.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- - -import io -import os -import shutil -import pytest - -from nbformat import write as write_nb -from nbformat.v4 import new_notebook - -from ...utils import remove - - -@pytest.mark.usefixtures("temp_cwd") -class BaseTestApp(object): - - def _empty_notebook(self, path, kernel=None): - nb = new_notebook() - if kernel is not None: - nb.metadata.kernelspec = { - "display_name": "kernel", - "language": kernel, - "name": kernel - } - - full_dest = os.path.abspath(path) - if not os.path.exists(os.path.dirname(full_dest)): - os.makedirs(os.path.dirname(full_dest)) - if os.path.exists(full_dest): - remove(full_dest) - with io.open(full_dest, mode='w', encoding='utf-8') as f: - write_nb(nb, f, 4) - - def _copy_file(self, src: str, dest: str) -> None: - full_src = os.path.join(os.path.dirname(__file__), src) - full_dest = os.path.abspath(dest) - if not os.path.exists(os.path.dirname(full_dest)): - os.makedirs(os.path.dirname(full_dest)) - if os.path.exists(full_dest): - remove(full_dest) - shutil.copy(full_src, full_dest) - - def _move_file(self, src, dest): - full_src = os.path.abspath(src) - full_dest = os.path.abspath(dest) - if not os.path.exists(os.path.dirname(full_dest)): - os.makedirs(os.path.dirname(full_dest)) - if os.path.exists(full_dest): - remove(full_dest) - shutil.move(full_src, full_dest) - - def _make_file(self, path: str, contents: str = "") -> None: - full_dest = os.path.abspath(path) - if not os.path.exists(os.path.dirname(full_dest)): - os.makedirs(os.path.dirname(full_dest)) - if os.path.exists(full_dest): - remove(full_dest) - with open(path, "w") as fh: - fh.write(contents) - - def _get_permissions(self, filename): - st_mode = os.stat(filename).st_mode - # If setgid is true, return four bytes. For testing CourseDirectory.groupshared. - if st_mode & 0o2000: - return oct(st_mode)[-4:] - return oct(st_mode)[-3:] - - def _file_contents(self, path): - with open(path, "r") as fh: - contents = fh.read() - return contents diff --git a/nbgrader/tests/apps/conftest.py b/nbgrader/tests/apps/conftest.py deleted file mode 100644 index e1173224b..000000000 --- a/nbgrader/tests/apps/conftest.py +++ /dev/null @@ -1,134 +0,0 @@ -import os -import tempfile -import shutil -import pytest -import sys - -from textwrap import dedent - -from _pytest.fixtures import SubRequest - -from ...api import Gradebook -from ...utils import rmtree - - -@pytest.fixture -def db(request: SubRequest) -> str: - path = tempfile.mkdtemp(prefix='tmp-dbdir-') - dbpath = os.path.join(path, "nbgrader_test.db") - - def fin() -> None: - rmtree(path) - request.addfinalizer(fin) - - return "sqlite:///" + dbpath - - -@pytest.fixture -def course_dir(request: SubRequest) -> str: - path = tempfile.mkdtemp(prefix='tmp-coursedir-') - - def fin() -> None: - rmtree(path) - request.addfinalizer(fin) - - return path - - -@pytest.fixture -def temp_cwd(request: SubRequest, course_dir: str) -> str: - orig_dir = os.getcwd() - path = tempfile.mkdtemp(prefix='tmp-cwd-') - os.chdir(path) - - with open("nbgrader_config.py", "w") as fh: - fh.write(dedent( - """ - c = get_config() - c.CourseDirectory.root = r"{}" - """.format(course_dir) - )) - - def fin() -> None: - os.chdir(orig_dir) - rmtree(path) - request.addfinalizer(fin) - - return path - - -@pytest.fixture -def jupyter_config_dir(request): - path = tempfile.mkdtemp(prefix='tmp-configdir-') - - def fin(): - rmtree(path) - request.addfinalizer(fin) - - return path - - -@pytest.fixture -def jupyter_data_dir(request): - path = tempfile.mkdtemp(prefix='tmp-datadir-') - - def fin(): - rmtree(path) - request.addfinalizer(fin) - - return path - - -@pytest.fixture -def fake_home_dir(request, monkeypatch): - ''' - this fixture creates a temporary home directory. This prevents existing - nbgrader_config.py files in the user directory to interfer with the tests. - ''' - path = tempfile.mkdtemp(prefix='tmp-homedir-') - - def fin(): - rmtree(path) - request.addfinalizer(fin) - - monkeypatch.setenv('HOME', str(path)) - - return path - - -@pytest.fixture -def env(request, jupyter_config_dir, jupyter_data_dir): - env = os.environ.copy() - env['JUPYTER_DATA_DIR'] = jupyter_data_dir - env['JUPYTER_CONFIG_DIR'] = jupyter_config_dir - return env - - -@pytest.fixture -def exchange(request): - path = tempfile.mkdtemp(prefix='tmp-exchange-') - - def fin(): - rmtree(path) - request.addfinalizer(fin) - - return path - - -@pytest.fixture -def cache(request): - path = tempfile.mkdtemp(prefix='tmp-cache-') - - def fin(): - rmtree(path) - request.addfinalizer(fin) - - return path - -notwindows = pytest.mark.skipif( - sys.platform == 'win32', - reason='This functionality of nbgrader is unsupported on Windows') - -windows = pytest.mark.skipif( - sys.platform != 'win32', - reason='This test is only to be run on Windows') diff --git a/nbgrader/tests/apps/files/__init__.py b/nbgrader/tests/apps/files/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/nbgrader/tests/apps/files/autotest-hashed-changed.ipynb b/nbgrader/tests/apps/files/autotest-hashed-changed.ipynb deleted file mode 100644 index 8ddbb8633..000000000 --- a/nbgrader/tests/apps/files/autotest-hashed-changed.ipynb +++ /dev/null @@ -1,112 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "nbgrader": { - "cell_type": "code", - "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", - "grade": false, - "grade_id": "soln", - "locked": false, - "schema_version": 3, - "solution": true - } - }, - "outputs": [], - "source": [ - "# YOUR CODE HERE\n", - "a = 5\n", - "b = \"hello\"\n", - "c = [1, 2, \"test\"]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "editable": false, - "max_height": 100, - "nbgrader": { - "cell_type": "code", - "checksum": "a0a263f3cd77437ecaaaa68ffd10ca2f", - "grade": true, - "grade_id": "test_hashed", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": false - } - }, - "outputs": [], - "source": [ - "from hashlib import sha1\n", - "assert sha1(str(type(a)).encode(\"utf-8\")+b\"9231f34cdc3da9ea\").hexdigest() == \"659113e142e34b819add5d3c95d33a1cf10a09ae\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert sha1(str(a).encode(\"utf-8\")+b\"9231f34cdc3da9ea\").hexdigest() == \"455510a3efa0017fa35841cd77fb1554ee665d0a\", \"value of a is not correct\"\n", - "\n", - "assert sha1(str(type(b)).encode(\"utf-8\")+b\"13937391b113e128\").hexdigest() == \"a7a6d540df4104f3adb629c5c3ea3c7c461eaa6f\", \"type of b is not str. b should be an str\"\n", - "assert sha1(str(len(b)).encode(\"utf-8\")+b\"13937391b113e128\").hexdigest() == \"b8eda0d698f28aedc80f0f318c35af3447c18f29\", \"length of b is not correct\"\n", - "assert sha1(str(b.lower()).encode(\"utf-8\")+b\"13937391b113e128\").hexdigest() == \"2755a4e47f3b60f679787659f804488276ec718e\", \"value of b is not correct\"\n", - "assert sha1(str(b).encode(\"utf-8\")+b\"13937391b113e128\").hexdigest() == \"2755a4e47f3b60f679787659f804488276ec718e\", \"correct string value of b but incorrect case of letters\"\n", - "\n", - "assert sha1(str(type(c)).encode(\"utf-8\")+b\"e33308cee3b48f12\").hexdigest() == \"2da3504fe36c50ab1d44ffb61bd4a02af8f0fdbd\", \"type of c is not list. c should be a list\"\n", - "assert sha1(str(len(c)).encode(\"utf-8\")+b\"e33308cee3b48f12\").hexdigest() == \"5f8b7c79f0b7ee5fafa410947a40589acbf2b644\", \"length of c is not correct\"\n", - "assert sha1(str(sorted(map(str, c))).encode(\"utf-8\")+b\"e33308cee3b48f12\").hexdigest() == \"10c943e426df5e88a394da50cc9cc07f6cb2fa33\", \"values of c are not correct\"\n", - "assert sha1(str(c).encode(\"utf-8\")+b\"e33308cee3b48f12\").hexdigest() == \"c538ca013d170994051b3d55fb2c488d2fadb776\", \"order of elements of c is not correct\"\n", - "\n", - "\n", - "# differing spacings, num comment characters, trailing whitespace\n", - "assert sha1(str(type(a)).encode(\"utf-8\")+b\"cfe4dae897378d1a\").hexdigest() == \"463fc87d8b90bcf558e04b5bff454d0fe10f0447\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert sha1(str(a).encode(\"utf-8\")+b\"cfe4dae897378d1a\").hexdigest() == \"586e477f328cc5e3c852148517751d072d36afee\", \"value of a is not correct\"\n", - "\n", - "assert sha1(str(type(a)).encode(\"utf-8\")+b\"ec2be017656b8d95\").hexdigest() == \"2abb3c781a9ed14b1dec5a00736dc170f3301ce2\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert sha1(str(a).encode(\"utf-8\")+b\"ec2be017656b8d95\").hexdigest() == \"b86b5db1c86084688de67ed5b3e2001206365a44\", \"value of a is not correct\"\n", - "\n", - "assert sha1(str(type(a)).encode(\"utf-8\")+b\"da663014407e8b68\").hexdigest() == \"4c5e40a4f0ec62a3dbf5232e9d22faba40b1d394\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert sha1(str(a).encode(\"utf-8\")+b\"da663014407e8b68\").hexdigest() == \"419c839a51a681a8fe1bd658d02c285ab657a1ef\", \"value of a is not correct\"\n", - "\n", - "assert sha1(str(type(a)).encode(\"utf-8\")+b\"f97fab4e29c2f008\").hexdigest() == \"21248e96318cac95a37157dcded2b3e9b3778668\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert sha1(str(a).encode(\"utf-8\")+b\"f97fab4e29c2f008\").hexdigest() == \"0e27de567679deef32bd63ebd416cb835db76ba4\", \"value of a is not correct\"\n", - "\n", - "assert sha1(str(type(a)).encode(\"utf-8\")+b\"72eea5ecf1bd1e19\").hexdigest() == \"6008755168651743936153fa68a21c1c4369b61a\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert sha1(str(a).encode(\"utf-8\")+b\"72eea5ecf1bd1e19\").hexdigest() == \"b5d66cbe4aaaaf8a45e1ceef0e2e03f70fcefa59\", \"value of a is not correct\"\n", - "\n", - "assert sha1(str(type(a)).encode(\"utf-8\")+b\"93205c8cd03cafd5\").hexdigest() == \"2292f651355b47a594a97fe2e04253414aefe4bd\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert sha1(str(a).encode(\"utf-8\")+b\"93205c8cd03cafd5\").hexdigest() == \"f5eba7f0391b94b32a57691d8f982659d50fbf9f\", \"value of a is not correct\"\n", - "\n", - "print('Success!')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/nbgrader/tests/apps/files/autotest-hashed-unchanged.ipynb b/nbgrader/tests/apps/files/autotest-hashed-unchanged.ipynb deleted file mode 100644 index 86f485097..000000000 --- a/nbgrader/tests/apps/files/autotest-hashed-unchanged.ipynb +++ /dev/null @@ -1,110 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "nbgrader": { - "cell_type": "code", - "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", - "grade": false, - "grade_id": "soln", - "locked": false, - "schema_version": 3, - "solution": true - } - }, - "outputs": [], - "source": [ - "# YOUR CODE HERE\n", - "raise NotImplementedError()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "editable": false, - "max_height": 100, - "nbgrader": { - "cell_type": "code", - "checksum": "a0a263f3cd77437ecaaaa68ffd10ca2f", - "grade": true, - "grade_id": "test_hashed", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": false - } - }, - "outputs": [], - "source": [ - "from hashlib import sha1\n", - "assert sha1(str(type(a)).encode(\"utf-8\")+b\"9231f34cdc3da9ea\").hexdigest() == \"659113e142e34b819add5d3c95d33a1cf10a09ae\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert sha1(str(a).encode(\"utf-8\")+b\"9231f34cdc3da9ea\").hexdigest() == \"455510a3efa0017fa35841cd77fb1554ee665d0a\", \"value of a is not correct\"\n", - "\n", - "assert sha1(str(type(b)).encode(\"utf-8\")+b\"13937391b113e128\").hexdigest() == \"a7a6d540df4104f3adb629c5c3ea3c7c461eaa6f\", \"type of b is not str. b should be an str\"\n", - "assert sha1(str(len(b)).encode(\"utf-8\")+b\"13937391b113e128\").hexdigest() == \"b8eda0d698f28aedc80f0f318c35af3447c18f29\", \"length of b is not correct\"\n", - "assert sha1(str(b.lower()).encode(\"utf-8\")+b\"13937391b113e128\").hexdigest() == \"2755a4e47f3b60f679787659f804488276ec718e\", \"value of b is not correct\"\n", - "assert sha1(str(b).encode(\"utf-8\")+b\"13937391b113e128\").hexdigest() == \"2755a4e47f3b60f679787659f804488276ec718e\", \"correct string value of b but incorrect case of letters\"\n", - "\n", - "assert sha1(str(type(c)).encode(\"utf-8\")+b\"e33308cee3b48f12\").hexdigest() == \"2da3504fe36c50ab1d44ffb61bd4a02af8f0fdbd\", \"type of c is not list. c should be a list\"\n", - "assert sha1(str(len(c)).encode(\"utf-8\")+b\"e33308cee3b48f12\").hexdigest() == \"5f8b7c79f0b7ee5fafa410947a40589acbf2b644\", \"length of c is not correct\"\n", - "assert sha1(str(sorted(map(str, c))).encode(\"utf-8\")+b\"e33308cee3b48f12\").hexdigest() == \"10c943e426df5e88a394da50cc9cc07f6cb2fa33\", \"values of c are not correct\"\n", - "assert sha1(str(c).encode(\"utf-8\")+b\"e33308cee3b48f12\").hexdigest() == \"c538ca013d170994051b3d55fb2c488d2fadb776\", \"order of elements of c is not correct\"\n", - "\n", - "\n", - "# differing spacings, num comment characters, trailing whitespace\n", - "assert sha1(str(type(a)).encode(\"utf-8\")+b\"cfe4dae897378d1a\").hexdigest() == \"463fc87d8b90bcf558e04b5bff454d0fe10f0447\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert sha1(str(a).encode(\"utf-8\")+b\"cfe4dae897378d1a\").hexdigest() == \"586e477f328cc5e3c852148517751d072d36afee\", \"value of a is not correct\"\n", - "\n", - "assert sha1(str(type(a)).encode(\"utf-8\")+b\"ec2be017656b8d95\").hexdigest() == \"2abb3c781a9ed14b1dec5a00736dc170f3301ce2\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert sha1(str(a).encode(\"utf-8\")+b\"ec2be017656b8d95\").hexdigest() == \"b86b5db1c86084688de67ed5b3e2001206365a44\", \"value of a is not correct\"\n", - "\n", - "assert sha1(str(type(a)).encode(\"utf-8\")+b\"da663014407e8b68\").hexdigest() == \"4c5e40a4f0ec62a3dbf5232e9d22faba40b1d394\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert sha1(str(a).encode(\"utf-8\")+b\"da663014407e8b68\").hexdigest() == \"419c839a51a681a8fe1bd658d02c285ab657a1ef\", \"value of a is not correct\"\n", - "\n", - "assert sha1(str(type(a)).encode(\"utf-8\")+b\"f97fab4e29c2f008\").hexdigest() == \"21248e96318cac95a37157dcded2b3e9b3778668\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert sha1(str(a).encode(\"utf-8\")+b\"f97fab4e29c2f008\").hexdigest() == \"0e27de567679deef32bd63ebd416cb835db76ba4\", \"value of a is not correct\"\n", - "\n", - "assert sha1(str(type(a)).encode(\"utf-8\")+b\"72eea5ecf1bd1e19\").hexdigest() == \"6008755168651743936153fa68a21c1c4369b61a\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert sha1(str(a).encode(\"utf-8\")+b\"72eea5ecf1bd1e19\").hexdigest() == \"b5d66cbe4aaaaf8a45e1ceef0e2e03f70fcefa59\", \"value of a is not correct\"\n", - "\n", - "assert sha1(str(type(a)).encode(\"utf-8\")+b\"93205c8cd03cafd5\").hexdigest() == \"2292f651355b47a594a97fe2e04253414aefe4bd\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert sha1(str(a).encode(\"utf-8\")+b\"93205c8cd03cafd5\").hexdigest() == \"f5eba7f0391b94b32a57691d8f982659d50fbf9f\", \"value of a is not correct\"\n", - "\n", - "print('Success!')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/nbgrader/tests/apps/files/autotest-hashed.ipynb b/nbgrader/tests/apps/files/autotest-hashed.ipynb deleted file mode 100644 index 1b4df7b83..000000000 --- a/nbgrader/tests/apps/files/autotest-hashed.ipynb +++ /dev/null @@ -1,82 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": false, - "grade_id": "soln", - "locked": false, - "schema_version": 3, - "solution": true - } - }, - "outputs": [], - "source": [ - "### BEGIN SOLUTION\n", - "a = 5\n", - "b = \"hello\"\n", - "c = [1, 2, \"test\"]\n", - "### END SOLUTION" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "test_hashed", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": false - } - }, - "outputs": [], - "source": [ - "### HASHED AUTOTEST a\n", - "### HASHED AUTOTEST b\n", - "### HASHED AUTOTEST c\n", - "\n", - "# differing spacings, num comment characters, trailing whitespace\n", - "### HASHED AUTOTEST a \n", - "#HASHED AUTOTEST a\n", - "# HASHED AUTOTEST a\n", - "## HASHED AUTOTEST a\n", - "# HASHED AUTOTEST a\n", - "# # # # HASHED AUTOTEST a" - ] - } - ], - "metadata": { - "celltoolbar": "Create Assignment", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/nbgrader/tests/apps/files/autotest-hidden-changed-right.ipynb b/nbgrader/tests/apps/files/autotest-hidden-changed-right.ipynb deleted file mode 100644 index d17fa7735..000000000 --- a/nbgrader/tests/apps/files/autotest-hidden-changed-right.ipynb +++ /dev/null @@ -1,85 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "nbgrader": { - "cell_type": "code", - "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", - "grade": false, - "grade_id": "soln", - "locked": false, - "schema_version": 3, - "solution": true - } - }, - "outputs": [], - "source": [ - "# YOUR CODE HERE\n", - "a = 5\n", - "b = \"hello\"\n", - "c = [1, 2, \"test\"]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "editable": false, - "max_height": 100, - "nbgrader": { - "cell_type": "code", - "checksum": "2fd15c162690346636dd1f2881f6b31e", - "grade": true, - "grade_id": "test_hidden", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": false - } - }, - "outputs": [], - "source": [ - "\n", - "assert str(type(a)) == \"\", \"type of type(a) is not correct\"\n", - "\n", - "assert str(type(b)) == \"\", \"type of type(b) is not correct\"\n", - "\n", - "assert str(type(c)) == \"\", \"type of type(c) is not correct\"\n", - "\n", - "print('Success!')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/nbgrader/tests/apps/files/autotest-hidden-changed-wrong.ipynb b/nbgrader/tests/apps/files/autotest-hidden-changed-wrong.ipynb deleted file mode 100644 index e84452ef5..000000000 --- a/nbgrader/tests/apps/files/autotest-hidden-changed-wrong.ipynb +++ /dev/null @@ -1,85 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "nbgrader": { - "cell_type": "code", - "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", - "grade": false, - "grade_id": "soln", - "locked": false, - "schema_version": 3, - "solution": true - } - }, - "outputs": [], - "source": [ - "# YOUR CODE HERE\n", - "a = 7\n", - "b = \"notright\"\n", - "c = [3, 4, \"hi\"]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "editable": false, - "max_height": 100, - "nbgrader": { - "cell_type": "code", - "checksum": "2fd15c162690346636dd1f2881f6b31e", - "grade": true, - "grade_id": "test_hidden", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": false - } - }, - "outputs": [], - "source": [ - "\n", - "assert str(type(a)) == \"\", \"type of type(a) is not correct\"\n", - "\n", - "assert str(type(b)) == \"\", \"type of type(b) is not correct\"\n", - "\n", - "assert str(type(c)) == \"\", \"type of type(c) is not correct\"\n", - "\n", - "print('Success!')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/nbgrader/tests/apps/files/autotest-hidden-unchanged.ipynb b/nbgrader/tests/apps/files/autotest-hidden-unchanged.ipynb deleted file mode 100644 index be153756b..000000000 --- a/nbgrader/tests/apps/files/autotest-hidden-unchanged.ipynb +++ /dev/null @@ -1,83 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "nbgrader": { - "cell_type": "code", - "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", - "grade": false, - "grade_id": "soln", - "locked": false, - "schema_version": 3, - "solution": true - } - }, - "outputs": [], - "source": [ - "# YOUR CODE HERE\n", - "raise NotImplementedError()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "editable": false, - "max_height": 100, - "nbgrader": { - "cell_type": "code", - "checksum": "2fd15c162690346636dd1f2881f6b31e", - "grade": true, - "grade_id": "test_hidden", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": false - } - }, - "outputs": [], - "source": [ - "\n", - "assert str(type(a)) == \"\", \"type of type(a) is not correct\"\n", - "\n", - "assert str(type(b)) == \"\", \"type of type(b) is not correct\"\n", - "\n", - "assert str(type(c)) == \"\", \"type of type(c) is not correct\"\n", - "\n", - "print('Success!')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/nbgrader/tests/apps/files/autotest-hidden.ipynb b/nbgrader/tests/apps/files/autotest-hidden.ipynb deleted file mode 100644 index c2e6e8e23..000000000 --- a/nbgrader/tests/apps/files/autotest-hidden.ipynb +++ /dev/null @@ -1,80 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": false, - "grade_id": "soln", - "locked": false, - "schema_version": 3, - "solution": true - } - }, - "outputs": [], - "source": [ - "### BEGIN SOLUTION\n", - "a = 5\n", - "b = \"hello\"\n", - "c = [1, 2, \"test\"]\n", - "### END SOLUTION" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "test_hidden", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": false - } - }, - "outputs": [], - "source": [ - "### BEGIN HIDDEN TESTS\n", - "### AUTOTEST a\n", - "### AUTOTEST b\n", - "### AUTOTEST c\n", - "### END HIDDEN TESTS\n", - "\n", - "### AUTOTEST type(a)\n", - "### AUTOTEST type(b)\n", - "### AUTOTEST type(c)" - ] - } - ], - "metadata": { - "celltoolbar": "Create Assignment", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/nbgrader/tests/apps/files/autotest-multi-changed.ipynb b/nbgrader/tests/apps/files/autotest-multi-changed.ipynb deleted file mode 100644 index 04470cca4..000000000 --- a/nbgrader/tests/apps/files/autotest-multi-changed.ipynb +++ /dev/null @@ -1,267 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "nbgrader": { - "cell_type": "code", - "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", - "grade": false, - "grade_id": "soln", - "locked": false, - "schema_version": 3, - "solution": true - } - }, - "outputs": [], - "source": [ - "# YOUR CODE HERE\n", - "a = 5\n", - "b = \"hello\"\n", - "c = [1, 2, \"test\"]\n", - "d = {1 : 6, 3: 5} #right type, wrong value\n", - "e = [5, -3.3, 'd'] #right values, wrong type\n", - "f = True # wrong value\n", - "def fun(x):\n", - " if x < 0:\n", - " raise ValueError\n", - " else:\n", - " return x" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "editable": false, - "max_height": 100, - "nbgrader": { - "cell_type": "code", - "checksum": "620860a42fa05b556da01013ef99ed8c", - "grade": true, - "grade_id": "test_multi", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": false - } - }, - "outputs": [], - "source": [ - "# the basic tests from the simple notebook\n", - "from hashlib import sha1\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "assert str(type(b)) == \"\", \"type of b is not str. b should be an str\"\n", - "assert str(len(b)) == \"5\", \"length of b is not correct\"\n", - "assert str(b.lower()) == \"hello\", \"value of b is not correct\"\n", - "assert str(b) == \"hello\", \"correct string value of b but incorrect case of letters\"\n", - "\n", - "assert str(type(c)) == \"\", \"type of c is not list. c should be a list\"\n", - "assert str(len(c)) == \"3\", \"length of c is not correct\"\n", - "assert str(sorted(map(str, c))) == \"['1', '2', 'test']\", \"values of c are not correct\"\n", - "assert str(c) == \"[1, 2, 'test']\", \"order of elements of c is not correct\"\n", - "\n", - "print('Success!')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "editable": false, - "max_height": 100, - "nbgrader": { - "cell_type": "code", - "checksum": "c46928757ed303ce204ef97299b39f82", - "grade": true, - "grade_id": "cell-f2803ba7c42d03ab", - "locked": true, - "points": 1, - "schema_version": 3, - "solution": false, - "task": false - } - }, - "outputs": [], - "source": [ - "# multiple expressions per line\n", - "from hashlib import sha1\n", - "assert str(type(a)) == \"\", \"type of type(a) is not correct\"\n", - "\n", - "assert str(type(str(a))) == \"\", \"type of str(a) is not str. str(a) should be an str\"\n", - "assert str(len(str(a))) == \"1\", \"length of str(a) is not correct\"\n", - "assert str(str(a).lower()) == \"5\", \"value of str(a) is not correct\"\n", - "assert str(str(a)) == \"5\", \"correct string value of str(a) but incorrect case of letters\"\n", - "\n", - "assert str(type(a == \"5\")) == \"\", \"type of a == \\\"5\\\" is not bool. a == \\\"5\\\" should be a bool\"\n", - "assert str(a == \"5\") == \"False\", \"boolean value of a == \\\"5\\\" is not correct\"\n", - "\n", - "assert str(type([ch for ch in b])) == \"\", \"type of [ch for ch in b] is not list. [ch for ch in b] should be a list\"\n", - "assert str(len([ch for ch in b])) == \"5\", \"length of [ch for ch in b] is not correct\"\n", - "assert str(sorted(map(str, [ch for ch in b]))) == \"['e', 'h', 'l', 'l', 'o']\", \"values of [ch for ch in b] are not correct\"\n", - "assert str([ch for ch in b]) == \"['h', 'e', 'l', 'l', 'o']\", \"order of elements of [ch for ch in b] is not correct\"\n", - "\n", - "assert str(type(len(c))) == \"\", \"type of len(c) is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(len(c)) == \"3\", \"value of len(c) is not correct\"\n", - "\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "\n", - "# intervening regular code\n", - "print(\"hello!\")\n", - "\n", - "# differing spacings, numbers of comment characters, trailing whitespace\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "\n", - "\n", - "print('Success!')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "editable": false, - "max_height": 100, - "nbgrader": { - "cell_type": "code", - "checksum": "2fc8066fae8e7aeb865116c88d1be31a", - "grade": true, - "grade_id": "cell-693350420ec62f1b", - "locked": true, - "points": 1, - "schema_version": 3, - "solution": false, - "task": false - } - }, - "outputs": [], - "source": [ - "# a few common types\n", - "from hashlib import sha1\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "assert str(type(b)) == \"\", \"type of b is not str. b should be an str\"\n", - "assert str(len(b)) == \"5\", \"length of b is not correct\"\n", - "assert str(b.lower()) == \"hello\", \"value of b is not correct\"\n", - "assert str(b) == \"hello\", \"correct string value of b but incorrect case of letters\"\n", - "\n", - "assert str(type(c)) == \"\", \"type of c is not list. c should be a list\"\n", - "assert str(len(c)) == \"3\", \"length of c is not correct\"\n", - "assert str(sorted(map(str, c))) == \"['1', '2', 'test']\", \"values of c are not correct\"\n", - "assert str(c) == \"[1, 2, 'test']\", \"order of elements of c is not correct\"\n", - "\n", - "assert str(type(d)) == \"\", \"type of d is not dict. d should be a dict\"\n", - "assert str(len(list(d.keys()))) == \"2\", \"number of keys of d is not correct\"\n", - "assert str(sorted(map(str, d.keys()))) == \"['3', 'a']\", \"keys of d are not correct\"\n", - "assert str(sorted(map(str, d.values()))) == \"['[1.2, 3]', 'f']\", \"correct keys, but values of d are not correct\"\n", - "assert str(d) == \"{'a': 'f', 3: [1.2, 3]}\", \"correct keys and values, but incorrect correspondence in keys and values of d\"\n", - "\n", - "assert str(type(e)) == \"\", \"type of e is not tuple. e should be a tuple\"\n", - "assert str(len(e)) == \"3\", \"length of e is not correct\"\n", - "assert str(sorted(map(str, e))) == \"['-3.3', '5', 'd']\", \"values of e are not correct\"\n", - "assert str(e) == \"(5, -3.3, 'd')\", \"order of elements of e is not correct\"\n", - "\n", - "assert str(type(f)) == \"\", \"type of f is not bool. f should be a bool\"\n", - "assert str(f) == \"False\", \"boolean value of f is not correct\"\n", - "\n", - "print('Success!')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "editable": false, - "max_height": 100, - "nbgrader": { - "cell_type": "code", - "checksum": "cc9122036c0a86a93446786109a3558e", - "grade": true, - "grade_id": "cell-13479eb0e5fff152", - "locked": true, - "points": 1, - "schema_version": 3, - "solution": false, - "task": false - } - }, - "outputs": [], - "source": [ - "\n", - "# a function that checks whether a function throws an error\n", - "\"\"\"Check that squares raises an error for invalid inputs\"\"\"\n", - "def test_func_throws(func, ErrorType):\n", - " try:\n", - " func()\n", - " except ErrorType:\n", - " return True\n", - " else:\n", - " print('Did not raise right type of error!')\n", - " return False\n", - "\n", - "# test a custom function\n", - "from hashlib import sha1\n", - "assert str(type(fun(3))) == \"\", \"type of fun(3) is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(fun(3)) == \"3\", \"value of fun(3) is not correct\"\n", - "\n", - "assert str(type(test_func_throws(lambda : fun(-4), ValueError))) == \"\", \"type of test_func_throws(lambda : fun(-4), ValueError) is not bool. test_func_throws(lambda : fun(-4), ValueError) should be a bool\"\n", - "assert str(test_func_throws(lambda : fun(-4), ValueError)) == \"True\", \"boolean value of test_func_throws(lambda : fun(-4), ValueError) is not correct\"\n", - "\n", - "assert str(type(test_func_throws(lambda : fun(0), ValueError))) == \"\", \"type of test_func_throws(lambda : fun(0), ValueError) is not bool. test_func_throws(lambda : fun(0), ValueError) should be a bool\"\n", - "assert str(test_func_throws(lambda : fun(0), ValueError)) == \"False\", \"boolean value of test_func_throws(lambda : fun(0), ValueError) is not correct\"\n", - "\n", - "\n", - "print('Success!')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/nbgrader/tests/apps/files/autotest-multi-unchanged.ipynb b/nbgrader/tests/apps/files/autotest-multi-unchanged.ipynb deleted file mode 100644 index 6090389a7..000000000 --- a/nbgrader/tests/apps/files/autotest-multi-unchanged.ipynb +++ /dev/null @@ -1,257 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "nbgrader": { - "cell_type": "code", - "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", - "grade": false, - "grade_id": "soln", - "locked": false, - "schema_version": 3, - "solution": true - } - }, - "outputs": [], - "source": [ - "# YOUR CODE HERE\n", - "raise NotImplementedError()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "editable": false, - "max_height": 100, - "nbgrader": { - "cell_type": "code", - "checksum": "620860a42fa05b556da01013ef99ed8c", - "grade": true, - "grade_id": "test_multi", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": false - } - }, - "outputs": [], - "source": [ - "# the basic tests from the simple notebook\n", - "from hashlib import sha1\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "assert str(type(b)) == \"\", \"type of b is not str. b should be an str\"\n", - "assert str(len(b)) == \"5\", \"length of b is not correct\"\n", - "assert str(b.lower()) == \"hello\", \"value of b is not correct\"\n", - "assert str(b) == \"hello\", \"correct string value of b but incorrect case of letters\"\n", - "\n", - "assert str(type(c)) == \"\", \"type of c is not list. c should be a list\"\n", - "assert str(len(c)) == \"3\", \"length of c is not correct\"\n", - "assert str(sorted(map(str, c))) == \"['1', '2', 'test']\", \"values of c are not correct\"\n", - "assert str(c) == \"[1, 2, 'test']\", \"order of elements of c is not correct\"\n", - "\n", - "print('Success!')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "editable": false, - "max_height": 100, - "nbgrader": { - "cell_type": "code", - "checksum": "c46928757ed303ce204ef97299b39f82", - "grade": true, - "grade_id": "cell-f2803ba7c42d03ab", - "locked": true, - "points": 1, - "schema_version": 3, - "solution": false, - "task": false - } - }, - "outputs": [], - "source": [ - "# multiple expressions per line\n", - "from hashlib import sha1\n", - "assert str(type(a)) == \"\", \"type of type(a) is not correct\"\n", - "\n", - "assert str(type(str(a))) == \"\", \"type of str(a) is not str. str(a) should be an str\"\n", - "assert str(len(str(a))) == \"1\", \"length of str(a) is not correct\"\n", - "assert str(str(a).lower()) == \"5\", \"value of str(a) is not correct\"\n", - "assert str(str(a)) == \"5\", \"correct string value of str(a) but incorrect case of letters\"\n", - "\n", - "assert str(type(a == \"5\")) == \"\", \"type of a == \\\"5\\\" is not bool. a == \\\"5\\\" should be a bool\"\n", - "assert str(a == \"5\") == \"False\", \"boolean value of a == \\\"5\\\" is not correct\"\n", - "\n", - "assert str(type([ch for ch in b])) == \"\", \"type of [ch for ch in b] is not list. [ch for ch in b] should be a list\"\n", - "assert str(len([ch for ch in b])) == \"5\", \"length of [ch for ch in b] is not correct\"\n", - "assert str(sorted(map(str, [ch for ch in b]))) == \"['e', 'h', 'l', 'l', 'o']\", \"values of [ch for ch in b] are not correct\"\n", - "assert str([ch for ch in b]) == \"['h', 'e', 'l', 'l', 'o']\", \"order of elements of [ch for ch in b] is not correct\"\n", - "\n", - "assert str(type(len(c))) == \"\", \"type of len(c) is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(len(c)) == \"3\", \"value of len(c) is not correct\"\n", - "\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "\n", - "# intervening regular code\n", - "print(\"hello!\")\n", - "\n", - "# differing spacings, numbers of comment characters, trailing whitespace\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "\n", - "\n", - "print('Success!')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "editable": false, - "max_height": 100, - "nbgrader": { - "cell_type": "code", - "checksum": "2fc8066fae8e7aeb865116c88d1be31a", - "grade": true, - "grade_id": "cell-693350420ec62f1b", - "locked": true, - "points": 1, - "schema_version": 3, - "solution": false, - "task": false - } - }, - "outputs": [], - "source": [ - "# a few common types\n", - "from hashlib import sha1\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "assert str(type(b)) == \"\", \"type of b is not str. b should be an str\"\n", - "assert str(len(b)) == \"5\", \"length of b is not correct\"\n", - "assert str(b.lower()) == \"hello\", \"value of b is not correct\"\n", - "assert str(b) == \"hello\", \"correct string value of b but incorrect case of letters\"\n", - "\n", - "assert str(type(c)) == \"\", \"type of c is not list. c should be a list\"\n", - "assert str(len(c)) == \"3\", \"length of c is not correct\"\n", - "assert str(sorted(map(str, c))) == \"['1', '2', 'test']\", \"values of c are not correct\"\n", - "assert str(c) == \"[1, 2, 'test']\", \"order of elements of c is not correct\"\n", - "\n", - "assert str(type(d)) == \"\", \"type of d is not dict. d should be a dict\"\n", - "assert str(len(list(d.keys()))) == \"2\", \"number of keys of d is not correct\"\n", - "assert str(sorted(map(str, d.keys()))) == \"['3', 'a']\", \"keys of d are not correct\"\n", - "assert str(sorted(map(str, d.values()))) == \"['[1.2, 3]', 'f']\", \"correct keys, but values of d are not correct\"\n", - "assert str(d) == \"{'a': 'f', 3: [1.2, 3]}\", \"correct keys and values, but incorrect correspondence in keys and values of d\"\n", - "\n", - "assert str(type(e)) == \"\", \"type of e is not tuple. e should be a tuple\"\n", - "assert str(len(e)) == \"3\", \"length of e is not correct\"\n", - "assert str(sorted(map(str, e))) == \"['-3.3', '5', 'd']\", \"values of e are not correct\"\n", - "assert str(e) == \"(5, -3.3, 'd')\", \"order of elements of e is not correct\"\n", - "\n", - "assert str(type(f)) == \"\", \"type of f is not bool. f should be a bool\"\n", - "assert str(f) == \"False\", \"boolean value of f is not correct\"\n", - "\n", - "print('Success!')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "editable": false, - "max_height": 100, - "nbgrader": { - "cell_type": "code", - "checksum": "cc9122036c0a86a93446786109a3558e", - "grade": true, - "grade_id": "cell-13479eb0e5fff152", - "locked": true, - "points": 1, - "schema_version": 3, - "solution": false, - "task": false - } - }, - "outputs": [], - "source": [ - "\n", - "# a function that checks whether a function throws an error\n", - "\"\"\"Check that squares raises an error for invalid inputs\"\"\"\n", - "def test_func_throws(func, ErrorType):\n", - " try:\n", - " func()\n", - " except ErrorType:\n", - " return True\n", - " else:\n", - " print('Did not raise right type of error!')\n", - " return False\n", - "\n", - "# test a custom function\n", - "from hashlib import sha1\n", - "assert str(type(fun(3))) == \"\", \"type of fun(3) is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(fun(3)) == \"3\", \"value of fun(3) is not correct\"\n", - "\n", - "assert str(type(test_func_throws(lambda : fun(-4), ValueError))) == \"\", \"type of test_func_throws(lambda : fun(-4), ValueError) is not bool. test_func_throws(lambda : fun(-4), ValueError) should be a bool\"\n", - "assert str(test_func_throws(lambda : fun(-4), ValueError)) == \"True\", \"boolean value of test_func_throws(lambda : fun(-4), ValueError) is not correct\"\n", - "\n", - "assert str(type(test_func_throws(lambda : fun(0), ValueError))) == \"\", \"type of test_func_throws(lambda : fun(0), ValueError) is not bool. test_func_throws(lambda : fun(0), ValueError) should be a bool\"\n", - "assert str(test_func_throws(lambda : fun(0), ValueError)) == \"False\", \"boolean value of test_func_throws(lambda : fun(0), ValueError) is not correct\"\n", - "\n", - "\n", - "print('Success!')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/nbgrader/tests/apps/files/autotest-multi.ipynb b/nbgrader/tests/apps/files/autotest-multi.ipynb deleted file mode 100644 index 7488e64df..000000000 --- a/nbgrader/tests/apps/files/autotest-multi.ipynb +++ /dev/null @@ -1,164 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "nbgrader": { - "grade": false, - "grade_id": "soln", - "locked": false, - "schema_version": 3, - "solution": true - } - }, - "outputs": [], - "source": [ - "### BEGIN SOLUTION\n", - "a = 5\n", - "b = \"hello\"\n", - "c = [1, 2, \"test\"]\n", - "d = {\"a\" : \"f\", 3 : [1.2, 3]}\n", - "e = (5, -3.3, \"d\")\n", - "f = False\n", - "def fun(x):\n", - " if x < 0:\n", - " raise ValueError\n", - " else:\n", - " return x\n", - "### END SOLUTION" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "test_multi", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": false - } - }, - "outputs": [], - "source": [ - "# the basic tests from the simple notebook\n", - "### AUTOTEST a\n", - "### AUTOTEST b\n", - "### AUTOTEST c" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "cell-f2803ba7c42d03ab", - "locked": true, - "points": 1, - "schema_version": 3, - "solution": false, - "task": false - } - }, - "outputs": [], - "source": [ - "# multiple expressions per line\n", - "### AUTOTEST type(a); str(a); a == \"5\";\n", - "### AUTOTEST [ch for ch in b]; len(c);\n", - "### AUTOTEST a;\n", - "\n", - "# intervening regular code\n", - "print(\"hello!\")\n", - "\n", - "# differing spacings, numbers of comment characters, trailing whitespace\n", - "### AUTOTEST a \n", - "#AUTOTEST a\n", - "# AUTOTEST a\n", - "## AUTOTEST a\n", - "# AUTOTEST a\n", - "# # # # AUTOTEST a\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "cell-693350420ec62f1b", - "locked": true, - "points": 1, - "schema_version": 3, - "solution": false, - "task": false - } - }, - "outputs": [], - "source": [ - "# a few common types\n", - "### AUTOTEST a; b; c; d; e; f;" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "cell-13479eb0e5fff152", - "locked": true, - "points": 1, - "schema_version": 3, - "solution": false, - "task": false - } - }, - "outputs": [], - "source": [ - "\n", - "# a function that checks whether a function throws an error\n", - "\"\"\"Check that squares raises an error for invalid inputs\"\"\"\n", - "def test_func_throws(func, ErrorType):\n", - " try:\n", - " func()\n", - " except ErrorType:\n", - " return True\n", - " else:\n", - " print('Did not raise right type of error!')\n", - " return False\n", - "\n", - "# test a custom function\n", - "### AUTOTEST fun(3)\n", - "### AUTOTEST test_func_throws(lambda : fun(-4), ValueError)\n", - "### AUTOTEST test_func_throws(lambda : fun(0), ValueError)\n" - ] - } - ], - "metadata": { - "celltoolbar": "Create Assignment", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/nbgrader/tests/apps/files/autotest-simple-changed.ipynb b/nbgrader/tests/apps/files/autotest-simple-changed.ipynb deleted file mode 100644 index d918a0df8..000000000 --- a/nbgrader/tests/apps/files/autotest-simple-changed.ipynb +++ /dev/null @@ -1,92 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "nbgrader": { - "cell_type": "code", - "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", - "grade": false, - "grade_id": "soln", - "locked": false, - "schema_version": 3, - "solution": true - } - }, - "outputs": [], - "source": [ - "# YOUR CODE HERE\n", - "a = 5\n", - "b = \"hello\"\n", - "c = [1, 2, \"test\"]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "editable": false, - "max_height": 100, - "nbgrader": { - "cell_type": "code", - "checksum": "f1736680e92dee1b5debd87ec5790f10", - "grade": true, - "grade_id": "test_simple", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": false - } - }, - "outputs": [], - "source": [ - "from hashlib import sha1\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "assert str(type(b)) == \"\", \"type of b is not str. b should be an str\"\n", - "assert str(len(b)) == \"5\", \"length of b is not correct\"\n", - "assert str(b.lower()) == \"hello\", \"value of b is not correct\"\n", - "assert str(b) == \"hello\", \"correct string value of b but incorrect case of letters\"\n", - "\n", - "assert str(type(c)) == \"\", \"type of c is not list. c should be a list\"\n", - "assert str(len(c)) == \"3\", \"length of c is not correct\"\n", - "assert str(sorted(map(str, c))) == \"['1', '2', 'test']\", \"values of c are not correct\"\n", - "assert str(c) == \"[1, 2, 'test']\", \"order of elements of c is not correct\"\n", - "\n", - "print('Success!')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/nbgrader/tests/apps/files/autotest-simple-unchanged.ipynb b/nbgrader/tests/apps/files/autotest-simple-unchanged.ipynb deleted file mode 100644 index 214e5fbbb..000000000 --- a/nbgrader/tests/apps/files/autotest-simple-unchanged.ipynb +++ /dev/null @@ -1,90 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "nbgrader": { - "cell_type": "code", - "checksum": "e0106b7b1e000112c4ce2c8f8fa64ed1", - "grade": false, - "grade_id": "soln", - "locked": false, - "schema_version": 3, - "solution": true - } - }, - "outputs": [], - "source": [ - "# YOUR CODE HERE\n", - "raise NotImplementedError()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "deletable": false, - "editable": false, - "max_height": 100, - "nbgrader": { - "cell_type": "code", - "checksum": "f1736680e92dee1b5debd87ec5790f10", - "grade": true, - "grade_id": "test_simple", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": false - } - }, - "outputs": [], - "source": [ - "from hashlib import sha1\n", - "assert str(type(a)) == \"\", \"type of a is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()\"\n", - "assert str(a) == \"5\", \"value of a is not correct\"\n", - "\n", - "assert str(type(b)) == \"\", \"type of b is not str. b should be an str\"\n", - "assert str(len(b)) == \"5\", \"length of b is not correct\"\n", - "assert str(b.lower()) == \"hello\", \"value of b is not correct\"\n", - "assert str(b) == \"hello\", \"correct string value of b but incorrect case of letters\"\n", - "\n", - "assert str(type(c)) == \"\", \"type of c is not list. c should be a list\"\n", - "assert str(len(c)) == \"3\", \"length of c is not correct\"\n", - "assert str(sorted(map(str, c))) == \"['1', '2', 'test']\", \"values of c are not correct\"\n", - "assert str(c) == \"[1, 2, 'test']\", \"order of elements of c is not correct\"\n", - "\n", - "print('Success!')" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/nbgrader/tests/apps/files/autotest-simple.ipynb b/nbgrader/tests/apps/files/autotest-simple.ipynb deleted file mode 100644 index 42c87b071..000000000 --- a/nbgrader/tests/apps/files/autotest-simple.ipynb +++ /dev/null @@ -1,74 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Create a variable named `a` with value `5`, a variable `b` with value `\"hello\"`, and a variable `c` with value `[1, 2, \"test\"]`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": false, - "grade_id": "soln", - "locked": false, - "schema_version": 3, - "solution": true - } - }, - "outputs": [], - "source": [ - "### BEGIN SOLUTION\n", - "a = 5\n", - "b = \"hello\"\n", - "c = [1, 2, \"test\"]\n", - "### END SOLUTION" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "nbgrader": { - "grade": true, - "grade_id": "test_simple", - "locked": false, - "points": 1, - "schema_version": 3, - "solution": false - } - }, - "outputs": [], - "source": [ - "### AUTOTEST a\n", - "### AUTOTEST b\n", - "### AUTOTEST c" - ] - } - ], - "metadata": { - "celltoolbar": "Create Assignment", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.10" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/nbgrader/tests/apps/files/autotests.yml b/nbgrader/tests/apps/files/autotests.yml deleted file mode 100644 index cfd000cb5..000000000 --- a/nbgrader/tests/apps/files/autotests.yml +++ /dev/null @@ -1,310 +0,0 @@ -#kernel_name: -# setup: -# hash: -# dispatch: -# normalize: -# check: -# success: -# -# templates: -# default: -# - test: -# fail: -# -# datatype: -# - test: -# fail: - - -python3: - setup: "from hashlib import sha1" - hash: 'sha1({{snippet}}.encode("utf-8")+b"{{salt}}").hexdigest()' - dispatch: "type({{snippet}})" - normalize: "str({{snippet}})" - check: 'assert {{snippet}} == "{{value}}", "{{message}}"' - success: "print('Success!')" - - templates: - default: - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not correct" - - - test: "{{snippet}}" - fail: "value of {{snippet}} is not correct" - - int: - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()" - - - test: "{{snippet}}" - fail: "value of {{snippet}} is not correct" - - float: - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not float. Please make sure it is float and not np.float64, etc. You can cast your value into a float using float()" - - - test: "round({{snippet}}, 2)" - fail: "value of {{snippet}} is not correct (rounded to 2 decimal places)" - - set: - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not set. {{snippet}} should be a set" - - - test: "len({{snippet}})" - fail: "length of {{snippet}} is not correct" - - - test: "{{snippet}}" - fail: "value of {{snippet}} is not correct" - - list: - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not list. {{snippet}} should be a list" - - - test: "len({{snippet}})" - fail: "length of {{snippet}} is not correct" - - - test: "sorted(map(str, {{snippet}}))" - fail: "values of {{snippet}} are not correct" - - - test: "{{snippet}}" - fail: "order of elements of {{snippet}} is not correct" - - tuple: - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not tuple. {{snippet}} should be a tuple" - - - test: "len({{snippet}})" - fail: "length of {{snippet}} is not correct" - - - test: "sorted(map(str, {{snippet}}))" - fail: "values of {{snippet}} are not correct" - - - test: "{{snippet}}" - fail: "order of elements of {{snippet}} is not correct" - - str: - - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not str. {{snippet}} should be an str" - - - test: "len({{snippet}})" - fail: "length of {{snippet}} is not correct" - - - test: "{{snippet}}.lower()" - fail: "value of {{snippet}} is not correct" - - - test: "{{snippet}}" - fail: "correct string value of {{snippet}} but incorrect case of letters" - - dict: - - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not dict. {{snippet}} should be a dict" - - - test: "len(list({{snippet}}.keys()))" - fail: "number of keys of {{snippet}} is not correct" - - - test: "sorted(map(str, {{snippet}}.keys()))" - fail: "keys of {{snippet}} are not correct" - - - test: "sorted(map(str, {{snippet}}.values()))" - fail: "correct keys, but values of {{snippet}} are not correct" - - - test: "{{snippet}}" - fail: "correct keys and values, but incorrect correspondence in keys and values of {{snippet}}" - - bool: - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not bool. {{snippet}} should be a bool" - - - test: "{{snippet}}" - fail: "boolean value of {{snippet}} is not correct" - - type: - - test: "{{snippet}}" - fail: "type of {{snippet}} is not correct" - -# --------------------------------------------- - -python: - setup: "from hashlib import sha1" - hash: 'sha1({{snippet}}.encode("utf-8")+b"{{salt}}").hexdigest()' - dispatch: "type({{snippet}})" - normalize: "str({{snippet}})" - check: 'assert {{snippet}} == "{{value}}", "{{message}}"' - success: "print('Success!')" - - templates: - default: - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not correct" - - - test: "{{snippet}}" - fail: "value of {{snippet}} is not correct" - - int: - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not int. Please make sure it is int and not np.int64, etc. You can cast your value into an int using int()" - - - test: "{{snippet}}" - fail: "value of {{snippet}} is not correct" - - float: - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not float. Please make sure it is float and not np.float64, etc. You can cast your value into a float using float()" - - - test: "round({{snippet}}, 2)" - fail: "value of {{snippet}} is not correct (rounded to 2 decimal places)" - - set: - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not set. {{snippet}} should be a set" - - - test: "len({{snippet}})" - fail: "length of {{snippet}} is not correct" - - - test: "{{snippet}}" - fail: "value of {{snippet}} is not correct" - - list: - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not list. {{snippet}} should be a list" - - - test: "len({{snippet}})" - fail: "length of {{snippet}} is not correct" - - - test: "sorted(map(str, {{snippet}}))" - fail: "values of {{snippet}} are not correct" - - - test: "{{snippet}}" - fail: "order of elements of {{snippet}} is not correct" - - tuple: - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not tuple. {{snippet}} should be a tuple" - - - test: "len({{snippet}})" - fail: "length of {{snippet}} is not correct" - - - test: "sorted(map(str, {{snippet}}))" - fail: "values of {{snippet}} are not correct" - - - test: "{{snippet}}" - fail: "order of elements of {{snippet}} is not correct" - - str: - - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not str. {{snippet}} should be an str" - - - test: "len({{snippet}})" - fail: "length of {{snippet}} is not correct" - - - test: "{{snippet}}.lower()" - fail: "value of {{snippet}} is not correct" - - - test: "{{snippet}}" - fail: "correct string value of {{snippet}} but incorrect case of letters" - - dict: - - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not dict. {{snippet}} should be a dict" - - - test: "len(list({{snippet}}.keys()))" - fail: "number of keys of {{snippet}} is not correct" - - - test: "sorted(map(str, {{snippet}}.keys()))" - fail: "keys of {{snippet}} are not correct" - - - test: "sorted(map(str, {{snippet}}.values()))" - fail: "correct keys, but values of {{snippet}} are not correct" - - - test: "{{snippet}}" - fail: "correct keys and values, but incorrect correspondence in keys and values of {{snippet}}" - - bool: - - test: "type({{snippet}})" - fail: "type of {{snippet}} is not bool. {{snippet}} should be a bool" - - - test: "{{snippet}}" - fail: "boolean value of {{snippet}} is not correct" - - type: - - test: "{{snippet}}" - fail: "type of {{snippet}} is not correct" - - - -# -------------------------------------------------------------------------------------------------- -ir: - setup: 'library(digest)' - hash: 'digest(paste({{snippet}}, "{{salt}}"))' - dispatch: 'class({{snippet}})' - normalize: 'toString({{snippet}})' - check: 'stopifnot("{{message}}"= setequal({{snippet}}, "{{value}}"))' - success: "print('Success!')" - - templates: - default: - - test: "class({{snippet}})" - fail: "type of {{snippet}} is not correct" - - - test: "{{snippet}}" - fail: "value of {{snippet}} is not correct" - - integer: - - test: "class({{snippet}})" - fail: "type of {{snippet}} is not integer" - - - test: "length({{snippet}})" - fail: "length of {{snippet}} is not correct" - - - test: "sort({{snippet}})" - fail: "values of {{snippet}} are not correct" - - numeric: - - test: "class({{snippet}})" - fail: "type of {{snippet}} is not double" - - - test: "round({{snippet}}, 2)" - fail: "value of {{snippet}} is not correct (rounded to 2 decimal places)" - - - test: "length({{snippet}})" - fail: "length of {{snippet}} is not correct" - - - test: "sort({{snippet}})" - fail: "values of {{snippet}} are not correct" - - list: - - test: "class({{snippet}})" - fail: "type of {{snippet}} is not list" - - - test: "length({{snippet}})" - fail: "length of {{snippet}} is not correct" - - - test: "sort(c(names({{snippet}})))" - fail: "values of {{snippet}} names are not correct" - - - test: "{{snippet}}" - fail: "order of elements of {{snippet}} is not correct" - - character: - - test: "class({{snippet}})" - fail: "type of {{snippet}} is not list" - - - test: "length({{snippet}})" - fail: "length of {{snippet}} is not correct" - - - test: "tolower({{snippet}})" - fail: "value of {{snippet}} is not correct" - - - test: "{{snippet}}" - fail: "correct string value of {{snippet}} but incorrect case of letters" - - logical: - - test: "class({{snippet}})" - fail: "type of {{snippet}} is not logical" - - - test: "{{snippet}}" - fail: "logical value of {{snippet}} is not correct" diff --git a/nbgrader/tests/apps/files/data.txt b/nbgrader/tests/apps/files/data.txt deleted file mode 100644 index 0f8a25a9d..000000000 --- a/nbgrader/tests/apps/files/data.txt +++ /dev/null @@ -1,4 +0,0 @@ -line 1 -line 2 -line 3 -line 4 \ No newline at end of file diff --git a/nbgrader/tests/apps/files/gradebook.db b/nbgrader/tests/apps/files/gradebook.db deleted file mode 100644 index a9a3ebbb4f7fadc1fc38790e68837f57c9d5367d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35840 zcmeI5O>Y}T7{~X;v6F4x_jc2!flYm|S8`NrD^e>!;F@h}HFc=#0u_hV*4_jw_HK#S z^hGKt3GH{_#+4g44txQ6;efsw=%C&MmS1QUoPeYH$o)j)Zi!( z2oXs0ub32biQ&)j&p4L*jr*S2VJ=P9<{X&*WcP70C9bTnPZnL7#SjV1Fx^Uu7GMmi zHK4{ARHtC2he~T$YoJOE-FL)1a*5^eoiz^AbLy zbCj#=b)~vmvz0J8YEN0JD_51eQmZJpC@GF7mmw<>ZIZP-v!k#PWYGT*e5YIggT!P7 zFZetf2xjfUAkA-}{Le7F!3q2dpI|cVuk5${2LCJ=Ds~?ob`)Yy-g7c^b6acaYE$oY zaOo(Ojo3aR2SZXQNd?8B?p!+Tg@HXtuEOiRCOnpVe=kg#v=;Vaja%5^D+`%|q+xop#H>Dj!L?P}Xw zE%UySl|xxg9)rwI-|nSn>BuBY&!xo0MRu?3gxT+h|9+SX zv2XoX0VWbJJOqy$JMHr(PmF#j$RWN|DOMIoN#}f5)uLZIa#4s>q2O0Y z^FO%Rc3|>Pl$=#sN4jz>DONM1oamYT?WUWxXbiv~PQw5Hf^iNCKp=JmVE!Mwkb}cO zARqwwAE1B&5QrTCxc`q`$iZPC5D2*i$negB_eE->6X+zk6OD*S?e8RySqu_vEMiG>3DgO)1xGU0(++G3PF; zhZfW`4fURGXdUaJt0pqaXV72OE^Z!)qHzU$&(!V-r9o0xAIa! zMyunk9FHmjD-t5GQ~T78?Uj$F#7u^LT6ND1EGr`#KS7@hc%KO;!FX*b)?50k4-V)* zJ7#kWp9++A)a{(qP(EqcXF&5ay}8xvcY_nnS9<&4hn#7X#fe1aK?x}VXCE3;FZ!N; zIQX_JuYEcRGR*&DH{^rEKp-Fh_x}M3C;)-j5rF)UUC6;)4uSt zV)hF9s^7Rjuj^_?U+aX-lfpQ5`G23$ zflRJRzC^#@#DhC zqX$p3`<*7g1j4t?S5CfWBg7X#_>G7#|BpD#!EQic3JJjd{}culTmk|S2|)fw1O>YR zfhi;a_y1EERB#ChL?i&$|A?SqHy|*D1R(#XFsR@X5Qs>+R_d6dBSY`&-{}IC& Kn1MhH2>b&?oziOn diff --git a/nbgrader/tests/apps/files/infinite-loop-with-output.ipynb b/nbgrader/tests/apps/files/infinite-loop-with-output.ipynb deleted file mode 100644 index 44b2fe82b..000000000 --- a/nbgrader/tests/apps/files/infinite-loop-with-output.ipynb +++ /dev/null @@ -1,26 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "x = []\n", - "while True:\n", - " x.append(1)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python", - "language": "python", - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/nbgrader/tests/apps/files/infinite-loop.ipynb b/nbgrader/tests/apps/files/infinite-loop.ipynb deleted file mode 100644 index b32eaf15d..000000000 --- a/nbgrader/tests/apps/files/infinite-loop.ipynb +++ /dev/null @@ -1,25 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "while True:\n", - " pass" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python", - "language": "python", - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/nbgrader/tests/apps/files/jupyter.png b/nbgrader/tests/apps/files/jupyter.png deleted file mode 100644 index 9848cfb961d106ecb1fdc63173b628f6a793493c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21090 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 ACjbBd diff --git a/nbgrader/tests/apps/files/myexporter.py b/nbgrader/tests/apps/files/myexporter.py deleted file mode 100644 index 470606d05..000000000 --- a/nbgrader/tests/apps/files/myexporter.py +++ /dev/null @@ -1,6 +0,0 @@ -from nbgrader.plugins.export import ExportPlugin - -class MyExporter(ExportPlugin): - def export(self, gradebook): - with open(self.to, "w") as fh: - fh.write("hello!") diff --git a/nbgrader/tests/apps/files/notebooks.zip b/nbgrader/tests/apps/files/notebooks.zip deleted file mode 100644 index a77940f2685ca3b10b8dfcc583cd58e050b1d7b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18018 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