Grading tests are failing
The grading tests are failing, even if the ci claims that everything is fine.
e.g. https://github.com/ucbds-infra/otter-grader/runs/3458763251#step:4:876

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a12ce945..1e5a37c8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@
 * Added `xeCJK` to the LaTeX exporter template and Docker images
 * Added PDF generation and submission in R per [#302](https://github.com/ucbds-infra/otter-grader/issues/302)
 * Updated intercell seeding in R per [#302](https://github.com/ucbds-infra/otter-grader/issues/302)
+* Fixed bug in test case point values for Assign R assignments per [#360](https://github.com/ucbds-infra/otter-grader/issues/360)
+* Enabled `solutions_pdf` and `template_pdf` in Assign R assignments per [#364](https://github.com/ucbds-infra/otter-grader/issues/364)
 
 **v3.0.6:** re-release of v3.0.5
 
diff --git a/otter/assign/__init__.py b/otter/assign/__init__.py
index f16d4b1c..ac258ac9 100644
--- a/otter/assign/__init__.py
+++ b/otter/assign/__init__.py
@@ -11,7 +11,7 @@ from .utils import run_tests, write_otter_config_file, run_generate_autograder
 from ..export import export_notebook
 from ..export.exporters import WkhtmltopdfNotFoundError
 from ..plugins import PluginCollection
-from ..utils import get_relpath, block_print
+from ..utils import block_print, get_relpath, knit_rmd_file
 
 
 def main(master, result, *, no_pdfs=False, no_run_tests=False, username=None, password=None, 
@@ -93,36 +93,55 @@ def main(master, result, *, no_pdfs=False, no_run_tests=False, username=None, pa
             run_generate_autograder(result, assignment, username, password, plugin_collection=pc)
         
         # generate PDF of solutions
-        if assignment.solutions_pdf and not assignment.is_rmd and not no_pdfs:
+        if assignment.solutions_pdf and not no_pdfs:
             print("Generating solutions PDF...")
             filtering = assignment.solutions_pdf == 'filtered'
 
-            try:
-                export_notebook(
-                    str(result / 'autograder' / master.name),
-                    dest=str(result / 'autograder' / (master.stem + '-sol.pdf')),
-                    filtering=filtering,
-                    pagebreaks=filtering,
-                    exporter_type="html",
-                )
-            except WkhtmltopdfNotFoundError:
-                export_notebook(
-                    str(result / 'autograder' / master.name),
-                    dest=str(result / 'autograder' / (master.stem + '-sol.pdf')),
-                    filtering=filtering,
-                    pagebreaks=filtering,
-                )
+            src = os.path.abspath(str(result / 'autograder' / master.name))
+            dst = os.path.abspath(str(result / 'autograder' / (master.stem + '-sol.pdf')))
+        
+            if not assignment.is_rmd:
+                try:
+                    export_notebook(
+                        src,
+                        dest=dst,
+                        filtering=filtering,
+                        pagebreaks=filtering,
+                        exporter_type="html",
+                    )
+                except WkhtmltopdfNotFoundError:
+                    export_notebook(
+                        src,
+                        dest=dst,
+                        filtering=filtering,
+                        pagebreaks=filtering,
+                    )
+
+            else:
+                if filtering:
+                    raise ValueError("Filtering is not supported with RMarkdown assignments")
+
+                knit_rmd_file(src, dst)
 
         # generate a tempalte PDF for Gradescope
-        if not assignment.is_rmd and assignment.template_pdf and not no_pdfs:
+        if assignment.template_pdf and not no_pdfs:
             print("Generating template PDF...")
-            export_notebook(
-                str(result / 'autograder' / master.name),
-                dest=str(result / 'autograder' / (master.stem + '-template.pdf')), 
-                filtering=True, 
-                pagebreaks=True, 
-                exporter_type="latex",
-            )
+            
+            src = os.path.abspath(str(result / 'autograder' / master.name))
+            dst = os.path.abspath(str(result / 'autograder' / (master.stem + '-template.pdf')))
+
+            if not assignment.is_rmd:
+                export_notebook(
+                    src,
+                    dest=dst, 
+                    filtering=True, 
+                    pagebreaks=True, 
+                    exporter_type="latex",
+                )
+
+            else:
+                raise ValueError(f"Filtering is not supported with RMarkdown assignments; use " + \
+                    "solutions_pdf to generate a Gradescope template instead.")
 
         # generate the .otter file if needed
         if not assignment.is_rmd and assignment.save_environment:
diff --git a/otter/assign/constants.py b/otter/assign/constants.py
index 4436d1f4..a8754974 100644
--- a/otter/assign/constants.py
+++ b/otter/assign/constants.py
@@ -31,7 +31,7 @@ test = list(
     ottr::TestCase$new(
       hidden = {% if tc.hidden %}TRUE{% else %}FALSE{% endif %},
       name = {% if tc.name %}"{{ tc.name }}"{% else %}NA{% endif %},
-      points = {{ tc.points }},{% if tc.success_message %}
+      points = {% if tc.points is none %}NA{% else %}{{ tc.points }}{% endif %},{% if tc.success_message %}
       success_message = "{{ tc.success_message }}",{% endif %}{% if tc.failure_message %}
       failure_message = "{{ tc.failure_message }}",{% endif %}
       code = {
diff --git a/otter/assign/r_adapter/tests.py b/otter/assign/r_adapter/tests.py
index a1922198..b38c4ab1 100644
--- a/otter/assign/r_adapter/tests.py
+++ b/otter/assign/r_adapter/tests.py
@@ -9,12 +9,17 @@ import nbformat
 
 from collections import namedtuple
 
-from ..constants import BEGIN_TEST_CONFIG_REGEX, END_TEST_CONFIG_REGEX, OTTR_TEST_FILE_TEMPLATE, OTTR_TEST_NAME_REGEX, TEST_REGEX
+from ..constants import BEGIN_TEST_CONFIG_REGEX, END_TEST_CONFIG_REGEX, OTTR_TEST_FILE_TEMPLATE, \
+    OTTR_TEST_NAME_REGEX, TEST_REGEX
 from ..tests import write_test
 from ..utils import get_source, lock
 
+from ...test_files.abstract_test import TestFile
+
+
 Test = namedtuple('Test', ['name', 'hidden', 'points', 'body', 'success_message', 'failure_message'])
 
+
 def read_test(cell, question, assignment, rmd=False):
     """
     Returns the contents of a test as a ``(name, hidden, body)`` named tuple
@@ -58,15 +63,8 @@ def read_test(cell, question, assignment, rmd=False):
     success_message = config.get("success_message", None)
     failure_message = config.get("failure_message", None)
 
-    # for line in lines:
-    #     match = re.match(OTTR_TEST_NAME_REGEX, line)
-    #     if match:
-    #         test_name = match.group(1)
-    #         break
-    # assert test_name is not None, f"Could not parse test name:\n{cell}"
-    # TODO: hook up success_message and failure_message
-    # TODO: add parsing for TEST CONFIG blocks
-    return Test(test_name, hidden, None, '\n'.join(source[i+1:]), success_message, failure_message)
+    return Test(test_name, hidden, points, '\n'.join(source[i+1:]), success_message, failure_message)
+
 
 def gen_test_cell(question, tests, tests_dict, assignment):
     """
@@ -86,25 +84,25 @@ def gen_test_cell(question, tests, tests_dict, assignment):
     cell = nbformat.v4.new_code_cell()
     cell.source = ['. = ottr::check("tests/{}.R")'.format(question['name'])]
 
-    points = question.get('points', len(tests))
-    if points is None:
-        points = 1
-    if isinstance(points, (int, float)):
-        if points % len(tests) == 0:
-            points = [points // len(tests) for _ in range(len(tests))]
-        else:
-            points = [points / len(tests) for _ in range(len(tests))]
-    assert isinstance(points, list) and len(points) == len(tests), \
-        f"Points for question {question['name']} could not be parsed:\n{points}"
-
-    # update point values
-    tests = [tc._replace(points=p) for tc, p in zip(tests, points)]
+    points = question.get('points', None)
+    if isinstance(points, dict):
+        points = points.get('each', 1) * len(tests)
+    elif isinstance(points, list):
+        if len(points) != len(tests):
+            raise ValueError(
+                f"Error in question {question['name']}: length of 'points' is {len(points)} but there "
+                f"are {len(tests)} tests")
+
+    # check for errors in resolving points
+    tests = TestFile.resolve_test_file_points(points, tests)
+
     test = gen_suite(question['name'], tests, points)
 
     tests_dict[question['name']] = test
     lock(cell)
     return cell
 
+
 def gen_suite(name, tests, points):
     """
     Generates an R-formatted test file for ottr
@@ -120,6 +118,7 @@ def gen_suite(name, tests, points):
     template_data = {'name': name, 'test_cases': tests}
     return OTTR_TEST_FILE_TEMPLATE.render(**template_data)
 
+
 def remove_hidden_tests_from_dir(nb, test_dir, assignment, use_files=True):
     """
     Rewrites test files in a directory to remove hidden tests
diff --git a/otter/assign/rmarkdown_adapter/tests.py b/otter/assign/rmarkdown_adapter/tests.py
index ec8edbd4..85797451 100644
--- a/otter/assign/rmarkdown_adapter/tests.py
+++ b/otter/assign/rmarkdown_adapter/tests.py
@@ -6,6 +6,8 @@ from ..constants import TEST_REGEX
 from ..r_adapter.tests import gen_suite
 from ..utils import get_source
 
+from ...test_files.abstract_test import TestFile
+
 
 def is_test_cell(cell):
     """
@@ -41,17 +43,18 @@ def gen_test_cell(question, tests, tests_dict, assignment):
     source = f'```{{r}}\n. = ottr::check("tests/{question["name"]}.R")\n```'
     cell = create_cell("code", source)
 
-    points = question.get('points', len(tests))
-    if isinstance(points, (int, float)):
-        if points % len(tests) == 0:
-            points = [points // len(tests) for _ in range(len(tests))]
-        else:
-            points = [points / len(tests) for _ in range(len(tests))]
-    assert isinstance(points, list) and len(points) == len(tests), \
-        f"Points for question {question['name']} could not be parsed:\n{points}"
-
-    # update point values
-    tests = [tc._replace(points=p) for tc, p in zip(tests, points)]
+    points = question.get('points', None)
+    if isinstance(points, dict):
+        points = points.get('each', 1) * len(tests)
+    elif isinstance(points, list):
+        if len(points) != len(tests):
+            raise ValueError(
+                f"Error in question {question['name']}: length of 'points' is {len(points)} but there "
+                f"are {len(tests)} tests")
+
+    # check for errors in resolving points
+    tests = TestFile.resolve_test_file_points(points, tests)
+
     test = gen_suite(question['name'], tests, points)
 
     tests_dict[question['name']] = test
diff --git a/otter/assign/tests.py b/otter/assign/tests.py
index ad8c8d2b..dafa8d73 100644
--- a/otter/assign/tests.py
+++ b/otter/assign/tests.py
@@ -105,8 +105,7 @@ def gen_test_cell(question, tests, tests_dict, assignment):
         if len(points) != len(tests):
             raise ValueError(
                 f"Error in question {question['name']}: length of 'points' is {len(points)} but there "
-                f"are {len(tests)} tests"
-            )
+                f"are {len(tests)} tests")
 
     # check for errors in resolving points
     TestFile.resolve_test_file_points(points, tests)
diff --git a/otter/run/run_autograder/runners/r_runner.py b/otter/run/run_autograder/runners/r_runner.py
index 974e822a..a1226191 100644
--- a/otter/run/run_autograder/runners/r_runner.py
+++ b/otter/run/run_autograder/runners/r_runner.py
@@ -15,7 +15,7 @@ from ..utils import OtterRuntimeError
 from ....export import export_notebook
 from ....generate.token import APIClient
 from ....test_files import GradingResults
-from ....utils import chdir
+from ....utils import chdir, knit_rmd_file
 
 
 NBFORMAT_VERSION = 4
@@ -124,7 +124,7 @@ class RRunner(AbstractLanguageRunner):
                     pagebreaks=self.options["pagebreaks"], exporter_type="latex")
 
             else:
-                r(f"rmarkdown::render('{subm_path}', 'pdf_document', '{pdf_path}')")
+                knit_rmd_file(subm_path, pdf_path)
 
         except Exception as e:
             print(f"\n\nError encountered while generating and submitting PDF:\n{e}")
diff --git a/otter/utils.py b/otter/utils.py
index cac3de09..857b5a63 100644
--- a/otter/utils.py
+++ b/otter/utils.py
@@ -7,6 +7,7 @@ import random
 import re
 import string
 import shutil
+import tempfile
 
 from contextlib import contextmanager, redirect_stdout
 from IPython import get_ipython
@@ -286,3 +287,25 @@ def assert_path_exists(path_tuples):
             raise ValueError(f"Path {path} is not a directory")
         if is_dir is False and not os.path.isfile(path):
             raise ValueError(f"Path {path} is not a file")
+
+
+def knit_rmd_file(rmd_path, pdf_path):
+    """
+    Use ``rpy2`` and ``rmarkdown::render`` to knit an RMarkdown file to a PDF, allowing errors.
+
+    Args:
+        rmd_path (``str``): the path to the Rmd file
+        pdf_path (``str``): the path at which to write the PDF
+    """
+    from rpy2.robjects import r
+
+    with tempfile.NamedTemporaryFile(mode="w", suffix=".Rmd",) as ntf:
+        with open(rmd_path) as f:
+            contents = f.read()
+
+        contents = "```{r cache = F, include = F}\nknitr::opts_chunk$set(error = TRUE)\n```\n" + contents
+        ntf.write(contents)
+        ntf.seek(0)
+
+        pdf_path = os.path.abspath(pdf_path)
+        r(f"""rmarkdown::render("{ntf.name}", "pdf_document", "{pdf_path}")""")
diff --git a/test/test-assign/rmd-autograder-correct/tests/q8.R b/test/test-assign/rmd-autograder-correct/tests/q8.R
index 9cb64de0..7d923ef6 100644
--- a/test/test-assign/rmd-autograder-correct/tests/q8.R
+++ b/test/test-assign/rmd-autograder-correct/tests/q8.R
@@ -4,7 +4,7 @@ test = list(
     ottr::TestCase$new(
       hidden = FALSE,
       name = NA,
-      points = 1,
+      points = 0.5,
       code = {
         test_that("q8a", {
             expect_equal(length(z), 10)
@@ -14,7 +14,7 @@ test = list(
     ottr::TestCase$new(
       hidden = TRUE,
       name = NA,
-      points = 1,
+      points = 0.5,
       code = {
         test_that("q8b", {
             actual = c(
diff --git a/test/test-assign/rmd-correct/autograder/autograder.zip b/test/test-assign/rmd-correct/autograder/autograder.zip
index f58482b4..1dfd1905 100644
Binary files a/test/test-assign/rmd-correct/autograder/autograder.zip and b/test/test-assign/rmd-correct/autograder/autograder.zip differ
diff --git a/test/test-assign/rmd-correct/autograder/rmd-example-sol.pdf b/test/test-assign/rmd-correct/autograder/rmd-example-sol.pdf
new file mode 100644
index 00000000..419555e5
Binary files /dev/null and b/test/test-assign/rmd-correct/autograder/rmd-example-sol.pdf differ
diff --git a/test/test-assign/rmd-correct/autograder/tests/q8.R b/test/test-assign/rmd-correct/autograder/tests/q8.R
index 9cb64de0..7d923ef6 100644
--- a/test/test-assign/rmd-correct/autograder/tests/q8.R
+++ b/test/test-assign/rmd-correct/autograder/tests/q8.R
@@ -4,7 +4,7 @@ test = list(
     ottr::TestCase$new(
       hidden = FALSE,
       name = NA,
-      points = 1,
+      points = 0.5,
       code = {
         test_that("q8a", {
             expect_equal(length(z), 10)
@@ -14,7 +14,7 @@ test = list(
     ottr::TestCase$new(
       hidden = TRUE,
       name = NA,
-      points = 1,
+      points = 0.5,
       code = {
         test_that("q8b", {
             actual = c(
diff --git a/test/test-assign/rmd-correct/student/tests/q8.R b/test/test-assign/rmd-correct/student/tests/q8.R
index cedd2099..4b0706f8 100644
--- a/test/test-assign/rmd-correct/student/tests/q8.R
+++ b/test/test-assign/rmd-correct/student/tests/q8.R
@@ -4,7 +4,7 @@ test = list(
     ottr::TestCase$new(
       hidden = FALSE,
       name = NA,
-      points = 1,
+      points = 0.5,
       code = {
         test_that("q8a", {
             expect_equal(length(z), 10)
diff --git a/test/test-assign/rmd-example.Rmd b/test/test-assign/rmd-example.Rmd
index 5dc3b939..4063318f 100644
--- a/test/test-assign/rmd-example.Rmd
+++ b/test/test-assign/rmd-example.Rmd
@@ -8,7 +8,6 @@ output: pdf_document
 ```
 BEGIN ASSIGNMENT
 generate: true
-template_pdf: true
 solutions_pdf: true
 files:
     - data.csv
diff --git a/test/test_assign.py b/test/test_assign.py
index ba4ca835..5436303c 100644
--- a/test/test_assign.py
+++ b/test/test_assign.py
@@ -93,7 +93,8 @@ class TestAssign(TestCase):
         """
         assign(TEST_FILES_PATH + "rmd-example.Rmd", TEST_FILES_PATH + "output", v1=True)
 
-        self.assertDirsEqual(TEST_FILES_PATH + "output", TEST_FILES_PATH + "rmd-correct", ignore_ext=[".zip"])
+        self.assertDirsEqual(TEST_FILES_PATH + "output", TEST_FILES_PATH + "rmd-correct", 
+            ignore_ext=[".zip", ".pdf"], ignore_dirs=["rmd-example-sol_files"])
         
         # check gradescope zip file
         self.check_gradescope_zipfile(
['otter/run/run_autograder/runners/r_runner.py', 'test/test-assign/rmd-example.Rmd', 'otter/assign/__init__.py', 'test/test_assign.py', 'otter/assign/tests.py', 'otter/assign/rmarkdown_adapter/tests.py', 'CHANGELOG.md', 'otter/assign/constants.py', 'otter/assign/r_adapter/tests.py', 'otter/utils.py', 'test/test-assign/rmd-correct/autograder/tests/q8.R', 'test/test-assign/rmd-correct/student/tests/q8.R', 'test/test-assign/rmd-autograder-correct/tests/q8.R']