#!/usr/bin/env python3
#
# This script takes a JSON file with the following format as input:
# [
#      {
#          "test": "TestSite.js",
#          "login": "login",
#          "password": "password",
#          "url": [
#              "http://www.site-to-test.com/",
#              "http://www.site-to-test.de/"
#              ]
#      },
#      {
#          "test": "TestAnotherSite.js",
#          "login": "login",
#          "password": "password",
#          "url": [ "http://www.anothersite-to-test.com/" ]
#      },
#      {
#          "test": "TestYetAnotherSite.js",
#          "login" : "",
#          "password": "",
#          "url" : [ "http://example.com" ]
#      },
#      ...
#  ]
#
# It executes test.js for each test script and URL.
#

"""Usage: test-runner.py [options] [credential_filename]"""


import subprocess
import time
import json
import sys
import os
import shutil

from glob import glob
from optparse import OptionParser
from contextlib import contextmanager
from xml.sax.saxutils import escape


parser = OptionParser(usage=__doc__)
parser.add_option('-f',
                  '--format',
                  dest='format',
                  choices=['xml', 'text'],
                  default='text',
                  help='Format of the test results: xml or text.')
parser.add_option('-o',
                  '--output',
                  dest='output',
                  help='Write XML to this filename.')
parser.add_option('-s',
                  '--show-window',
                  dest='show_window',
                  action='store_true',
                  default=False,
                  help='Display each test running in browser window.')
parser.add_option('-t',
                  '--test',
                  dest='script',
                  default='',
                  help='Script to test. Example: --test GMail')
parser.add_option('-l',
                  '--local',
                  dest='local',
                  default='',
                  help='Search script files (*.user.js and *.test.js) in a local folder instead of grabbing one from LP. Example: --local ~/scripts/')
options, args = parser.parse_args()


# Force XML format if the user chooses an output file.
if options.output:
    options.format = 'xml'


STARTED = time.time()
CREDENTIALS_FILENAME = args[0] if args else 'site-credentials'
TIMEOUT = 150 # seconds
XML_TEMPLATE = """
<testsuite errors="{n_errors}" failures="{n_failures}" name="" tests="{n_tests}" time="{runtime}">
{output}
</testsuite>
"""
FAILURE_TEMPLATE = """
<testcase classname="__main__.SiteTest" name="testSite({name} {url} {login})" time="{runtime}">
<failure type="AssertionError">
{message}
</failure>
</testcase>
"""
PASSING_TEMPLATE = """
<testcase classname="__main__.SiteTest" name="testSite({name} {url} {login})" time="{runtime}" />
"""


try:
    with open(CREDENTIALS_FILENAME, 'r') as fd:
        creds = json.loads(fd.read())
except Exception as e:
    exit(e + '\nCould not read JSON from site-credentials file.\n' + __doc__)


tested = []
failed = []
output = []


def grab_remote_lp_userscript(name, filename):
    with open(filename, 'w') as fd:
        args = ('bzr', 'cat', 'lp:unity-webapps-{}/{}'.format(
            name.lower(), filename))
        print(' '.join(args))
        subprocess.Popen(args, stdout=fd).communicate()


@contextmanager
def get_files(name):
    """Download files when needed, and cleanup when done."""
    files = []
    for ftype in ('test', 'user'):
        filename = '{}.{}.js'.format(name, ftype)
        files.append(filename)
        if not options.local:
            grab_remote_lp_userscript(name, filename)
        else:
            userscript = os.path.join(options.local, filename)
            print('Using local copy: "{}"'.format(userscript))
            if not os.path.exists(userscript):
                raise Exception("Could not find userscript file: " + userscript)
            shutil.copy(userscript, os.getcwd())
    yield
    for filename in files:
        try:
            print('Removing {}...'.format(filename))
            os.unlink(filename)
        except Exception as e:
            print('Failed to clean {}: {}'.format(filename, e))
    print()


def run_test_js(name, url, login=None, password=None):
    """Invoke test.js test case appropriately."""
    args = ['./test.js']
    if options.show_window:
        args.append('--show-window')
    if login:
        args.append('--login')
        args.append(login)
    if password:
        args.append('--password')
        args.append(password)
    if url:
        args.append('--url')
        args.append(url)
    args.append('{}.test.js'.format(name))
    print(' '.join(args))

    begin = time.time()

    try:
        testjs = subprocess.Popen(
            args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except Exception as e:
        print('Failed to launch test.js: ' + e)
        return

    while True:
        time.sleep(1)
        time_lapse = time.time() - begin
        if testjs.poll() is not None:
            break # Process finished, stop waiting
        elif time_lapse > TIMEOUT:
            testjs.terminate()

    log, err = [ data.decode() for data in testjs.communicate() ]
    url = escape(url)

    runtime = time.time() - begin
    if testjs.returncode != 0:
        failed.append(name)
        message = escape(err + '\n\n' + log)
        output.append(FAILURE_TEMPLATE.format(**locals()))
        print(err)
        print(log)
    else:
        output.append(PASSING_TEMPLATE.format(**locals()))


if options.local is not None and len(options.local) != 0 and not options.script:
    userscripts = glob(os.path.join(options.local, '*.user.js'))
    if len(userscripts) == 1:
        options.script = os.path.basename(userscripts[0]).replace('.user.js', '')
        print('Using locally found {} as the default test name.'.format(
            options.script))
    else:
        exit('Not clear which .user.js file to test, please specify --script.')


# Iterate over credentials listed in site-credentials file.
for cred in creds:
    login = cred.get('login')
    password = cred.get('password')
    name = cred['test'].split('.')[0]

    if options.script and name != options.script.split('.')[0]:
        # If a specific test was chosen, don't run anything else
        continue

    # Call test.js appropriately
    with get_files(name):
        for url in cred.get('url', []):
            tested.append(url)
            run_test_js(name, url, login, password)


# Notify of failures.
if options.format == 'xml':
    xml = XML_TEMPLATE.format(
        n_errors=0,
        n_failures=len(failed),
        n_tests=len(tested),
        runtime=time.time() - STARTED,
        output=''.join(output),
    )
    if options.output:
        with open(options.output, 'w') as fd:
            print(xml, file=fd)
    else:
        print(xml)


print('{} FAILURES'.format(len(failed)))
exit(len(failed))
