Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move docs from Django to website #72

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/source/contact.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
##############
Contacting Tin
##############

If you need to get in touch with the Tin team, you can email us at tin@tjhsst.edu

Alternatively, you can visit the Syslab at TJ to talk to us in person.
4 changes: 2 additions & 2 deletions docs/source/contributing/tests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ For example, if you find yourself needing to create a second student often, you


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

.. code-block:: python

Expand All @@ -192,7 +192,7 @@ anything, use it with ``pytest.usefixtures``.
assignment.is_quiz = True
assignment.save()

@pytest.usefixtures("all_assigments_quiz")
@pytest.mark.usefixtures("all_assigments_quiz")
def test_something(assignment):
# test something, but now assignment is a quiz
...
Expand Down
15 changes: 13 additions & 2 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,22 @@ Previously, teachers in TJHSST CS classes had to manually run student code.
As you can imagine, this was both time consuming, and dangerous.
In order to solve this problem, Tin was invented to safely run student code submissions.

Explore some of the technical documentation we have at our disposal!
Explore some of the documentation we have at our disposal!

.. toctree::
:maxdepth: 2
:caption: Contents:
:caption: Usage Guide

usage
contact


If you're interested in contributing a fix or a feature to Tin,
the following documents might be useful:

.. toctree::
:maxdepth: 1
:caption: Development

contributing
reference_index
Expand Down
13 changes: 13 additions & 0 deletions docs/source/usage.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#####
Usage
#####

If you're interested in writing a grader, check out
the pages below:

.. toctree::
:maxdepth: 1
:caption: Grader Documentation

usage/graders/writing_graders
usage/graders/examples
22 changes: 22 additions & 0 deletions docs/source/usage/graders/examples.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
###############
Grader Examples
###############

If you haven't already, check out :doc:`writing_graders` before
looking at some examples.

The following graders range from simple, to more sophisticated.

.. toctree::
:caption: Sample Graders
:maxdepth: 1

examples/file_io
examples/fibonacci
examples/addition

To see an example of a grader that looks for a specific function name
in a student script, see :doc:`addition <examples/addition>`.

To see an example of a grader that gives specific permissions on files,
check out :doc:`file_io <examples/file_io>`.
102 changes: 102 additions & 0 deletions docs/source/usage/graders/examples/addition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from __future__ import annotations

import importlib.util
import sys
import traceback
from collections.abc import Callable
from pathlib import Path

student_code_path: str = sys.argv[2]
username: str = sys.argv[3]
log_file = Path(sys.argv[4])

test_cases = (
(1, 2),
(3, 4),
(1000, 20345),
(54, 78),
)

secret_test_cases = (
(127, 856.7),
(789.101, 101112),
)


def import_module(modname: str = "student_submission", func_name="add_num") -> Callable:
"""Imports the student submission and returns the function with the given name.

It accomplishes this by utilizing a lot of the machinery provided by the python module
``importlib``. If you don't understand how it works, feel free to just copy paste this
function and pass a different value for the ``func_name`` parameter.
"""

spec = importlib.util.spec_from_file_location(modname, student_code_path)

# these are probably grader errors and not student errors, so we raise an
# exception instead of printing
if spec is None:
raise ImportError(f"Could not load spec for module {student_code_path!r}")
if spec.loader is None:
raise ImportError(f"No loader found for module {student_code_path!r} with {spec=!r}")

submission = importlib.util.module_from_spec(spec)

if submission is None:
raise ImportError("Module spec is None")

sys.modules[modname] = submission

try:
spec.loader.exec_module(submission)
except Exception:
# this traceback could provide sensitive information, so we don't provide it to students
print("Could not test submission, an exception was raised while initializing.")
log_file.write_text(f"Student {username} import error:\n\n" + traceback.format_exc())
# it's not our fault so we exit 0
sys.exit(0)

try:
func = getattr(submission, func_name)
except AttributeError:
print(f"Could not find function {func_name!r}")
sys.exit(0)

return func


def run_submission(func: Callable) -> None:
# grade submissions
failing_cases = 0
tol = 1e-8
for x, y in test_cases:
try:
# take into account floating point error
if func(x, y) - (x + y) > tol:
print(f"Failed on test case {x=},{y=}")
failing_cases += 1
except Exception:
print(f"Code errored on test case {x=},{y=}")
failing_cases += 1

for idx, (x, y) in enumerate(secret_test_cases):
try:
if func(x, y) - (x + y) > tol:
print(f"Failed on secret test case {idx}")
failing_cases += 1
except Exception:
print(f"Code errored on secret test case {idx}")
failing_cases += 1

raw = 1 - failing_cases / (len(test_cases) + len(secret_test_cases))
# print score, rounding to two decimal places
print(f"Score: {raw * 100:.2f}%")


def main() -> None:
submission = import_module()
run_submission(submission)


if __name__ == "__main__":
main()
26 changes: 26 additions & 0 deletions docs/source/usage/graders/examples/addition.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
###########
Add Numbers
###########

----------
Assignment
----------
Write a program that has a function called ``add_num`` that takes two parameters
:math:`x` and :math:`y`, and returns their sum :math:`x+y`.


----------------
Example Solution
----------------

.. code-block:: python

def add_num(x, y):
return x + y


--------------
Example Grader
--------------

.. literalinclude:: addition.py
66 changes: 66 additions & 0 deletions docs/source/usage/graders/examples/fibonacci.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from __future__ import annotations

import subprocess
import sys

# this assignment is out of 100 points
N = 100
score = 0
failing_cases = []

# set up the fibonacci sequence so that we can check student answers
cur_fib = 1
next_fib = 1

# parse information from Tin
submission, _submission_file, username, log_file, *_ = sys.argv[1:]

for i in range(1, N + 1):
try:
# pass n as an argument to the student submission
res = subprocess.run(
[sys.executable, submission, str(i)],
# it shouldn't take more than 5 seconds
timeout=5,
stdin=subprocess.DEVNULL,
capture_output=True,
check=False,
)
# the student submission is too slow
except subprocess.TimeoutExpired:
print(f"Script timeout for number {i}")
else:
# check if the script failed
if res.stderr or res.returncode != 0:
print(f"Script error for number {i}")
failing_cases.append(i)
continue

try:
stdout = res.stdout.strip().decode("utf-8")
except UnicodeDecodeError:
print(f"Non-UTF-8 output for number {i}")
failing_cases.append(i)
continue

if not stdout.isdigit():
print(f"Non-integer printed for number {i}")
failing_cases.append(i)
continue

student_ans = int(stdout)
if student_ans == cur_fib:
score += 1
else:
print(f"Invalid result for number {i} (printed {student_ans}, answer is {cur_fib})")
failing_cases.append(i)

# calculate our next fibonacci number
next_fib, cur_fib = cur_fib + next_fib, next_fib

print(f"Score: {score / N}")

with open(log_file, "a", encoding="utf-8") as logfile:
logfile.write(
f"User: {username}; Score: {score}/{N}; Failing test cases: {', '.join(str(case) for case in failing_cases)}\n"
)
31 changes: 31 additions & 0 deletions docs/source/usage/graders/examples/fibonacci.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#############
Nth Fibonacci
#############

----------
Assignment
----------
Write a program that takes an integer ``n`` and returns the nth Fibonacci number.

---------------
Sample Solution
---------------

.. code-block:: python

import sys

n = int(sys.argv[1])-1
nums = [0, 1]
while n >= len(nums):
nums.append(nums[-1] + nums[-2])
return nums[n]




--------------
Example Grader
--------------

.. literalinclude:: fibonacci.py
51 changes: 51 additions & 0 deletions docs/source/usage/graders/examples/file_io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from __future__ import annotations

import subprocess
import sys
from pathlib import Path

submission = sys.argv[1]
student_submission = Path(sys.argv[2])

# input file is in the same directory as our grader (this file)
# make sure to use the absolute path
INPUT_FILE = (Path(__file__).parent / "input.txt").resolve()

# output file is in the same directory as the student submission
# This way we can avoid multiple submissions trying to write to
# the same file.
# Again, making sure to use the absolute path
OUTPUT_FILE = (student_submission.parent / "output.txt").resolve()


command = [
sys.executable,
submission,
# give read permissions to the input
# making sure to use the absolute path to the file
"--read",
INPUT_FILE,
# and allow them to read/write to the output file
"--write",
OUTPUT_FILE,
# and then pass the arguments to the student submission
"--",
INPUT_FILE,
OUTPUT_FILE,
]

resp = subprocess.run(
command,
stdout=sys.stdout,
stderr=subprocess.STDOUT,
check=False,
)

if (
resp.returncode != 0
or not OUTPUT_FILE.exists()
or OUTPUT_FILE.read_text() != INPUT_FILE.read_text()
):
print("Score: 0%")
else:
print("Score: 100%")
Loading
Loading