#!/usr/bin/python

"""@package tester.py.in
Integration tests for obfsproxy.

The obfsproxy binary is assumed to exist in the current working
directory, and you need to have Python 2.6 or better (but not 3).
You need to be able to make connections to arbitrary high-numbered
TCP ports on the loopback interface.
"""

import difflib
import errno
import multiprocessing
import Queue
import re
import signal
import socket
import struct
import subprocess
import time
import traceback
import unittest

def diff(label, expected, received):
    """
    Helper: generate unified-format diffs between two named strings.
    Pythonic escaped-string syntax is used for unprintable characters.
    """
    if expected == received:
        return ""
    else:
        return (label + "\n"
                + "\n".join(s.encode("string_escape")
                            for s in
                            difflib.unified_diff(expected.split("\n"),
                                                 received.split("\n"),
                                                 "expected", "received",
                                                 lineterm=""))
                + "\n")

class Obfsproxy(subprocess.Popen):
    """
    Helper: Run obfsproxy instances and confirm that they have
    completed without any errors.
    """
    def __init__(self, *args, **kwargs):
        """Spawns obfsproxy with 'args'"""
        argv = ["../../obfsproxy.py", "--no-log"]
        if len(args) == 1 and (isinstance(args[0], list) or
                               isinstance(args[0], tuple)):
            argv.extend(args[0])
        else:
            argv.extend(args)

        subprocess.Popen.__init__(self, argv,
                                  stdin=open("/dev/null", "r"),
                                  stdout=subprocess.PIPE,
                                  stderr=subprocess.PIPE,
                                  **kwargs)

    severe_error_re = re.compile(r"\[(?:warn|err(?:or)?)\]")

    def check_completion(self, label, force_stderr):
        """
        Checks the output and exit status of obfsproxy to see if
        everything went fine.

        Returns an empty string if the test was good, otherwise it
        returns a report that should be printed to the user.
        """
        if self.poll() is None:
            self.send_signal(signal.SIGINT)

        (out, err) = self.communicate()

        report = ""
        def indent(s):
            return "| " + "\n| ".join(s.strip().split("\n"))

        # exit status should be zero
        if self.returncode > 0:
            report += label + " exit code: %d\n" % self.returncode
        elif self.returncode < 0:
            report += label + " killed: signal %d\n" % -self.returncode

        # there should be nothing on stdout
        if out != "":
            report += label + " stdout:\n%s\n" % indent(out)

        # there will be debugging messages on stderr, but there should be
        # no [warn], [err], or [error] messages.
        if force_stderr or self.severe_error_re.search(err):
            report += label + " stderr:\n%s\n" % indent(err)

        return report

    def stop(self):
        """Terminates obfsproxy."""
        if self.poll() is None:
            self.terminate()

def connect_with_retry(addr):
    """
    Helper: Repeatedly try to connect to the specified server socket
    until either it succeeds or one full second has elapsed.  (Surely
    there is a better way to do this?)
    """

    retry = 0
    while True:
        try:
            return socket.create_connection(addr)
        except socket.error, e:
            if e.errno != errno.ECONNREFUSED: raise
            if retry == 20: raise
            retry += 1
            time.sleep(0.05)

SOCKET_TIMEOUT = 1.0

class ReadWorker(object):
    """
    Helper: In a separate process (to avoid deadlock), listen on a
    specified socket.  The first time something connects to that socket,
    read all available data, stick it in a string, and post the string
    to the output queue.  Then close both sockets and exit.
    """

    @staticmethod
    def work(address, oq):
        listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        listener.bind(address)
        listener.listen(1)
        (conn, remote) = listener.accept()
        listener.close()
        conn.settimeout(SOCKET_TIMEOUT)
        data = ""
        try:
            while True:
                chunk = conn.recv(4096)
                if chunk == "": break
                data += chunk
        except socket.timeout:
            pass
        except Exception, e:
            data += "|RECV ERROR: " + e
        conn.close()
        oq.put(data)

    def __init__(self, address):
        self.oq = multiprocessing.Queue()
        self.worker = multiprocessing.Process(target=self.work,
                                              args=(address, self.oq))
        self.worker.start()

    def get(self):
        """
        Get a chunk of data from the ReadWorker's queue.
        """
        rv = self.oq.get(timeout=SOCKET_TIMEOUT+0.1)
        self.worker.join()
        return rv

    def stop(self):
        if self.worker.is_alive(): self.worker.terminate()

# Right now this is a direct translation of the former int_test.sh
# (except that I have fleshed out the SOCKS test a bit).
# It will be made more general and parametric Real Soon.

ENTRY_PORT  = 4999