
## cw1_checkout.py
## This program enables you to check your code solutions
## to the CW1 programming assignment.
## See instructions on the web site.

## YOU DO NOT NEED TO READ OR UNDERSTAND THIS PROGRAM

import os
import os.path
import getpass  #gives username
import builtins
import zipfile
import re



def menu(tests = None):
    if tests == None:
        tests = TESTS
    files_present = tests.files_present()
    selection = 1
    for filename in files_present:
            print( selection, ": test", filename)
            selection += 1
    num_files = len(files_present)
    print( "a : test all answer files" )
    print( "z : make zip file for submission (cw1_submission.zip)" )
    print( "q : quit")
    
    choice = input( "Enter number of file to test, or 'z' or 'q': ")
    
    if choice == 'q': 
        print("Exiting cw1_checker.")
        return
    
    if choice == 'z':
        tests.make_zip()
        return
    
    if choice == 'a':
       tests.test_all()
       return
    
    if is_int_castable(choice) and int(choice) in range(1, num_files+1):
       question = files_present[int(choice) -1][:-3]
       print()
       tests.test_question( question )
       return
    
    print( "??? Choice not valid." )
    print( "You must enter a number in range 1-" + str(num_files),
             "or 'a' or 'z' or 'q'.\n")
    menu(tests)



def is_int_castable(x):
    try: int(x); return True
    except: return False  


def get_file_contents_lines( filename ):        
    if os.path.isfile(filename):
        #print( "file exists", "File", filename,  "exists.")
        f = open(filename, mode='r', encoding='utf-8')
        file_contents_lines = f.readlines()
        f.close()
        return file_contents_lines
    else:
        print("!!! ERROR: File", filename, "does not exist!")
      #exit_without_error()
        return None

ALLOWED_IMPORT = "cw1_checkout"  
def get_import_lines( filename ):
    lines = get_file_contents_lines( filename )
    import_lines = []
    for line in lines:
        if line_matches_import(line):
           if not re.match("\s*import\s+" + ALLOWED_IMPORT + "\s*$", line):
              import_lines.append(line[:-1])
    return import_lines
  
def line_matches_import(line):
    return ( re.match( "^import\s+\w+", line )
             or
             re.match( "^from\s+\w+ import\s+", line )
           ) != None


def ensure_list( L ):
    if type( L ) == list: return L
    if type( L) == tuple: return list(L)
    return [L]


class Tests():
      def __init__(self):
          self.test_list = []

      def answer_tests(self):
          ats = []
          for t in self.test_list:
              ats.append( t.answer_test() )
          atests = Tests()
          atests.add(ats)
          return atests

      def total_marks(self):
          total = 0
          for test in self.test_list:
              if test.mark:
                  total += test.mark
          return total
            
      def add(self, tests):
          self.test_list += ensure_list(tests)
          
      def questions(self):
          qs = []
          for test in self.test_list:
              if not(test.question in qs):
                  qs.append(test.question)
          return qs
      
      def files(self):
          return [ q+".py" for q in self.questions()]
      
      def files_present(self):
          found = []
          for filename in self.files():
              if os.path.isfile(filename):
                 found.append(filename)
          return found
      
      def test_all(self):
          print("\n*Running all tests:")
          for test in self.test_list:
              #print("HERE")
              test.run()
              test.display_terse()
          print()
              
      def test_question(self, question):
          for test in self.test_list:
              if test.question == question:
                 test.run()
                 test.display_verbose() 
                
      def mark(self):
          mark = 0
          for test in self.test_list:
              test.run()
              mark += test.mark
          return mark
      
      def display_marks(self):
          mark = self.mark()
          for test in self.test_list:
              test.display_terse( with_mark = True)
          print( "TOTAL MARKS:", mark)
          
      def make_zip(self):
          print( "Making cw1_submission.zip")
          if self.files == []:
               print( "!!! ERROR: did not find any cw1 Python files" )
               print( "    The files required for this coursework are:" )
          with zipfile.ZipFile( 'cw1_submission.zip', mode='w' ) as subzip:
               for filename in self.files() :
                 if os.path.isfile(filename):
                    print( "Found", filename, "--- adding to cw1_submission.zip")
                    subzip.write(filename)
                 else:
                    print( "NOT found:", filename)
          
class Test():
      def __init__(self, question, inputs, answer, marks_available):
          self.question = question
          self.file = question + ".py"
          self.inputs   = ensure_list(inputs)
          self.answer   = answer
          self.marks_available = marks_available
          self.initialise()
          
      def initialise(self):
          self.given_answer = None
          self.mark     = None
          self.warning_messages = []
          self.error_messages   = []
          self.exception        = None
          self.imports          = [] 
          self.status     = "not run"
          self.input_count = 0
          self.input_lines = []
          self.last_print = None

      def answer_test(self):
          return Test(self.question + "_answer", self.inputs,
                      self.answer, self.marks_available)
        
          
      def run(self):
          self.initialise()
          
          if not os.path.isfile( self.file ):
             self.status = "no file" 
             self.mark = 0
             return
         
          self.imports = get_import_lines( self.file )
          if self.imports:
             self.status = "imports"
             self.mark = 0
             return

          try:
             file_func_def = get_file_as_function( self.file )
          except BaseException as e:
             self.status = "could not extract function"  
             # Shouldn't happen if file exists !!!
             print( "!!! ERROR: could not extract function from file:", self.file )
             self.mark = 0
             self.exception = e
             return
         
          globs = globals()
          try:
             exec( file_func_def, globs, globs )
          except BaseException as e:
                 self.status = "error loading file"
                 self.mark = 0
                 self.exception = e
                 return 
             
          try:      
             file_function( self )
          except BaseException as e:
                 self.status = "error running file"
                 self.mark = 0
                 self.exception = e
                 return 
          
          try:  
             self.given_answer = self.last_print[-1]
          except BaseException as e:
                 self.status = "no answer"
                 self.mark = 0
                 self.exception = e
                 return 
            
          if self.given_answer == "NOT ATTEMPTED":
              self.status = "QNA"
              self.mark = 0
              return
            
          if len(self.input_lines) < len(self.inputs):
               self.status = "too few inputs"
               self.mark = 0
               return
               
          if len(self.input_lines) > len(self.inputs):
               self.status = "too few inputs"
               self.mark = 0
               return 
            
          if self.given_answer == self.answer:
              self.status = "correct"
              self.mark = self.marks_available
          else:
              self.status = "incorrect"
              self.mark = 0
            
            
          #result = check_answer(question, test_count, last_print, display=display)
             
      def display_verbose(self):
           print = builtins.print
           inputs_string = ", ".join([repr(input) for input in self.inputs])
           print("Testing question", self.question, "with inputs:", inputs_string )
           
           ##print("STATUS:", self.status)
           
           if self.status == "no file":
               print( "!!! WARNING: could not find file:", self.file )          
          
           if self.status == "error loading file":
               print( "!!! ERROR: could not load question file:", self.file )
               print( "    It probably contains a syntax error." )
          
           if self.status == "imports":
              print( "!!! WARNING: imports found:")
              [print("   ", imp) for imp in self.imports]
              print( "    You are not allowed to import modules for this coursework." )
           
           if self.status == "error running file":
               print( "!!! ERROR: could not run question file:", self.file )
            
           if self.status == "no answer":
               print( "!!! ERROR: for some reason nothing was printed." ) 
            
           if self.status == "QNA":
              print( "QUESTION NOT ATTEMPTED")
              
           if self.status == "too few inputs":
              print( "!!! WARNING: Too few input statements in the file",
                "(expected {}, found {})".format(len(self.inputs),self.input_count))
          
           if self.status == "too many inputs":
              print( "!!! WARNING: Too many input statements in the file",
                "(expected {}, found {})".format(len(self.inputs),self.input_count))
          
           if self.status == "correct" or self.status == "incorrect": 
              [print("Input: ", *line) for line in self.input_lines]
           
           if self.status == "correct":
              print( "Answer:", self.last_print[:-1], repr(self.last_print[-1]) )
              print( "That is CORRECT :)" )
          
           if self.status == "incorrect":              
              print( "Answer:", self.last_print[:-1], repr(self.last_print[-1]) )
              print( "That is INCORRECT :(" )
              print( "The answer should be:", repr(self.answer ))
              if type(self.answer) != type(self.given_answer):
                  print("Note: the answer should be of type:", 
                               type(self.answer).__name__)
                  print("        but your answer is of type:",
                               type(self.given_answer).__name__)
           print()
       
      def display_terse(self, with_mark=False):
           #print("STATUS:", self.status)
           inputs_string = ", ".join([repr(input) for input in self.inputs])
           builtins.print( "{}: inputs: {} --> ".format(self.question, inputs_string),
                  end="" )

           print = builtins.print
           
           if with_mark:
               def print( *args ):
                   builtins.print( args, "[{}]".format(self.mark))
           
           if self.status == "no file":
               print( "!!! WARNING: could not find file:", self.file )          
          
           if self.status == "error loading file":
               print( "!!! could not load", self.file )
          
           if self.status == "imports":
              print( "!!! imports found")

           if self.status == "error running file":
               print( "!!! could not run", self.file )
            
           if self.status == "no answer":
               print( "!!! nothing" ) 
            
           if self.status == "QNA":
              print( "NOT ATTEMPTED")
              
           if self.status == "too few inputs":
              print( "!!! too few inputs" )
          
           if self.status == "too many inputs":
              print( "!!! too many inputs" )
          
           if self.status == "correct":
              print( "output:", repr(self.last_print[-1]),
                     " [CORRECT]  :)" )
          
           if self.status == "incorrect":              
              print( "output:", repr(self.last_print[-1]),
                     " [INCORRECT] :(" )


     
def question_files():
    return [ q + ".py" for q in TESTS.keys() ]

def question_files_present():
    return [ f for f in question_files() 
             if os.path.isfile(f) ]


    
file_function = None
FUNCTION_HEADER = """
def file_function( test1965 ):
    
    expected_inputs1965 = len(test1965.inputs)

    def print(*args):
        test1965.last_print = args
    
    def input( *args ):
        if test1965.input_count >= expected_inputs1965:
           test1965.input_count += 1 
           return None
        input_val = test1965.inputs[test1965.input_count]
        #input_line = list(args).copy()
        input_line = [x for x in args].copy()
        input_line.append(input_val) 
        test1965.input_lines.append( input_line )
        return input_val
"""

FUNCTION_FOOTER = """
    #builtins.print("test:",  test1965.name, test1965.status )
    if test1965.input_count < expected_inputs1965:
       test1965.status = "too few inputs"
    if test1965.input_count > expected_inputs1965:
       test1965.status = "too many inputs"
"""

def get_file_as_function( filename ):
    lines = get_file_contents_lines( filename )
    if lines == None:
        return None
    function = FUNCTION_HEADER
    for line in lines:
        function += "    " + line
    function += FUNCTION_FOOTER
    return function


TESTS= Tests()

TESTS.add( [ Test( "numvowels", "hello",    2, 1 ),
             Test( "numvowels", "Euphoria", 5, 1 ),
             
             Test( "numdiffvowels", "Euphoria", 5, 1 ),
             Test( "numdiffvowels", "Easy sleep", 2, 1 ),
             
             Test( "anonymise", "Brandon Bennett", "Xxxxxxx Xxxxxxx", 1),
             Test( "anonymise", "Lord E.J. Streeb-Greebly", 
                             "Xxxx X.X. Xxxxxx-Xxxxxxx", 1),

             Test( "password_strength", "hello", "WEAK", 1 ),
             Test( "password_strength", "7Kings8all9Pies", "STRONG", 1 ),
             Test( "password_strength", "brandon123", "MEDIUM", 1 ),

             Test( "megabyte_bars",  "2",  2.50, 1),
             Test( "megabyte_bars", "14", 12.50, 1),
             Test( "megabyte_bars", "16", 15.00, 1),
             Test( "megabyte_bars", "26", 20.25, 1)                        
          ]
         )    

def amenu():
    ATESTS = TESTS.answer_tests()
    menu(ATESTS)

print( "\n* cw1_checkout *\n" )
print( "Provides functions for checking your code, and for creating" )
print( "your coursework submission file.\n" )

print( "Type the command" )
print( "  menu() ")
print( "to see a menu of available options.\n" )







