Tutorial: Moodle VPL -- Testing Python Program that performs basic input/output

From DftWiki

Jump to: navigation, search

--D. Thiebaut (talk) 10:11, 26 February 2015 (EST)


Contents

MoodleVPLLogo.png



Moodle VPL Tutorials



This VPL activity tests a Python program that prompts the user for 2 strings. The first is the name of a text file (created by the script) that contains some text that will contain the word "chocolate." The second string is a word that will be substituted for the word "chocolate" that is supposed to be present in the text file (possibly multiple times).


vpl_run.sh


#! /bin/bash

cat > vpl_execution <<EOF
#! /bin/bash

# --- Python ----
if [[ `hostname -s` = "beowulf2" ]]; then
   python=/usr/bin/python3.3
else
   python=/usr/local/bin/python3.4
fi

prog=lab5_7.py
\$python \$prog

EOF


chmod +x vpl_execution


vpl_debug.sh


 


vpl_evaluate.sh


#! /bin/bash

cat   > vpl_execution <<EOF
#! /bin/bash

# --- Python ----
if [[ `hostname -s` = "beowulf2" ]]; then
   python=/usr/bin/python3.3
else
   python=/usr/local/bin/python3.4
fi



\$python evaluate.py

EOF


chmod +x vpl_execution


vpl_evaluate.cases


 


evaluate.py


# evaluate.py
# D. Thiebaut
# This program is used to test a student's python program on Moodle.

import sys
import random
import subprocess

#--- define what the student program is called, and what the solution
#--- program name is.
module = "lab5_7"
solutionModule = "lab5_7sol"
userOutSplitPattern = "POEM"     # pattern used to start recording the user
                               # output.  Useful when program does several
                               # input() statements, and user output starts
                               # after that.


#--- GLOBALS ---
interpreter = sys.executable

def commentLong( line ):
    print( "<|--\n" + line  + "\n --|>" )

def commentShort( text ):
    print( "Comment :=>> " + text )

def printGrade( grade ):
    print( "Grade :=>> ", grade )

# generateInputFileWithRandomInputs
# generate a file with name "inputFileName" with some random input
# for the program.
# MAKE SURE TO EDIT THIS TO MATCH THE PROGRAM BEING TESTED
def generateInputFileWithRandomInputs( inputFileName ):
    #--- generate random inputs ---
    fileName = random.choice( [ "choco.txt", "choc.txt", "chocolat.txt" ] )

    title    = [ "POEM: Chocolate Poem", "POEM: Chocolate Chocolate", "POEM: Chocolate Anyone?" ]
    lines    = [ "Chocolate, CHOCOLATE : you're the best!",
                 "Milk chocolate, dark chocolate?  Who cares?",
                 "I'm happy when I eat chocolate.  You?",
                 "This line does not contain c-h-o-c-o-l-a-t-e",
                 "It's cold... I want some hot chocolate, now!" ]
    author   = [ "--Anonymous", "--Francis", "--DFT" ]
    replace  = random.choice( [ "carrot", "cocoa", "cream" ] )

    poem     = [ random.choice( title ),
                 random.choice( lines ),
                 random.choice( lines ),
                 random.choice( lines ),
                 random.choice( author ) ]
    poem = "\n".join( poem )

    #--- create data file with input data ---
    text = poem + "\n"
    file = open( fileName, "w" )
    file.write( text )
    file.close()

    #--- create input file for stdin ---
    file = open( inputFileName, "w" )
    file.write( fileName + "\n" )
    file.write( replace + "\n" )
    file.close()

    return  text

# checkForFunctionPresence
# checks that "functionName" is defined and called in the program.
# MAKE SURE TO EDIT TO MATCH PROGRAM BEING TESTED
def checkForFunctionPresence( module, functionName ):
    foundDef = False
    foundCall = False

    for line in open( module+".py", "r" ).readlines():
        # remove comments
        idx = line.find( "#" )
        if ( idx >=0 ): line = line[0:idx]

        if line.startswith( "def " + functionName + "(" ):
            foundDef = True
            continue
        if line.startswith( "def " + functionName + " (" ):
            foundDef = True
            continue
        if line.find( functionName+"(" ) != -1:
            foundCall = True
            continue

    return (foundDef, foundCall)



# ==================================================================
# NO EDITS NEEDED BELOW!
# ==================================================================




# checkModuleRunsOK: runs the module as a shell subprocess and
# look for errors in the output.  This is required, because otherwise
# importing the module in this program will make this program crash.
# It's not possible (as far as I can tell0 to catch exceptions from
# the import or __module__ statements.
# returns True, none if no errors, otherwise False, string if there's
# an exception, and the error message (string) is in the returned 2nd
# arg.
# The module name is assumed to not include ".py"
def checkModuleRunsOk( module, inputFileName ):
    global interpreter
    p = subprocess.Popen( [ interpreter, module+".py" ],
                          stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE,
                          stdin=subprocess.PIPE)

    p.stdin.write( bytes( open( inputFileName, "r" ).read(), 'UTF-8' ) )
    data = p.communicate( )
    p.stdin.close()

    error = data[1].decode( 'UTF-8' )
    if len( error ) > 1:
        return False, error
    return True, None


# extractTextFromErrorMessage( sys_exc_info ):
def extractTextFromErrorMessage( sys_exc_info ):
    print( "sys_exec_info = ", sys_exc_info )
    text = ""
    for field in sys_exc_info:
        if type( field )==type( " " ):
            text += field + "\n"
    return text

# runModule:
# runs the module, passes it data from the input file on its stdin
# and get its output on stdout captured in outputFileName.
# We assume the module will not crash, because we already tested
# it with checkModuleRunsOk().
def runModule( module, inputFileName, outputFileName ):
    global userOutSplitPattern

    error = False

    #--- make stdin read information from the text file
    sys.stdin = open( inputFileName, "r" )

    #--- capture the stdout of the program to test into a file
    saveStdOut = sys.stdout
    saveStdErr = sys.stderr

    sys.stdout = open( outputFileName, "w" )
    sys.stderr = open( "errorOut", "w" )

    #--- run the student program ---
    try:
        _module = __import__(  module  )
    except:
        error = True
        sys.stderr.close()
        sys.stderr = saveStdErr
        sys.stdout.close()
        sys.stdout = saveStdOut
        text = sys.exc_info()[0]
        text = extractTextFromErrorMessage( text )
        print( "*** sys.exc_info() = ", text )
        return error, text

    #--- filter out junk from output of program ---
    sys.stdout.close()
    sys.stdout = saveStdOut
    sys.stderr.close()
    sys.stderr = saveStdErr
    file = open( outputFileName, "r" )
    text = file.read()
    index = text.find( userOutSplitPattern ) #"+-" )
    text = text[index:]
    text = text.strip( ).strip( "\n" ) + "\n"
    #print( text, end="" )
    file.close()
    return False, text

def removeBlankLines( lines ):
    newLines = []
    for line in lines.split( "\n" ):
        if len( line )==0:
            continue
        newLines.append( line )

    return ( "\n".join( newLines ) ) + "\n"

def compareUserExpected( inputLines, userOutText, expectedOutText ):
    userOutText = removeBlankLines( userOutText )
    expectedOutText = removeBlankLines( expectedOutText )
    #print( "userOutText:" + userOutText )
    #print( "expectedOutText:" + expectedOutText )
    misMatchLineNumbers = []
    userTextOutLines = userOutText.split( "\n" )
    expectedOutTextLines = expectedOutText.split( "\n" )

    for i in range( len( userTextOutLines ) ):
        lineNo = i+1
        if userTextOutLines[i] != expectedOutTextLines[i]:
            misMatchLineNumbers.append( lineNo )

    return misMatchLineNumbers



def main():
    global module
    global solutionModule


    #--- check that the main module uses a main() function
    foundDef, foundCall = checkForFunctionPresence( module, "main" )
    if (not foundDef) or (not foundCall):
        commentShort( "-Missing main() program" )
        commentShort( "Your program must use a main() function." )
        commentShort( "(make sure you spell it exactly \"main()\"!" )
        printGrade( 40 )
        return


    #--- generate input file with random data ---
    inputLines = generateInputFileWithRandomInputs( "input" )


    Ok, errorMessage = checkModuleRunsOk( module, "input" )
    if not Ok:
        commentLong( "- Your program crashed...\n"
                     + "Your program was tested with:\n"
                     + inputLines  + "\n"
                     + "Error message:\n"
                     + errorMessage + "\n" )
        printGrade( 50 )
        return


    error, userOutText        = runModule( module, "input", "userOut" )
    if error:
        commentLong( "- Your program crashed...\n"
                     + "Your program was tested with:\n"
                     + inputLines  + "\n"
                     + "Error message:\n"
                     + userOutText + "\n" )
        printGrade( 50 )
        return

    dummy, expectedOutText    = runModule( solutionModule, "input", "expectedOut" )

    misMatchLineNumbers = compareUserExpected( inputLines,
                                               userOutText,
                                               expectedOutText )
    if len( misMatchLineNumbers ) == 0:
        commentLong( "- Congrats, correct output!\n"
                     +"Your program was tested with:\n"
                     +inputLines + "\n"
                     +"Your output:\n"
                     +userOutText + "\n" )
        printGrade( 100 )
    else:
        s = "" if len( misMatchLineNumbers ) == 1 else "s"

        commentLong( "-Incorrect output.\n"
                    +"Your program was tested with:\n"
                     +inputLines + "\n"
                     +"Your output:\n"
                     +userOutText + "\n"
                     +"Expected output:\n"
                     +expectedOutText + "\n"
                     +("There are mismatches in Line%s " % s) +
                     ", ".join( [str(k) for k in misMatchLineNumbers] )
                     +"\n" )
        noLinesGood = len( expectedOutText.strip().split("\n") )
        noErrors    = len( misMatchLineNumbers )
        #print( "noLinesGood = ", noLinesGood, " noErrors = ", noErrors )

        grade       = round( 60 + 40 / noLinesGood *( noLinesGood- noErrors ) )
        printGrade( grade )

main()


lab5_7sol.py


def challenge7():
    # get the user input
    fileName = input( "File name? " )
    word     = input( "Word to swap in? " )

    # open the file and read the lines in
    file = open( fileName, "r" )
    text = file.read()
    file.close()

    # replace chocolate by word before we split the text
    text = text.replace( "Chocolate", word.capitalize() )
    text = text.replace( "chocolate", word.lower() )
    text = text.replace( "CHOCOLATE", word.upper() )

    # split the text into lines for formatting
    lines = text.strip().split( "\n" )

    # create a bar for the top and bottom of box
    bar = "+" + "-"*50 + "+"

    # get first line and print it
    #print( bar )
    #print( "|" + lines[0].upper().center( 50 ) + "|" )
    print( lines[0].upper()  )

    # print each line
    for line in lines[1:-1]:
        #print( "|" + line.center(50) + "|" )
        print(  line  )
    #print( "|" + lines[-1].rjust(50) + "|" )
    print(  lines[-1]   )
    #print( bar )

def main():
    challenge7()

main()