Created
June 3, 2012 23:59
-
-
Save bulatb/2865493 to your computer and use it in GitHub Desktop.
Autograder submission bot for CSE 120
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/software/common/python-2.7/bin/python2.7 | |
# ^ /software is where ieng6 keeps the nice things we can't have | |
import os | |
import time | |
import subprocess | |
from optparse import OptionParser | |
AUTOGRADER_SCRIPT = "autograder-proj%s" | |
SOURCE_PACKAGES = ['test', 'threads', 'userprog', 'vm'] | |
MTIME_FILE_NAME = ".autosubmit-mtime" | |
def build_parser(): | |
"""Build an optparse OptionParser (because ieng6 runs python 2.4). | |
Update: python 2.7 lives in /software/common, but now there's no | |
point in changing it. | |
""" | |
parser = OptionParser() | |
parser.add_option("-a", "--assignment", dest="assignment", default="3", help="Assignment number.") | |
parser.add_option("-p", "--partner", dest="partner", help="Your partner's login name. Include this if you want them to receive the email.") | |
parser.add_option("-e", "--email", dest="email", help="Extra addresses to mail results to.") | |
parser.add_option("-i", "--interval", dest="interval", type="int", default=10, help="Autograder submission interval, in minutes. Default is 10; use 0 to disable.") | |
parser.add_option("-k", "--kill-after", dest="kill_after", type="float", default=5, help="Stop submitting after this many hours. Default is 5; use 0 to run until killed. Fractional values allowed.") | |
parser.add_option("-f", "--force-submit", action="store_true", dest="force_submit", default=False, help="Submit every interval, even if no watched files have changed.") | |
parser.add_option("-o", "--once", action="store_true", dest="run_once", default=False, help="Same as -i 0. Don't use them together or the bad thing that happens will be all your fault.") | |
return parser | |
def generate_grader_input(options): | |
"""Generates input sent to the autograder's stdin | |
""" | |
grader_input = "%s%sy\ny\n" | |
partner = "n\n" | |
email = "n\n" | |
if options.partner is not None: | |
partner = "y\n%s\ny\n" % options.partner | |
if options.email is not None: | |
email = "y\n%s\n" % options.email | |
return grader_input % (partner, email) | |
def has_unsubmitted_changes(): | |
"""Runs stat on all the files in every package in SOURCE_PACKAGES and | |
returns true if any of their mtimes are after the last submission to | |
the autograder. | |
This is totally gross, but ieng6 doesn't seem to have inotify. | |
""" | |
try: | |
# IO grossness is because r+ won't create the file if it doesn't | |
# exist and w+ will truncate away the previous mtime. If the file | |
# exists, open with r+ and seek to the beginning when writing the | |
# new time; otherwise create it with w+ and call this again. | |
with open(".autosubmit-mtime", "r+") as mtime_file: | |
try: | |
last_submission_time = float(mtime_file.read().rstrip()) | |
except ValueError: | |
last_submission_time = 0.0 | |
for package in SOURCE_PACKAGES: | |
path_to_package = os.path.join(".", package) | |
for project_file in os.listdir(path_to_package): | |
mtime = os.stat(os.path.join(path_to_package, project_file)).st_mtime | |
if mtime > last_submission_time: | |
mtime_file.seek(0) | |
mtime_file.write(str(time.time())) | |
return True | |
except IOError as e: | |
# If the file doesn't exist, create it and try again. | |
if e[0] == 2: | |
mtime_file = open(MTIME_FILE_NAME, "w+") | |
mtime_file.close() | |
return has_unsubmitted_changes() | |
return False | |
def should_run_again(options, kill_time): | |
run_only_once = options.interval == "0" or options.run_once | |
run_until_killed = options.kill_after == "0" | |
before_kill_time = time.time() < kill_time | |
return (before_kill_time or run_until_killed) and not run_only_once | |
def run_grader(options): | |
grader = subprocess.Popen([AUTOGRADER_SCRIPT % (options.assignment)], stdin=subprocess.PIPE, stdout=subprocess.PIPE) | |
output = grader.communicate(generate_grader_input(options)) | |
print output[0] | |
return grader | |
def main(): | |
options = build_parser().parse_args()[0] | |
autograder_name = AUTOGRADER_SCRIPT % options.assignment | |
run_interval = abs(int(options.interval)) * 60 | |
kill_time = time.time() + abs(float(options.kill_after)) * 60 * 60 | |
run_until_killed = options.kill_after == "0" | |
run_once = run_interval == 0 or options.run_once | |
# Header message | |
print "Started autograder submission bot." | |
if run_once: | |
print "Running %s once." % (AUTOGRADER_SCRIPT % options.assignment) | |
else: | |
if run_until_killed: | |
nice_kill_time = "killed" | |
else: | |
nice_kill_time = time.strftime("%I:%M %p", time.localtime(kill_time)) | |
print "Running %s every %s minutes until %s." % (autograder_name, options.interval, nice_kill_time) | |
print "Autograder output will be shown below.\n" | |
# srs bsnss | |
first_run = True | |
try: | |
while should_run_again(options, kill_time) or first_run: | |
if has_unsubmitted_changes() or options.force_submit: | |
run_grader(options) | |
else: | |
print "No files modified since last submission." | |
if not run_once: | |
print time.strftime("Next run at %I:%M %p.", time.localtime(time.time() + run_interval)) | |
print "Going until %s.\n" % nice_kill_time | |
time.sleep(run_interval) | |
first_run = False | |
except KeyboardInterrupt: | |
# Just fall through | |
pass | |
finally: | |
print "Autosubmit bot stopped at %s." % time.strftime("%I:%M %p", time.localtime()) | |
if __name__ == "__main__": | |
main() |
I wish you could do pull requests between gists... or maybe you can and I just don't know how...
I added an option to redirect output to a file: https://gist.github.com/2905877
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@bulatb, you are my hero.