Skip to content

Commit 8b15637

Browse files
Move docs from Django to website (#72)
Since we have our [website](https://tjcsl.github.io/tin) there's really no reason to host docs on Tin itself anymore. I did try to improve the docs wherever possible, especially by adding more examples of graders. Co-authored-by: Krishnan Shankar <[email protected]>
1 parent 797ae74 commit 8b15637

26 files changed

+537
-310
lines changed

docs/source/contact.rst

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
##############
2+
Contacting Tin
3+
##############
4+
5+
If you need to get in touch with the Tin team, you can email us at [email protected]
6+
7+
Alternatively, you can visit the Syslab at TJ to talk to us in person.

docs/source/contributing/tests.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ For example, if you find yourself needing to create a second student often, you
183183

184184

185185
If a fixture only sets up something, and does not return
186-
anything, use it with ``pytest.usefixtures``.
186+
anything, use it with ``pytest.mark.usefixtures``.
187187

188188
.. code-block:: python
189189
@@ -192,7 +192,7 @@ anything, use it with ``pytest.usefixtures``.
192192
assignment.is_quiz = True
193193
assignment.save()
194194
195-
@pytest.usefixtures("all_assigments_quiz")
195+
@pytest.mark.usefixtures("all_assigments_quiz")
196196
def test_something(assignment):
197197
# test something, but now assignment is a quiz
198198
...

docs/source/index.rst

+13-2
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,22 @@ Previously, teachers in TJHSST CS classes had to manually run student code.
1515
As you can imagine, this was both time consuming, and dangerous.
1616
In order to solve this problem, Tin was invented to safely run student code submissions.
1717

18-
Explore some of the technical documentation we have at our disposal!
18+
Explore some of the documentation we have at our disposal!
1919

2020
.. toctree::
2121
:maxdepth: 2
22-
:caption: Contents:
22+
:caption: Usage Guide
23+
24+
usage
25+
contact
26+
27+
28+
If you're interested in contributing a fix or a feature to Tin,
29+
the following documents might be useful:
30+
31+
.. toctree::
32+
:maxdepth: 1
33+
:caption: Development
2334

2435
contributing
2536
reference_index

docs/source/usage.rst

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#####
2+
Usage
3+
#####
4+
5+
If you're interested in writing a grader, check out
6+
the pages below:
7+
8+
.. toctree::
9+
:maxdepth: 1
10+
:caption: Grader Documentation
11+
12+
usage/graders/writing_graders
13+
usage/graders/examples
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
###############
2+
Grader Examples
3+
###############
4+
5+
If you haven't already, check out :doc:`writing_graders` before
6+
looking at some examples.
7+
8+
The following graders range from simple, to more sophisticated.
9+
10+
.. toctree::
11+
:caption: Sample Graders
12+
:maxdepth: 1
13+
14+
examples/file_io
15+
examples/fibonacci
16+
examples/addition
17+
18+
To see an example of a grader that looks for a specific function name
19+
in a student script, see :doc:`addition <examples/addition>`.
20+
21+
To see an example of a grader that gives specific permissions on files,
22+
check out :doc:`file_io <examples/file_io>`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from __future__ import annotations
2+
3+
import importlib.util
4+
import sys
5+
import traceback
6+
from collections.abc import Callable
7+
from pathlib import Path
8+
9+
student_code_path: str = sys.argv[2]
10+
username: str = sys.argv[3]
11+
log_file = Path(sys.argv[4])
12+
13+
test_cases = (
14+
(1, 2),
15+
(3, 4),
16+
(1000, 20345),
17+
(54, 78),
18+
)
19+
20+
secret_test_cases = (
21+
(127, 856.7),
22+
(789.101, 101112),
23+
)
24+
25+
26+
def import_module(modname: str = "student_submission", func_name="add_num") -> Callable:
27+
"""Imports the student submission and returns the function with the given name.
28+
29+
It accomplishes this by utilizing a lot of the machinery provided by the python module
30+
``importlib``. If you don't understand how it works, feel free to just copy paste this
31+
function and pass a different value for the ``func_name`` parameter.
32+
"""
33+
34+
spec = importlib.util.spec_from_file_location(modname, student_code_path)
35+
36+
# these are probably grader errors and not student errors, so we raise an
37+
# exception instead of printing
38+
if spec is None:
39+
raise ImportError(f"Could not load spec for module {student_code_path!r}")
40+
if spec.loader is None:
41+
raise ImportError(f"No loader found for module {student_code_path!r} with {spec=!r}")
42+
43+
submission = importlib.util.module_from_spec(spec)
44+
45+
if submission is None:
46+
raise ImportError("Module spec is None")
47+
48+
sys.modules[modname] = submission
49+
50+
try:
51+
spec.loader.exec_module(submission)
52+
except Exception:
53+
# this traceback could provide sensitive information, so we don't provide it to students
54+
print("Could not test submission, an exception was raised while initializing.")
55+
log_file.write_text(f"Student {username} import error:\n\n" + traceback.format_exc())
56+
# it's not our fault so we exit 0
57+
sys.exit(0)
58+
59+
try:
60+
func = getattr(submission, func_name)
61+
except AttributeError:
62+
print(f"Could not find function {func_name!r}")
63+
sys.exit(0)
64+
65+
return func
66+
67+
68+
def run_submission(func: Callable) -> None:
69+
# grade submissions
70+
failing_cases = 0
71+
tol = 1e-8
72+
for x, y in test_cases:
73+
try:
74+
# take into account floating point error
75+
if func(x, y) - (x + y) > tol:
76+
print(f"Failed on test case {x=},{y=}")
77+
failing_cases += 1
78+
except Exception:
79+
print(f"Code errored on test case {x=},{y=}")
80+
failing_cases += 1
81+
82+
for idx, (x, y) in enumerate(secret_test_cases):
83+
try:
84+
if func(x, y) - (x + y) > tol:
85+
print(f"Failed on secret test case {idx}")
86+
failing_cases += 1
87+
except Exception:
88+
print(f"Code errored on secret test case {idx}")
89+
failing_cases += 1
90+
91+
raw = 1 - failing_cases / (len(test_cases) + len(secret_test_cases))
92+
# print score, rounding to two decimal places
93+
print(f"Score: {raw * 100:.2f}%")
94+
95+
96+
def main() -> None:
97+
submission = import_module()
98+
run_submission(submission)
99+
100+
101+
if __name__ == "__main__":
102+
main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
###########
2+
Add Numbers
3+
###########
4+
5+
----------
6+
Assignment
7+
----------
8+
Write a program that has a function called ``add_num`` that takes two parameters
9+
:math:`x` and :math:`y`, and returns their sum :math:`x+y`.
10+
11+
12+
----------------
13+
Example Solution
14+
----------------
15+
16+
.. code-block:: python
17+
18+
def add_num(x, y):
19+
return x + y
20+
21+
22+
--------------
23+
Example Grader
24+
--------------
25+
26+
.. literalinclude:: addition.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from __future__ import annotations
2+
3+
import subprocess
4+
import sys
5+
6+
# this assignment is out of 100 points
7+
N = 100
8+
score = 0
9+
failing_cases = []
10+
11+
# set up the fibonacci sequence so that we can check student answers
12+
cur_fib = 1
13+
next_fib = 1
14+
15+
# parse information from Tin
16+
submission, _submission_file, username, log_file, *_ = sys.argv[1:]
17+
18+
for i in range(1, N + 1):
19+
try:
20+
# pass n as an argument to the student submission
21+
res = subprocess.run(
22+
[sys.executable, submission, str(i)],
23+
# it shouldn't take more than 5 seconds
24+
timeout=5,
25+
stdin=subprocess.DEVNULL,
26+
capture_output=True,
27+
check=False,
28+
)
29+
# the student submission is too slow
30+
except subprocess.TimeoutExpired:
31+
print(f"Script timeout for number {i}")
32+
else:
33+
# check if the script failed
34+
if res.stderr or res.returncode != 0:
35+
print(f"Script error for number {i}")
36+
failing_cases.append(i)
37+
continue
38+
39+
try:
40+
stdout = res.stdout.strip().decode("utf-8")
41+
except UnicodeDecodeError:
42+
print(f"Non-UTF-8 output for number {i}")
43+
failing_cases.append(i)
44+
continue
45+
46+
if not stdout.isdigit():
47+
print(f"Non-integer printed for number {i}")
48+
failing_cases.append(i)
49+
continue
50+
51+
student_ans = int(stdout)
52+
if student_ans == cur_fib:
53+
score += 1
54+
else:
55+
print(f"Invalid result for number {i} (printed {student_ans}, answer is {cur_fib})")
56+
failing_cases.append(i)
57+
58+
# calculate our next fibonacci number
59+
next_fib, cur_fib = cur_fib + next_fib, next_fib
60+
61+
print(f"Score: {score / N}")
62+
63+
with open(log_file, "a", encoding="utf-8") as logfile:
64+
logfile.write(
65+
f"User: {username}; Score: {score}/{N}; Failing test cases: {', '.join(str(case) for case in failing_cases)}\n"
66+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#############
2+
Nth Fibonacci
3+
#############
4+
5+
----------
6+
Assignment
7+
----------
8+
Write a program that takes an integer ``n`` and returns the nth Fibonacci number.
9+
10+
---------------
11+
Sample Solution
12+
---------------
13+
14+
.. code-block:: python
15+
16+
import sys
17+
18+
n = int(sys.argv[1])-1
19+
nums = [0, 1]
20+
while n >= len(nums):
21+
nums.append(nums[-1] + nums[-2])
22+
return nums[n]
23+
24+
25+
26+
27+
--------------
28+
Example Grader
29+
--------------
30+
31+
.. literalinclude:: fibonacci.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from __future__ import annotations
2+
3+
import subprocess
4+
import sys
5+
import tempfile
6+
from pathlib import Path
7+
8+
submission = sys.argv[1]
9+
student_submission = Path(sys.argv[2])
10+
11+
# input file is in the same directory as our grader (this file)
12+
# make sure to use the absolute path
13+
INPUT_FILE = (Path(__file__).parent / "input.txt").resolve()
14+
15+
# note that since multiple submissions may be running at the same time
16+
# we should make sure to use a filename that's not already in use
17+
# to prevent different submissions from trying to access the same file.
18+
with tempfile.NamedTemporaryFile(dir=student_submission.parent) as f:
19+
command = [
20+
sys.executable,
21+
submission,
22+
# give read permissions to the input
23+
# making sure to use the absolute path to the file
24+
"--read",
25+
INPUT_FILE,
26+
# and allow them to read/write to the output file
27+
"--write",
28+
f.name,
29+
# and then pass the arguments to the student submission
30+
"--",
31+
INPUT_FILE,
32+
f.name,
33+
]
34+
35+
resp = subprocess.run(
36+
command,
37+
stdout=sys.stdout,
38+
stderr=subprocess.STDOUT,
39+
check=False,
40+
)
41+
42+
if resp.returncode != 0 or Path(f.name).read_text() != INPUT_FILE.read_text():
43+
print("Score: 0%")
44+
else:
45+
print("Score: 100%")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#######
2+
File IO
3+
#######
4+
5+
6+
----------
7+
Assignment
8+
----------
9+
Read from an input file and write the content to an output file.
10+
11+
12+
---------------
13+
Sample Solution
14+
---------------
15+
16+
.. code-block:: python
17+
18+
import sys
19+
20+
inp = sys.argv[1]
21+
out = sys.argv[2]
22+
with (
23+
open(inp, 'r') as f,
24+
open(out, 'w') as w
25+
):
26+
w.write(f.read())
27+
28+
-------------
29+
Sample Grader
30+
-------------
31+
32+
.. literalinclude:: file_io.py

0 commit comments

Comments
 (0)