#!/usr/bin/env python

"""
Test an executable IO behaviour.

Inputs: stdin, comand line arguments.

Outputs: stdout, stderr, and exit status.

Use as:

    ./test directory_of_executable basename_of_executable
"""

import imp
import subprocess
import sys
import os

class Input:
    """
    TODO environment variables.
    """
    def __init__(self, stdin, args = []):
        self.stdin  = stdin
        self.args   = args

    def __eq__(self, other):
        if type(other) is type(self):
            return self.__dict__ == other.__dict__
        return False

    def __repr__(self):
        output = ""
        previous = False
        if self.args:
            output += 'args:\n'  + repr(self.args)
            previous = True
        if self.stdin:
            if previous:
                output += '\n'
            output += 'stdin:\n' + self.stdin
            previous = True
        return output

class Output:
    def __init__(self, stdout, stderr = '', exit_status = 0):
        self.stdout         = stdout
        self.stderr         = stderr
        self.exit_status    = exit_status

    def __eq__(self, other):
        return (type(other) is type(self)
            and self.__dict__ == other.__dict__)

    def __repr__(self):
        output = ""
        previous = False
        if self.stdout:
            output += 'stdout:\n' + self.stdout
        if self.stderr:
            if previous:
                output += '\n'
            output += 'stderr:\n' + self.stderr
            previous = True
        if self.exit_status != 0:
            if previous:
                output += '\n'
            output += 'exit_status: ' + str(self.exit_status)
        return output

class IO:
    """
    Contains an Input Output pair, and information on how to run the input.

    Has many creation default parameters so that the least used parameters do not need to be specified.
    """
    def __init__(self, name, stdin="", stdout="", args = [], stderr = "", exit_status = 0):
        self.name = name
        self.input = Input(stdin, args)
        self.output_expected = Output(stdout, stderr, exit_status)

    def run(self, program_path):
        """
        Run test case using given program.

        :returns: empty str it test passed, error message if test failed.
        :rtype:   str
        """
        return_val = ""
        command = [program_path]
        command.extend(self.input.args)
        process = subprocess.Popen(
            command,
            shell  = False,
            stdin  = subprocess.PIPE,
            stdout = subprocess.PIPE,
            stderr = subprocess.PIPE
        )
        stdout, stderr = process.communicate(self.input.stdin)
        exit_status = process.wait()
        output = Output(stdout, stderr, exit_status)
        if not output == self.output_expected:
            return_val += (
                self.name + "\n"
                + (60 * "=") + "\n"
                + "Input\n" + repr(self.input) + "\n"
                + (40 * "-") + "\n"
                + "Output\n" + repr(output) + "\n"
                + (40 * "-") + "\n"
                + "Expected output\n" + repr(self.output_expected))
        return return_val

if __name__ == '__main__':
    try:
        test_io = imp.load_source('test_io', 'test_io.py')
    except IOError:
        print "No test_io.py file."
        sys.exit(1)
    if test_io:
        os.chdir(sys.argv[1])
        program_path = os.path.abspath(sys.argv[2])
        errors = []
        for inout in test_io.inouts:
            error = IO(*inout).run(program_path)
            if error:
                errors.append(error)
                sys.stdout.write('F')
            else:
                sys.stdout.write('.')
            sys.stdout.flush()
        print "\n"
        if errors:
            print "\n\n".join(errors)
        else:
            print "All tests passed."
