#!/usr/bin/env python3

"""Mininet test runner

 * must be run as root
 * you can run a specific test case only, by adding the class name of the test
   case to the command. Eg ./mininet_test.py FaucetUntaggedIPv4RouteTest

It is strongly recommended to run these tests via Docker, to ensure you have
all dependencies correctly installed. See ../docs/.
"""

# pylint: disable=missing-docstring

import argparse
import collections
import copy
import cProfile
import json
import glob
import inspect
import os
import sys
import multiprocessing
import pstats
import random
import re
import shutil
import subprocess
import tempfile
import threading
import time
import unittest

import yaml

from packaging import version

from concurrencytest import ConcurrentTestSuite, fork_for_tests
from mininet.log import setLogLevel
from mininet.clean import Cleanup

from clib import mininet_test_util

DEFAULT_HARDWARE = 'Open vSwitch'

# Only these hardware types will be tested with meters.
SUPPORTS_METERS = (
    DEFAULT_HARDWARE,  # TODO: tested with OVS userspace only.
    'Aruba',
    'NoviFlow',
    'ZodiacGX',
)

SUPPORTS_METADATA = (
    DEFAULT_HARDWARE,
)


EXTERNAL_DEPENDENCIES = (
    ('ryu-manager', ['--version'],
     'ryu-manager', r'ryu-manager (\d+\.\d+)\n', "4.9"),
    ('ovs-vsctl', ['--version'], 'Open vSwitch',
     r'ovs-vsctl\s+\(Open vSwitch\)\s+(\d+\.\d+)\.\d+\n', "2.3"),
    ('tcpdump', ['-h'], 'tcpdump',
     r'tcpdump\s+version\s+(\d+\.\d+)\.\d+\n', "4.5"),
    ('nc', ['-h'], 'OpenBSD netcat', '', 0),
    ('vconfig', [], 'the VLAN you are talking about', '', 0),
    ('fuser', ['-V'], r'fuser \(PSmisc\)',
     r'fuser \(PSmisc\) (\d+\.\d+)\n', "22.0"),
    ('lsof', ['-v'], r'lsof version',
     r'revision: (\d+\.\d+)\n', "4.86"),
    ('mn', ['--version'], r'\d+\.\d+.\d+',
     r'(\d+\.\d+).\d+', "2.2"),
    ('exabgp', ['--version'], 'ExaBGP',
     r'ExaBGP : (\d+\.\d+).\d+', "4.0"),
    ('pip3', ['show', 'influxdb'], 'influxdb',
     r'Version:\s+(\d+\.\d+)\.\d+', "3.0"),
    ('curl', ['--version'], 'libcurl',
     r'curl (\d+\.\d+).\d+', "7.3"),
    ('ladvd', ['-h'], 'ladvd',
     r'ladvd version (\d+\.\d+)\.\d+', "0.9"),
    ('iperf', ['--version'], 'iperf',
     r'iperf version (\d+\.\d+)\.\d+', "2.0"),
    ('fping', ['-v'], 'fping',
     r'fping: Version (\d+\.\d+)', "3.10"),
    ('rdisc6', ['-V'], 'ndisc6',
     r'ndisc6.+tool (\d+\.\d+)', "1.0"),
    ('tshark', ['-v'], r'(?i)tshark',
     r'TShark\s*[a-zA-Z\(\)]*\s*([\d\.]+)', "2.1"),
    ('scapy', ['-h'], 'Usage: scapy', '', 0),
)

# see hw_switch_config.yaml for how to bridge in an external hardware switch.
HW_SWITCH_CONFIG_FILE = 'hw_switch_config.yaml'
CONFIG_FILE_DIRS = ['/etc/faucet', './', '/faucet-src']
REQUIRED_TEST_PORTS = 4

REQUIRED_HW_CONFIG = {
    'dp_ports': (dict,),
    'cpn_intf': (str,),
    'dpid': (int,),
    'hardware': (str,),
    'hw_switch': (bool,),
    'of_port': (int,),
    'gauge_of_port': (int,),
}

OPTIONAL_HW_CONFIG = {
    'cpn_ipv6': (bool,),
    'ctl_privkey': (str,),
    'ca_certs': (str,),
    'ctl_cert': (str,),
}

ALL_HW_CONFIG = {**REQUIRED_HW_CONFIG, **OPTIONAL_HW_CONFIG}


def import_hw_config():
    """Import configuration for physical switch testing."""
    for config_file_dir in CONFIG_FILE_DIRS:
        config_file_name = os.path.join(config_file_dir, HW_SWITCH_CONFIG_FILE)
        if os.path.isfile(config_file_name):
            break
    if os.path.isfile(config_file_name):
        print('Using config from %s' % config_file_name)
    else:
        print('Cannot find %s in %s' % (HW_SWITCH_CONFIG_FILE, CONFIG_FILE_DIRS))
        sys.exit(-1)
    try:
        with open(config_file_name, 'r') as config_file:
            config = yaml.safe_load(config_file)
    except IOError:
        print('Could not load YAML config data from %s' % config_file_name)
        sys.exit(-1)
    if config.get('hw_switch', False):
        unknown_keys = set(config.keys()) - set(ALL_HW_CONFIG.keys())
        if unknown_keys:
            print('unknown config %s in %s' % (unknown_keys, config_file_name))
            sys.exit(-1)
        missing_required_keys = set(REQUIRED_HW_CONFIG.keys()) - set(config.keys())
        if missing_required_keys:
            print('missing required config: %s' % missing_required_keys)
            sys.exit(-1)
        for config_key, config_value in config.items():
            valid_types = ALL_HW_CONFIG[config_key]
            valid_values = [
                config_value for valid_type in valid_types
                if isinstance(config_value, valid_type)]
            if not valid_values:
                print('%s (%s) must be of type %s in %s' % (
                    config_key, config_value,
                    valid_types, config_file_name))
                sys.exit(-1)
        dp_ports = config['dp_ports']
        if len(dp_ports) < REQUIRED_TEST_PORTS:
            print('At least %u dataplane ports are required, '
                  '%d are provided in %s.' %
                  (REQUIRED_TEST_PORTS, len(dp_ports), config_file_name))
            sys.exit(-1)
        return config
    return None


def check_dependencies():
    """Verify dependant libraries/binaries are present with correct versions."""
    print('Checking library/binary dependencies')
    for (binary, binary_get_version, binary_present_re,
         binary_version_re, binary_minversion) in EXTERNAL_DEPENDENCIES:
        binary_args = [binary] + binary_get_version
        required_binary = 'required binary/library %s' % (
            ' '.join(binary_args))
        try:
            proc = subprocess.Popen(
                binary_args,