jmooremcc

studentCodeEvaluator.py

Dec 20th, 2020 (edited)
698
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 13.27 KB | None | 0 0
  1. #!/usr/bin/env python
  2. # studentCodeEvaluator.py
  3. # *
  4. # *  Copyright (C) 2020 John Moore
  5. # *
  6. # *  This Program is free software; you can redistribute it and/or modify
  7. # *  it under the terms of the GNU General Public License as published by
  8. # *  the Free Software Foundation; either version 2, or (at your option)
  9. # *  any later version.
  10. # *
  11. # *  This Program is distributed in the hope that it will be useful,
  12. # *  but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # *  GNU General Public License for more details.
  15. # *
  16. # * write to the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
  17. # * http://www.gnu.org/copyleft/gpl.html
  18. # *
  19.  
  20. """
  21. studentCodeEvaluator is a program that helps an instructor grade python code written by their students.
  22.  
  23. The program expects the student code files to be in their own directory. This means that the instructor can create
  24. unique assignments and create folders that contain the student's code for that assignment.
  25.  
  26. If the student's programs read data from stdin, the instructor can create a response file that will supply input to
  27. their programs. The response file is a simple text file that has one response per line. The response file is located
  28. in the assignment directory by default.
  29.  
  30. studentCodeEvaluator generates a log file named after the folder the code resides in. The log file contains the output of
  31. stdout and stderr for each program run. By default, the log file if located in the assignment directory.
  32.  
  33. syntax:
  34.    studentCodeEvaluator assignmentDir -r <responsefile> -l <logfilepath> -t <timeout> -p <pgmArgs> -n <numReturns>
  35.  
  36.    assignmentDir (required) is the absolute path to the directory containing the student's code
  37.  
  38.    responsefile (optional) is the path to the response file. The response file is assigned to stdin in the student's
  39.        program.
  40.  
  41.    logfilepath (optional) is the path to the location where the log file will be created. The logfile will have the
  42.        have the same name as the assignmentDir but with a .log extension.
  43.  
  44.    timeout (optional) The instructor can specify the maximum time a student's program can run in seconds.
  45.        The default value is 10 seconds.
  46.  
  47.    pgmArgs (optional) are args that can be passed to a student's program.
  48.        The default value is None
  49.  
  50.     pgmKwArgs (optional) are kwargs that can be passed to a student's program.
  51.        The default value is None
  52.  
  53.    numReturns (optional) is a value that represents the number of lines the student's program will write to stdout.
  54.        If a student's program does not use all of the responsefile, an error will appear in the log file.
  55.  
  56. """
  57.  
  58. import argparse
  59. import subprocess
  60. import json
  61. import sys, os
  62. from time import sleep
  63.  
  64.  
  65. def serialize(data, key=None):
  66.     if key is None:
  67.         return json.dumps(data)
  68.     else:
  69.         return key+":"+json.dumps(data)
  70.  
  71.  
  72. def deserialize(data, key=None):
  73.     if key is None:
  74.         return json.loads(data)
  75.     else:
  76.         tmp = data.split(':',1)
  77.         if tmp[0] == key and len(tmp) == 2:
  78.             return json.loads(tmp[1])
  79.  
  80.  
  81. def isDir(path):
  82.     return os.path.isdir(path)
  83.  
  84. def isFullpath(path):
  85.     return not os.path.isdir(path) and os.path.isabs(path)
  86.  
  87. def isRelativepath(path):
  88.     return path[:2] == '.'+os.path.sep
  89.  
  90. def dirjoin(dir, fname):
  91.     if not isFullpath(fname):
  92.         return os.path.join(dir, fname)
  93.  
  94.     return fname
  95.  
  96. def getDirname(path):
  97.     if os.path.isdir(path):
  98.         return path
  99.  
  100.     tmp = os.path.split(path)
  101.     if len(tmp[0]) == 0:
  102.         return None
  103.     else:
  104.         return tmp[0]
  105.  
  106. def list2Dictionary(kwlist):
  107.     return dict(zip(*[iter(kwlist)]*2))
  108.  
  109. def generateOutputfile(codefile, outputfile, responsefile = None, pgmArgs = None):
  110.     if pgmArgs is None:
  111.         cmd = [sys.executable, codefile]
  112.     else:
  113.         cmd = [sys.executable, codefile, *pgmArgs]
  114.  
  115.     try:
  116.         if responsefile is None:
  117.             result = subprocess.run(cmd, shell=True, capture_output=True, encoding='utf8')
  118.         else:
  119.             responsefp = open(responsefile, 'r')
  120.             result = subprocess.run(cmd, shell=True, stdin=responsefp, capture_output=True, encoding='utf8')
  121.             responsefp.close()
  122.  
  123.         outputfilefp = open(outputfile, 'w')
  124.         outputfilefp.write(result.stdout)
  125.         outputfilefp.close()
  126.         return True
  127.     except Exception as e:
  128.         print(str(e))
  129.         return False
  130.  
  131.  
  132. def runCode(codefile, responsefp, logfilefp, timeout, numOutputlines, pgmArgs = None):
  133.     if pgmArgs is None:
  134.         cmd = [sys.executable, codefile]
  135.     else:
  136.         cmd = [sys.executable, codefile, *pgmArgs]
  137.  
  138.     try:
  139.         if responsefp is None:
  140.             result = subprocess.run(cmd, shell=True, timeout=timeout, capture_output=True, encoding='utf8')
  141.         else:
  142.             result = subprocess.run(cmd, shell=True, stdin=responsefp, timeout=timeout, capture_output=True, encoding='utf8')
  143.  
  144.         logfilefp.write('STDOUT:\n')
  145.         logfilefp.write(result.stdout)
  146.  
  147.         logfilefp.write('\nSTDERR:\n')
  148.         logfilefp.write(result.stderr)
  149.         if numOutputlines is not None:
  150.             N = len(result.stdout.split("\n"))
  151.             # print(f"numReturns:{N}<{numReturns}={N<numReturns}")
  152.             if N < numOutputlines and result.returncode == 0:
  153.                 logfilefp.write("Program Error: Not All Input Utilized\n\n")
  154.             else:
  155.                 logfilefp.write('\n\n')
  156.     except subprocess.TimeoutExpired as e:
  157.         logfilefp.write('STDOUT:\n')
  158.         logfilefp.write(str(e.stdout))
  159.  
  160.         logfilefp.write('\nSTDERR:\n')
  161.         logfilefp.write("Execution Time Expired Error\n")
  162.         logfilefp.write(str(e)+"\n\n")
  163.     except Exception as e:
  164.         logfilefp.write(str(e)+"\n\n")
  165.  
  166.  
  167.  
  168. def join(dir, file):
  169.     return(dir + os.path.sep + file)
  170.  
  171.  
  172. def studentCodeEvaluator(assignmentDir, logfilepath=None, responseFile=None, timeout=None, numOutputlines = None, pgmArgs=None, pgmKwArgs=None):
  173.     print(f"assignmentDir:{assignmentDir}")
  174.     basename = os.path.basename(assignmentDir)
  175.     print(f"logfilepath:{join(logfilepath,basename+'.log')}")
  176.     print(f"responsefile:{responseFile}")
  177.     print(f"responsefilepath:{join(assignmentDir, responseFile)}")
  178.     print(f"timeout:{timeout}")
  179.  
  180.     logname = os.path.basename(assignmentDir) + '.log'
  181.     logfilepath = os.path.join(logfilepath,logname)
  182.     logfilerp = open(logfilepath, 'w', encoding='utf8')
  183.  
  184.     if pgmArgs is not None and pgmKwArgs is not None:
  185.         pgmArgs = pgmArgs + (pgmKwArgs,)
  186.     elif pgmKwArgs is not None:
  187.         pgmArgs = pgmKwArgs
  188.  
  189.     rfp = None
  190.     if responseFile is not None:
  191.         if not isFullpath(responseFile):
  192.             if not isRelativepath(responseFile):
  193.                 responsefilepath = os.path.join(assignmentDir, responseFile)
  194.             else:
  195.                 responsefilepath = responseFile
  196.         else:
  197.             responsefilepath = responseFile
  198.  
  199.         rfp = open(responsefilepath,'r', encoding='utf8')
  200.  
  201.     files = next(os.walk(assignmentDir))[2]
  202.     for file in files:
  203.         ext = os.path.splitext(file)[1]
  204.         if '.py' in ext:
  205.             print(file)
  206.             logfilerp.write("="*len(file))
  207.             logfilerp.write('\n'+file+'\n')
  208.             codepath = os.path.join(assignmentDir, file)
  209.             runCode(codepath, rfp, logfilerp, timeout, numOutputlines, pgmArgs)
  210.             rfp.seek(0, 0)
  211.  
  212.     rfp.close()
  213.     logfilerp.close()
  214.  
  215. def countCodefileInputs(codefile):
  216.     count = 0
  217.     with open(codefile, 'r') as fp:
  218.             for line in fp:
  219.                 if line[0] == '#' or len(line) == 0:
  220.                     continue
  221.  
  222.                 if 'input(' in line:
  223.                     count += 1
  224.  
  225.  
  226.     return count
  227.  
  228.  
  229. def countOutputfileNreturns(outputfile):
  230.     count = 0
  231.     with open(outputfile, 'r') as fp:
  232.         for line in fp:
  233.             line = line.strip()
  234.             if len(line) == 0 or line[0] == '#':
  235.                 continue
  236.  
  237.             if len(line) > 1:
  238.                 count += 1
  239.  
  240.     return count
  241.  
  242.  
  243. def countOutputLines(codefile, outputfile, responseFile=None, pgmArgs=None, pgmKwArgs=None):
  244.     """
  245.    This function executes the codefile and counts and reports the number of lines of text
  246.    the codefile writes to stdout.If present the content of the responseFile replaces the codefile's stdin
  247.    :param str codefile: The codefile to be run
  248.    :param str outputfile: The file to write the stdout to
  249.    :param str responseFile: A file of responses that will replace the codefile's stdin
  250.    :param tuple pgmArgs: A list of arguments
  251.    :param str pgmKwArgs: a json kwargs string
  252.    :return: None
  253.    """
  254.     print("Computing nReturns:")
  255.     print(f"\tcodefile:{os.path.basename(codefile)} pgmArgs:{pgmArgs}")
  256.     print(f"\toutputfile:{outputfile}")
  257.     print(f"\tresponsefile:{responseFile}")
  258.  
  259.     nReturns = 0
  260.     dir = getDirname(codefile)
  261.  
  262.     # convert simple filename to absolute filename
  263.     if not isFullpath(outputfile):
  264.         if dir is not None:
  265.             outputfile = dirjoin(dir,outputfile)
  266.  
  267.     # convert simple filename to absolute filename
  268.     if not isFullpath(responseFile):
  269.         if dir is not None:
  270.             responseFile = dirjoin(dir, responseFile)
  271.  
  272.     if pgmArgs is not None and pgmKwArgs is not None:
  273.         pgmArgs = pgmArgs + (pgmKwArgs,)
  274.     elif pgmKwArgs is not None:
  275.         pgmArgs = pgmKwArgs
  276.  
  277.     if generateOutputfile(codefile, outputfile, responseFile, pgmArgs):
  278.         try:
  279.             nReturns = countOutputfileNreturns(outputfile)
  280.         except Exception as e:
  281.             print(str(e))
  282.  
  283.     print(f"\tnReturns:{nReturns}")
  284.  
  285.  
  286. def parseArgs():
  287.     parser = argparse.ArgumentParser(
  288.         prog='studentcodeEvaluator',
  289.         formatter_class=argparse.RawTextHelpFormatter,
  290.         description="Student Code Evaluator")
  291.     parser.add_argument(
  292.         'assignmentDir',
  293.         type=str,
  294.         metavar='Assignment Directory',
  295.         help="(Required) Path to assignment directory that contains the student's code.\n \n"
  296.     )
  297.     parser.add_argument(
  298.         '-a',
  299.         metavar='Program Args',
  300.         dest='pgmArgs',
  301.         nargs='*',
  302.         default=None,
  303.         help="These are args to be passed to the student's program.\n \n"
  304.     )
  305.     parser.add_argument(
  306.         '-kwa',
  307.         metavar='Program kwArgs',
  308.         dest='pgmKwArgs',
  309.         nargs='*',
  310.         default=None,
  311.         help="These are kwargs to be passed to the student's program.\n \n"
  312.     )
  313.     parser.add_argument(
  314.         '-c',
  315.         metavar='OutputLines File',
  316.         dest='outputLinesfile',
  317.         default = None,
  318.         help="A utility to count the number of lines of text the student's code outputs to stdout.\n"\
  319.             "This option requires the following parameters:\n"\
  320.             "\t(1) The codefile to be run.\n"
  321.             "\t(2) The output file.\n"
  322.             "\t(3) The response file (optional).\n"
  323.             "\t(4) pgmArgs to be passed to the codefile (optional).\n"
  324.             "The utility will print the number of lines of text generated by the codefile.\n \n"
  325.     )
  326.     parser.add_argument(
  327.         '-l',
  328.         metavar='Logfile Path',
  329.         dest='logfilepath',
  330.         default='assignmentDir',
  331.         help='The location of the teacher log file\n'\
  332.             'The default location is the assignmentDir.\n \n'
  333.     )
  334.     parser.add_argument(
  335.         '-n',
  336.         type = int,
  337.         metavar='Number of Output Lines',
  338.         dest='numOutputlines',
  339.         default = None,
  340.         help="The number of lines of output stdout should contain.\n"\
  341.             "This parameter is used to determine if a student's code reads and\n"\
  342.             "prints all of the input from the responsefile.\n \n"
  343.     )
  344.     parser.add_argument(
  345.         '-r',
  346.         metavar='Response File',
  347.         dest='responseFile',
  348.         default=None,
  349.         help="The response file provides input to stdin of the student's program.\n \n"
  350.     )
  351.     parser.add_argument(
  352.         '-t',
  353.         metavar='Timeout',
  354.         dest='timeout',
  355.         type=int,
  356.         default=10,
  357.         help="The timeout is the maximum amount of time in seconds the students\n"\
  358.             "program will be allowed to run. If a student's code runs longer than this value,\n"\
  359.             "it will generate a time expired error.\n"\
  360.             "The default value for timeout is %(default)s seconds\n \n"
  361.     )
  362.     parser.add_argument(
  363.         '--version',
  364.         action='version',
  365.         version='%(prog)s 1.0'
  366.     )
  367.     args = parser.parse_args()
  368.     if args.logfilepath == 'assignmentDir':
  369.         args.logfilepath = args.assignmentDir
  370.  
  371.     if args.pgmKwArgs is not None:
  372.         args.pgmKwArgs = serialize(list2Dictionary(args.pgmKwArgs),'kwargs')
  373.  
  374.     if args.pgmArgs is not None:
  375.         args.pgmArgs = tuple(args.pgmArgs)
  376.  
  377.     return args
  378.  
  379. def main():
  380.     args=parseArgs()
  381.     if args.outputLinesfile is not None:
  382.         countOutputLines(args.assignmentDir, args.outputLinesfile, responseFile=args.responseFile, pgmArgs=args.pgmArgs, pgmKwArgs=args.pgmKwArgs)
  383.     else:
  384.         kwargs = vars(args)
  385.         del kwargs['outputLinesfile']
  386.         studentCodeEvaluator(**kwargs)
  387.  
  388.     print("Finished...")
  389.  
  390. if __name__ == '__main__':
  391.     main()
  392.  
Add Comment
Please, Sign In to add comment