diff --git a/CHANGELOG.md b/CHANGELOG.md index 126548ebc..0dc52b1a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ **v5.2.0 (unreleased):** * Migrate installation of `ottr` from `setup.sh` to `environment.yml` with the [`r-ottr` conda-forge recipe](https://anaconda.org/conda-forge/r-ottr) +* Updated Otter Assign to allow multiline statements in test cases per [#590](https://github.com/ucbds-infra/otter-grader/issues/590) **v5.1.2:** diff --git a/otter/assign/utils.py b/otter/assign/utils.py index 453ff91a5..bbedbf79c 100644 --- a/otter/assign/utils.py +++ b/otter/assign/utils.py @@ -160,7 +160,7 @@ def remove_tag(cell, tag): return cell -def str_to_doctest(code_lines, lines): +def str_to_doctest(code_lines, lines, opens=None): """ Convert a list of lines of Python code ``code_lines`` to the doctest format and appending the results to ``lines``. @@ -174,16 +174,26 @@ def str_to_doctest(code_lines, lines): """ if len(code_lines) == 0: return lines + if opens is None: + opens = [] + in_statement = len(opens) > 0 line = code_lines.pop(0) + for c in line: + if c == "(" or c == "[" or c == "{": + opens.append(c) + elif c == ")" or c == "]" or c == "}": + opens.pop() if line.startswith(" ") or line.startswith("\t"): - return str_to_doctest(code_lines, lines + ["... " + line]) + return str_to_doctest(code_lines, lines + ["... " + line], opens=opens) elif bool(re.match(r"^except[\s\w]*:", line)) or line.startswith("elif ") or \ line.startswith("else:") or line.startswith("finally:"): - return str_to_doctest(code_lines, lines + ["... " + line]) + return str_to_doctest(code_lines, lines + ["... " + line], opens=opens) elif len(lines) > 0 and lines[-1].strip().endswith("\\"): - return str_to_doctest(code_lines, lines + ["... " + line]) + return str_to_doctest(code_lines, lines + ["... " + line], opens=opens) + elif in_statement: + return str_to_doctest(code_lines, lines + ["... " + line], opens=opens) else: - return str_to_doctest(code_lines, lines + [">>> " + line]) + return str_to_doctest(code_lines, lines + [">>> " + line], opens=opens) def run_tests(assignment, debug=False): diff --git a/test/test_assign/files/example-autograder-correct/tests/q1.py b/test/test_assign/files/example-autograder-correct/tests/q1.py index 9f1dab417..228572c48 100644 --- a/test/test_assign/files/example-autograder-correct/tests/q1.py +++ b/test/test_assign/files/example-autograder-correct/tests/q1.py @@ -2,7 +2,7 @@ test = { 'name': 'q1', 'points': None, - 'suites': [ { 'cases': [ { 'code': '>>> isinstance(x, int)\nTrue', + 'suites': [ { 'cases': [ { 'code': '>>> isinstance( x,\n... int,\n... )\nTrue', 'failure_message': 'This is not an int.', 'hidden': False, 'locked': False, diff --git a/test/test_assign/files/example-correct/autograder/example.ipynb b/test/test_assign/files/example-correct/autograder/example.ipynb index 9d5ce105c..2bd549c9a 100644 --- a/test/test_assign/files/example-correct/autograder/example.ipynb +++ b/test/test_assign/files/example-correct/autograder/example.ipynb @@ -481,7 +481,7 @@ { "cases": [ { - "code": ">>> isinstance(x, int)\nTrue", + "code": ">>> isinstance( x,\n... int,\n... )\nTrue", "failure_message": "This is not an int.", "hidden": false, "locked": false, diff --git a/test/test_assign/files/example-correct/student/example.ipynb b/test/test_assign/files/example-correct/student/example.ipynb index c1da8d382..5c4874b9d 100644 --- a/test/test_assign/files/example-correct/student/example.ipynb +++ b/test/test_assign/files/example-correct/student/example.ipynb @@ -390,7 +390,7 @@ { "cases": [ { - "code": ">>> isinstance(x, int)\nTrue", + "code": ">>> isinstance( x,\n... int,\n... )\nTrue", "failure_message": "This is not an int.", "hidden": false, "locked": false, diff --git a/test/test_assign/files/example.ipynb b/test/test_assign/files/example.ipynb index ed5c4d447..c840acaa9 100644 --- a/test/test_assign/files/example.ipynb +++ b/test/test_assign/files/example.ipynb @@ -99,7 +99,10 @@ "failure_message: This is not an int.\n", "name: q1a-1\n", "\"\"\" # END TEST CONFIG\n", - "isinstance(x, int)" + "isinstance(", + " x,\n", + " int,\n", + ")" ] }, { diff --git a/test/utils.py b/test/utils.py index ef2847d78..e545eeabd 100644 --- a/test/utils.py +++ b/test/utils.py @@ -49,7 +49,11 @@ def assert_notebooks_equal(p1, p2): # ignore cell IDs for c in [*nb1.cells, *nb2.cells]: c.pop("id", None) - assert nb1 == nb2 + diff = subprocess.run( + ["diff", "--context=5", p1, p2], + stdout=subprocess.PIPE, + ).stdout.decode("utf-8") + assert nb1 == nb2, f"Contents of {p1} did not equal contents of {p2}:\n{diff}" def assert_files_equal(p1, p2, ignore_trailing_whitespace=True):