bpa2-grader.tar
Automated grading of programming assignments.
import os, os.path, sys
import logging, threading, subprocess, itertools, collections
import signal
from contextlib import contextmanager
__author__= David Menendez
__version__ = 3.0.4
logger = logging.getLogger(__name__)
NORMAL, EXTRA, USER = range(3)
category_names = [Regular credit, Extra credit, Personal (not graded)]
class Error(Exception):
def report(self, ctx):
print()
print(ctx + :, *self.args)
class CommandError(Error):
def __init__(self, cmd, code, out=None):
self.cmd = cmd
self.code = code
self.out = out
def report(self, ctx):
print()
print(f'{ctx}: error running {self.cmd[0]!r} (return code {self.code}))
if len(self.cmd) > 1:
print(arguments, self.cmd[1:])
if self.out is not None:
print(self.out)
# TODO: option to run non-silently
def run_command(cmd):
Execute a command without a timeout. Useful for calling make.
logger.debug(Running %s, cmd)
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding=latin-1)
(out,err) = p.communicate()
if out:
logger.debug(Response
%s, out)
if p.returncode != 0:
raise CommandError(cmd, p.returncode, out)
return out
#
class TestReporter:
def __init__(self, **kws):
self.requested_tests = 0
self.completed_tests = 0
self.failures = 0
self.errors = 0
self.points = 0
self.score = 0
self.show_successes = kws.get(show_successes, False)
self.show_comments = kws.get(show_comments, True)
self.show_input = kws.get(show_input, True)
self.show_output = kws.get(show_output, True)
self.show_status = kws.get(show_status, True)
self.bar_visible= False
def clear_bar(self):
if self.bar_visible:
sys.stderr.write(r)
sys.stderr.write( * 80)
sys.stderr.write(r)
self.bar_visible = False
def set_status(self, status_msg):
if self.show_status:
sys.stderr.write(r)
sys.stderr.write(status_msg)
sys.stderr.write( * (80 len(status_msg)))
self.bar_visible = True
else:
print(status_msg)
def message(self, msg):
self.clear_bar()
print()
print(msg)
def begin_test(self, crnt_test):
self.crnt_test = crnt_test
self.refresh()
def refresh(self):
if self.show_status:
if self.errors:
msg = frCompleted {self.completed_tests} of {self.requested_tests}. Failures {self.failures}. Errors {self.errors}.
else:
msg = frCompleted {self.completed_tests} of {self.requested_tests}. Failures {self.failures}.
sys.stderr.write(msg)
self.bar_visible = True
reporter = None
def get_reporter():
global reporter
if reporter is None:
reporter = TestReporter()
return reporter
#
class Test:
time_limit = 30
output_limit = 16*1024
error_limit= 5
encoding = latin-1# less vulnerable to student bugs than ASCII
def __init__(self, cmd, dir = None, group = , weight = 1, category = NORMAL, ref_code = 0):
if not cmd:
raise ValueError(fAttempt to create {type(self)} with empty command)
self.dir = dir
self.cmd = cmd
self.group = group
self.weight = weight
self.category = category
self.ref_code = ref_code
def run(self):
Perform the test and report the number of successes.
logger.debug(Running %s: %s, self.group, self.cmd)
self.summary =
self.comments = []
self.prepare()
p = subprocess.Popen(self.cmd,
stdin= subprocess.PIPE,
stdout = subprocess.PIPE,
stderr = subprocess.STDOUT,
encoding = self.encoding)
def cancel():
p.kill()
self.summary = timed out
timer = threading.Timer(self.time_limit, cancel)
try:
self.handle_stdin(p.stdin)
timer.start()
out = p.stdout.read(self.output_limit)
if p.stdout.read(1):
p.kill()
self.summary = exceeded output limit
# make sure we get the final exit code. if we got here, p has either closed
# stdout or been killed.
p.wait()
finally:
timer.cancel()
logger.debug(Complete. Code %s
%s, p.returncode, out)
if self.summary:
pass
elif p.returncode == self.ref_code:
self.analyze_output(out)
elif p.returncode < 0:sig = -p.returncodetry:sig = signal.Signals(sig).nameexcept ValueError:passself.summary = ‘terminated by signal ‘ + sigself.check_for_sanitizer_output(p.pid, out)else:self.summary = ‘unexpected return code: ‘ + str(p.returncode)self.check_for_sanitizer_output(p.pid, out)success = not self.summaryreporter = get_reporter()if success and reporter.show_successes:self.summary = ‘correct’if self.summary:reporter.clear_bar()print()print(f'{self.group}: {self.summary}’)print(f’ arguments {self.cmd}’)if reporter.show_comments:print()for line in self.comments:print(”, line)if reporter.show_input:self.print_input()if reporter.show_output:print()print(‘output’)print(‘—‘)print(out, end=”)print(‘—‘)del self.summarydel self.commentsreturn (success, self.weight if success else 0)def prepare(self):if self.dir is not None:logger.debug(‘Moving to %r’, self.dir)os.chdir(self.dir)def handle_stdin(self, proc_stdin):proc_stdin.close()def print_input(self):passdef analyze_output(self, out):passdef check_for_sanitizer_output(self, pid, output):”””Detect error messages from AddressSanitizer.”””keyword = f’=={pid}==’logger.debug(‘Checking for %r’, keyword)lines = iter(output.split(‘
‘))for line in lines:if line.startswith(keyword):if ‘AddressSanitizer’ in line:self.summary = ‘terminated by AddressSanitizer’breakelse: # not foundreturn# continue searching for SUMMARYfor line in lines:if line.startswith(‘SUMMARY:’):self.comments.append(line)returnclass RefTest(Test):”””Compare program output with a specified reference string.”””def __init__(self, cmd, ref, **kws):super().__init__(cmd, **kws)self.ref = refdef analyze_output(self, full_out):out = full_out.split(‘
‘, 1)[0].rstrip()if out != self.ref:self.summary = ‘incorrect output’self.comments += [‘expected: ‘ + self.ref,’received: ‘ + out]class FileRefTest(Test):”””Compare program output with a reference file.”””def __init__(self, cmd, ref_file, **kws):super().__init__(cmd, **kws)self.ref_file = ref_filedef analyze_output(self, out):try:logger.debug(‘Opening reference file %r’, self.ref_file)self.comments.append(‘reference file: ‘ + repr(self.ref_file))reflines = open(self.ref_file).read().rstrip().split(‘
‘)outlines = out.rstrip().split(‘
‘)logger.debug(‘out %d lines; ref %d lines’, len(outlines), len(reflines))errors = [(i,refl,outl) for (i,(refl,outl))in enumerate(zip(reflines, outlines), 1)if refl != outl]if self.error_limit and len(errors) > self.error_limit:
errs = len(errors) self.error_limit
errors = errors[:self.error_limit]
else:
errs = 0
errors = list(itertools.chain.from_iterable(
[line {:,}.format(i),
expected: + repr(refl),
received: + repr(outl)] for (i,refl,outl) in errors))
if errs:
errors.append({:,} additional errors.format(errs))
if len(reflines) < len(outlines):errors += [‘{:,} extra lines in output’.format(len(outlines) – len(reflines))]elif len(reflines) > len(outlines):
errors += [
line {:,}.format(len(outlines)+1),
expected: + repr(reflines[len(outlines)]),
received end of file]
if errors:
self.summary = incorrect output
self.comments += errors
except IOError as e:
raise Error(fUnable to open reference file {self.ref_file!r}: {e.strerror})
class InputFileTest(Test):
Test with a specified input given by input_file.
def __init__(self, cmd, input_file, **kws):
super().__init__(cmd, **kws)
self.input_file = input_file
def print_input(self):
try:
logger.debug(Opening input file %r, self.input_file)
input = open(self.input_file).read().rstrip()
print()
print(input)
print()
print(input)
print()
except IOError as e:
raise Error(Unable to open input file {}: {}.format(
self.input_file, e.strerror))
class FileTest(FileRefTest, InputFileTest):
Tests with specified input and reference files.
pass
class InputFileStdinTest(InputFileTest):
Test with a specified input given by input_file. Input file is send to the
process on stdin.
def handle_stdin(self, stdin):
try:
logger.debug(Opening input file %r, self.input_file)
self.comments.append(input file: + repr(self.input_file))
with open(self.input_file) as f:
stdin.write(f.read())
except IOError as e:
raise Error(fUnable to send input file {self.input_file!r}: {e.strerror})
finally:
stdin.close()
class StdinTest(InputFileStdinTest, FileTest):
Test with specified input and reference files. The input is is sent to the process
on stdin.
pass
#
class AbstractTestGroup:
@classmethod
def Project(cls, name, *args, **kws):
tests = cls(*args, **kws)
return Project(name, tests)
def __init__(self, id=, weight=1, name=None, category=NORMAL, make_cmd=None):
self.id = id
self.name = id if name is None else name
self.weight = weight
self.category = category
if make_cmd:
self.make_cmd = make_cmd
def get_tests(self, project, prog, build_dir, data_dir):
raise NotImplementedError
@staticmethod
def make_cmd(prog, arg):
return [prog, arg]
class StringTests(AbstractTestGroup):
Look for tests in a file named
def __init__(self, prefix=tests, suffix=.txt, **kws):
super().__init__(**kws)
self.file = prefix + (self.id or ) + suffix
Test = RefTest
def get_tests(self, project, prog, build_dir, data_dir):
test_group = project + : + self.name if self.name else project
test_file = os.path.join(data_dir, self.file)
if not os.path.exists(test_file):
logger.warning(Test file not found: %r, test_file)
return
logger.debug(Opening tests file: %r, test_file)
with open(test_file) as lines:
try:
while True:
arg = next(lines).rstrip()
ref = next(lines).rstrip()
yield self.Test(cmd= self.make_cmd(./ + prog, arg),
ref= ref,
category = self.category,
group= test_group,
weight = self.weight,
dir= build_dir)
except StopIteration:
return
class FileTests(AbstractTestGroup):
Look for pairs of test files containing reference and input data.
If id is None, they are named:
Otherwise, they are named:
def __init__(self, arg_prefix=test., ref_prefix=ref., suffix=.txt, **kws):
super().__init__(**kws)
self.suffix = suffix
if self.id:
self.arg_prefix = f'{arg_prefix}{self.id}.
self.ref_prefix = f'{ref_prefix}{self.id}.
else:
self.arg_prefix = arg_prefix
self.ref_prefix = ref_prefix
Test = FileTest
def get_tests(self, project, prog, build_dir, data_dir):
test_group = project + : + self.name if self.name else project
# gather the names of the reference files
fnames = [fname for fname in os.listdir(data_dir)
if fname.startswith(self.ref_prefix)
and fname.endswith(self.suffix)]
fnames.sort()
prog = ./ + prog
# for each reference name, find the corresponding input file
for ref_name in fnames:
# swap ref_prefix for arg_prefix
arg_name = self.arg_prefix + ref_name[len(self.ref_prefix):]
arg = os.path.join(data_dir, arg_name)
if not os.path.exists(arg):
logger.warning(Unmatched reference file: %r, ref_name)
continue
ref = os.path.join(data_dir, ref_name)
yield self.Test(cmd= self.make_cmd(prog, arg),
input_file = arg,
ref_file = ref,
category = self.category,
group= test_group,
weight = self.weight,
dir= build_dir)
class StdinFileTests(FileTests):
Test = StdinTest
@staticmethod
def make_cmd(prog, arg):
return [prog]
#
class Project:
def __init__(self, name, *groups, **kws):
self.tests = None
self.name= name
self.prog= kws.get(prog_name, self.name)
self.ready = False
# make sure groups have distinct names
groupids = collections.Counter(g.id for g in groups)
if len(groupids) < len(groups):raise ValueError(‘Duplicate test group ids for ‘ + name + ‘: ‘ +str([g for g in groupids if groupids[g] > 1]))
# separate regular and user test groups
self.groups = tuple(g for g in groups if g.category != USER)
if not self.groups:
raise ValueError(Must provide at least one test group)
self.user_groups = tuple(g for g in groups if g.category == USER)
# generate a user group if none are specified
if not self.user_groups:
user_class = kws.get(user_class, type(self.groups[0]))
if user_class is not None:
self.user_groups = ( user_class(name=0, category=USER) ,)
def has_context(self):
return hasattr(self, src_dir)
and hasattr(self, build_dir)
and hasattr(self, data_dir)
and hasattr(self, user_dir)
def set_context(self, src_dir, build_dir, data_dir, user_dir=None):
self.src_dir = src_dir
self.build_dir = build_dir
self.data_dir = data_dir
self.user_dir =
os.path.join(src_dir, tests) if user_dir is None else user_dir
def gather_tests(self, requests):
if not self.has_context():
raise Exception(Attempt to gather tests without context)
logger.info(Gathering tests for %r, self.name)
if not os.path.isdir(self.src_dir):
get_reporter().message(fNo source found for {self.name})
logger.info(Source dir not found: %r, self.src_dir)
return 0
if not os.path.isdir(self.data_dir):
raise Error(Data directory not found: + repr(self.data_dir))
# if project is requested by name, then all groups are requested
if self.name in requests:
requests = ()
self.tests = []
for group in self.groups:
if not requests or f'{self.name}:{group.name} in requests:
self.tests.extend(group.get_tests(self.name, self.prog, self.build_dir, self.data_dir))
if self.user_groups and os.path.isdir(self.user_dir):
for group in self.user_groups:
if not requests or f'{self.name}:{group.name} in requests:
self.tests.extend(group.get_tests(self.name, self.prog, self.build_dir, self.user_dir))
count = len(self.tests)
logger.info(Total tests for %s: %s, self.name, count)
return count
def prepare_build_dir(self):
Ensure that build_dir exists and contains the Makefile
if not self.tests:
return
os.makedirs(self.build_dir, exist_ok=True)
Makefile = os.path.join(self.build_dir, Makefile)
if not os.path.exists(Makefile):# TODO: option to force overwrite
logger.info(Creating Makefile: %r, Makefile)
srcpath = os.path.relpath(self.src_dir, self.build_dir)
if in srcpath:
raise Error(space in path from SRC_DIR to BUILD_DIR + repr(srcpath))
with open(Makefile, w) as f:
f.write(fSRCPATH={srcpath}
vpath %.c $(SRCPATH)
vpath %.h $(SRCPATH)
include $(SRCPATH)/Makefile
)
def clear(self):
Run make clean in the object directory
if not hasattr(self, build_dir):
raise Exception(Attempt to clear without context)
os.chdir(self.build_dir)
run_command([make, clean])
def build(self, clear=False):
Run make in the build directory
if not self.tests:
return
if not hasattr(self, build_dir):
raise Exception(Attempt to build without context)
get_reporter().set_status(fBuilding {self.name}.)
try:
#os.makedirs(self.build_dir, exist_ok=True)
os.chdir(self.build_dir)
if clear:
self.clear()
run_command([make])
if not os.path.exists(self.prog):
raise Error(executable not created: + self.prog)
self.ready = True
except Error as e:
reporter = get_reporter()
reporter.errors += 1
reporter.clear_bar()
e.report(self.name)
def get_tests(self):
return self.tests if self.ready else []
class MultiProject:
def __init__(self, *projects):
self.projects = projects
self.context = False
names = collections.Counter(p.name for p in projects)
if len(names) < len(projects):raise ValueError(‘Duplicate project names ‘ + str([p for p in names if names[p] > 1]))
def has_context(self):
return self.context
def set_context(self, src_dir, build_dir, data_dir):
for p in self.projects:
p.set_context(
os.path.join(src_dir, p.name),
os.path.join(build_dir, p.name),
os.path.join(data_dir, p.name))
self.context = True
def prepare_build_dir(self):
for p in self.projects:
p.prepare_build_dir()
def clear(self):
for p in self.projects:
p.clear()
def build(self, clear=False):
for p in self.projects:
p.build(clear)
def gather_tests(self, requests):
count = 0
for p in self.projects:
count += p.gather_tests(requests)
logger.info(Total tests: %s, count)
return count
def get_tests(self):
return itertools.chain.from_iterable(p.get_tests() for p in self.projects)
#
import time
def test_project(project, src_dir, build_dir, data_dir, fail_stop=False, requests=(), init_only=False):
Fully run tests for a project, using the specified directory roots.
reporter = get_reporter()
project.set_context(src_dir, build_dir, data_dir)
logger.debug(gather phase)
reporter.requested_tests = project.gather_tests(requests)
# TODO: filter test cases by request
if reporter.requested_tests < 1:reporter.message(‘No tests requested.’)returnlogger.debug(‘build_dir prep phase’)project.prepare_build_dir()if init_only:returnlogger.debug(‘build phase’)project.build()if fail_stop and reporter.errors:reporter.message(‘grader: abort.’)returnlogger.debug(‘test phase’)points = collections.defaultdict(collections.Counter)scores = collections.defaultdict(collections.Counter)failures = collections.defaultdict(collections.Counter)for t in project.get_tests():points[t.category][t.group] += t.weighttry:reporter.begin_test(t.group)(success, credit) = t.run()reporter.completed_tests += 1except Error as e:reporter.errors += 1reporter.clear_bar()e.report(t.group)success = Falsecredit = 0if not success:reporter.failures += 1failures[t.category][t.group] += 1if fail_stop:reporter.message(f’grader: aborting. Completed {reporter.completed_tests} of {reporter.requested_tests}.’)returnscores[t.category][t.group] += creditlogger.debug(‘report phase’)reporter.clear_bar()print()print(‘Tests performed:’, reporter.completed_tests, ‘of’, reporter.requested_tests)print(‘Tests failed: ‘, reporter.failures)if reporter.errors:print(‘Errors: ‘, reporter.errors)for category,catscores in scores.items():cat_score = 0cat_points = 0group_width = max(5, max(len(g) for g in catscores))reporter.clear_bar()print()print(category_names[category])print(‘—–‘)print(f'{“”:{group_width}} Points Failed Score’)for group,score in catscores.items():failed = failures[category][group] or ”group_points = points[category][group]cat_points += group_pointscat_score+= scoreprint(f'{group:{group_width}} {group_points:6.1f} {failed:6} {score:5.1f}’)if len(catscores) > 1:
print(f{:{group_width}} )
print(f{:{group_width}} {cat_points:6.1f}{cat_score:5.1f})
logcfg = {
version: 1,
disable_existing_loggers: False,
formatters: {
normal: { format: %(asctime)s %(levelname)-8s %(message)s },
},
handlers: {
file: {
class: logging.FileHandler,
#filename: autograder.log,
filename: os.path.join(sys.path[0], autograder.log),
mode: a,
formatter: normal,
delay: True,
},
},
root: {
handlers: [file],
},
}
def get_args(src_subdir):
import argparse
argp = argparse.ArgumentParser()
argp.add_argument(-1, stop, action=store_true,
help=Stop after the first error. Increases verbosity.)
argp.add_argument(-v, verbose, action=count, default=0,
help=Print more output)
argp.add_argument(-q, quiet, action=count, default=0,
help=Print less output),
argp.add_argument(-i, init, action=store_true,
help=Create the build directory, but do not compile or test)
argp.add_argument(-f, fresh, action=store_true,
help=Delete object directory and rebuild before testing)
argp.add_argument(-s, src, metavar=dir, default=src_subdir,
help=Directory containing program files)
argp.add_argument(-b, build, metavar=dir, default=None,
help=Directory to place object files)
argp.add_argument(-a, archive, metavar=tar,
help=Archive containing program files (overrides -s and -o))
# argp.add_argument(-x, extra, action=store_true,
# help=Include extra credit tests)
# argp.add_argument(-m, multiply, nargs=2, metavar=(project,factor),
# action=append, default=[],
# help=Multiply a particular project score by some factor.)
argp.add_argument(-d, debug, action=store_true,
help=Increase logging)
argp.add_argument(program, nargs=*,
help=Name of program to grade)
return argp.parse_args()
@contextmanager
def temp_dir():
Create a temporary directory, and delete it and its contents once
the context has been closed. Yields the directory path
import tempfile, shutil
dir = tempfile.mkdtemp()
try:
logger.debug(Created temporary directory: %r, dir)
yield dir
finally:
logger.debug(Deleting temporary directory)
shutil.rmtree(dir)
def main(name, assignment, release=1,
src_subdir = src,
build_subdir = build,
data_subdir = data,
logcfg = logcfg):
import logging.config
args = get_args(src_subdir)
if logcfg:
logging.config.dictConfig(logcfg)
if args.debug:
logger.setLevel(logging.DEBUG)
logger.info(Starting autograder %s release %s. Library %s,
name, release, __version__)
# data directory is relative to grader
data_dir = os.path.join(sys.path[0], data_subdir)
logger.debug(Data directory: %r, data_dir)
reporter = get_reporter()
verb = args.verbose args.quiet
if args.stop:
verb += 1
if args.build:
build_subdir = args.build
logger.debug(Verbosity level: %s, verb)
if verb < 0:reporter.show_comments = Falseif verb < 1:reporter.show_input = Falsereporter.show_output = Falseif verb > 1:
reporter.show_successes = True
kws = {
fail_stop: args.stop,
requests: set(args.program),
init_only: args.init,
}
try:
reporter.clear_bar()
print(f'{name} Auto-grader, Release {release})
if args.archive:
archive = os.path.realpath(args.archive)
logger.debug(Archive path: %r, archive)
if not os.path.exists(archive):
raise Error(archive not found: + repr(archive))
with temp_dir() as dir:
os.chdir(dir)
run_command([tar, -xf, archive])
if not os.path.isdir(src_subdir):
raise Error(archive does not contain directory + repr(src_subdir))
if os.path.exists(build_subdir):
reporter.message(fWARNING: archive contains {build_subdir!r})
import shutil
shutil.rmtree(build_subdir)
os.mkdir(build_subdir)
src_dir = os.path.realpath(src_subdir)
build_dir = os.path.realpath(build_subdir)
test_project(assignment, src_dir, build_dir, data_dir, **kws)
else:
src_dir = os.path.realpath(args.src)
logger.debug(Source directory: %r, src_dir)
if not os.path.isdir(src_dir):
raise Error(invalid src directory: + repr(src_dir))
# TODO: some control about how the build directory is handled
build_dir = os.path.realpath(build_subdir)
logger.debug(Build directory: %r, build_dir)
if args.fresh and os.path.isdir(build_dir):
import shutil
logger.info(Removing build_dir: %r, build_dir)
shutil.rmtree(build_dir)
test_project(assignment, src_dir, build_dir, data_dir, **kws)
except Error as e:
reporter.clear_bar()
e.report(grader)
exit(1)
except Exception as e:
logger.exception(Uncaught exception: %s, e)
reporter.clear_bar()
print(grader: internal error)
exit(1)
if __name__ == __main__:
import logging.config
logging.config.dictConfig(logcfg)
proj = MultiProject(
StringTests.Project(name=roman),
StringTests.Project(name=pal))
reporter = TestReporter(show_successes=False)
test_project(proj, os.path.realpath(src), os.path.realpath(obj), os.path.realpath(data))
#!/usr/bin/env python3
import autograde
import os, os.path
assignment_name = PA2
release=1
autograde.Test.time_limit = 60
class ParamTests(autograde.FileTests):
def get_tests(self, project, prog, build_dir, data_dir):
test_group = project + : + self.name if self.name else project
# gather the names of the reference files
fnames = [fname for fname in os.listdir(data_dir)
if fname.startswith(self.ref_prefix)
and fname.endswith(self.suffix)]
fnames.sort()
prog = ./ + prog
for ref_name in fnames:
autograde.logger.debug(Ref_name %r, ref_name)
parts = ref_name[len(self.ref_prefix):].split(.)
if len(parts) != 3:
autograde.logger.warning(Malformed reference file: %r, ref_name)
continue
arg = os.path.join(data_dir, fmanifest.{parts[0]}.txt)
width = parts[1]
if not os.path.exists(arg):
autograde.logger.warning(Manifest not found: %r, ref_name)
continue
ref = os.path.join(data_dir, ref_name)
yield self.Test(cmd= [prog, width, arg],
input_file = arg,
ref_file = ref,
category = self.category,
group= test_group,
weight = self.weight,
dir= build_dir)
assignment = ParamTests.Project(knapsack, weight=5)
if __name__ == __main__:
autograde.main(assignment_name, assignment, release)
TARGET = knapsack
CC = gcc
CFLAGS = -g -std=c99 -Wall -Wvla -Werror -fsanitize=address,undefined
$(TARGET): $(TARGET).c
$(CC) $(CFLAGS) -o $@ $^
clean:
rm -rf $(TARGET) *.o *.a *.dylib *.dSYM
6
Heavy4 39
Light1 1 10
Light2 1 10
Light3 1 10
Light4 1 10
Light5 1 10
3
A 1 1
B 1 1
C 1 1
10
Apple 1014
Bananna918
Cherry 2 5
Diamond2 100
Eggplant1530
Frankfurter530
Grape2 3
Havarti714
Ice_cream 20 100
Juice 2030
17
S9a19
S9b19
S10a 1 10
S10b 1 10
M9 2 18
M9.5a2 19
M9.5b2 19
L9 4 36
L9.254 37
L9.5 4 38
L9.754 39
X9 8 72
X9.258 74
X9.5 8 76
X9.875 8 79
Y9.510 95
Y9.910 99
Light1
Light2
Light3
Light4
Light5
50 / 5
Heavy
Light1
Light2
59 / 6
A
1 / 1
A
B
2 / 2
A
B
C
3 / 3
Bananna
Cherry
Diamond
Frankfurter
Grape
156 / 20
Cherry
Diamond
Frankfurter
Ice_cream
235 / 29
Bananna
Cherry
Diamond
Frankfurter
Grape
Ice_cream
256 / 40
Diamond
Eggplant
Frankfurter
Havarti
Ice_cream
274 / 49
S10a
Y9.9
109 / 11
pa2/autograde.py
pa2/grader.py
pa2/template.make
pa2/data/manifest.01.txt
pa2/data/manifest.02.txt
pa2/data/manifest.03.txt
pa2/data/manifest.04.txt
pa2/data/ref.01.5.txt
pa2/data/ref.01.6.txt
pa2/data/ref.02.1.txt
pa2/data/ref.02.2.txt
pa2/data/ref.02.3.txt
pa2/data/ref.03.20.txt
pa2/data/ref.03.30.txt
pa2/data/ref.03.40.txt
pa2/data/ref.03.50.txt
pa2/data/ref.04.11.txt
Reviews
There are no reviews yet.