# -*- coding: utf-8 -*-
# @Author: Cody Kochmann
# @Date:   2017-04-27 12:49:17
# @Last Modified 2018-03-12
# @Last Modified time: 2020-04-05 11:01:47

"""
battle_tested - automated function fuzzing library to quickly test production
                code to prove it is "battle tested" and safe to use.

Examples of Primary Uses:

    from battle_tested import fuzz

    def test_function(a,b,c):
        return c,b,a

    fuzz(test_function)
    # or to collect tests
    fuzz(test_function, keep_testing=True)

Or:

    from battle_tested import battle_tested

    @battle_tested()
    def test_function(a,b,c):
        return c,b,a

"""

from __future__ import print_function, unicode_literals
import builtins
from collections import deque
from functools import wraps, partial
from gc import collect as gc
from generators.inline_tools import attempt
from hypothesis import given, strategies as st, settings, Verbosity
from hypothesis.errors import HypothesisException
from itertools import product, cycle, chain, islice
from multiprocessing import Process, Queue, cpu_count as multi_cpu_count
from prettytable import PrettyTable
from random import choice, randint
from re import findall
from stricttuple import stricttuple
from string import ascii_letters, digits
from time import sleep
from time import time
import generators as gen
import logging
import os
import signal
import sys
import traceback



__all__ = 'battle_tested', 'fuzz', 'disable_traceback', 'enable_traceback', 'garbage', 'crash_map', 'success_map', 'results', 'stats', 'print_stats', 'function_versions', 'time_all_versions_of', 'easy_street', 'run_tests', 'multiprocess_garbage'


# try to set the encoding
attempt(lambda: (reload(sys), sys.setdefaultencoding('utf8')))

class hardware:
    ''' single reference of what hardware the system is working with '''
    # get the count of cpu cores, if it fails, assume 1 for safety
    cpu_count = attempt(multi_cpu_count, default_output=1)
    single_core = cpu_count == 1

class float(float): # this patches float.__repr__ to work correctly
    def __repr__(self):
        if all(i in '1234567890.' for i in builtins.float.__repr__(self)):
            return 'float({})'.format(builtins.float.__repr__(self))
        else:
            return 'float("{}")'.format(builtins.float.__repr__(self))

class complex(complex): # this patches float.__repr__ to work correctly
    def __repr__(self):
        return 'complex("{}")'.format(builtins.complex.__repr__(self))

def compilable(src):
    return attempt(
        lambda:(compile(src, 'waffles', 'exec'), True)[1] ,
        False
    )

def runnable(src):
    return attempt(
        lambda:(eval(compile(src, 'waffles', 'exec')), True)[1] ,
        False
    )

def runs_fine(src):
    return attempt(
        lambda:(eval(src), True)[1] ,
        False
    )

def valid_repr(o):
    ''' returns true if the object has a valid repr '''
    return attempt(
        lambda: (eval(repr(o)) == o) or (eval(repr(o)) is o),
        False
    )

class unittest_builder(object):
    @staticmethod
    def test_body(fn, test_code):
        ''' call this to add the code needed for a full unittest script '''
        d = {
            'function_path':fn.__code__.co_filename,
            'function_name':fn.__name__,
            'module_name':'.'.join(os.path.basename(fn.__code__.co_filename).split('.')[:-1]),
            'test_code': test_code
        }
        return '''#!/usr/bin/env python
# -*- coding: utf-8 -*-

import unittest
from uuid import UUID
from fractions import Fraction
import sys
import os.path

sys.path.append(os.path.dirname("{function_path}"))
from {module_name} import {function_name}

class Test_{function_name}(unittest.TestCase):
    """ automated unittest generated by battle_tested """{test_code}

if __name__ == '__main__':
    unittest.main()
'''.format(**d)

    @staticmethod
    def equal_test(test_name, invocation_code, output):
        ''' generate tests that assert that the input equals the output '''
        return '''
    def test_{}(self):
        self.assertEqual({}, {})'''.format(test_name, invocation_code, repr(output))

    @staticmethod
    def raises_test(test_name, invocation_code, ex_type):
        ''' generate a unittest that asserts that a certain input raises the given exception '''
        return '''
    def test_{}(self):
        with self.assertRaises({}):
            {}'''.format(test_name, ex_type.__name__, invocation_code.replace('nan', 'float("""nan""")'))

def getsource(fn):
    ''' basically just inspect.getsource, only this one doesn't crash as much '''
    from inspect import getsource
    try:
        return getsource(fn)
    except:
        return attempt(lambda: '{}'.format(fn), default_output='')

def pin_to_cpu(core_number):
    ''' pin the current process to a specific cpu to avoid dumping L1 cache'''
    assert type(core_number) == int, 'pin_to_cpu needs an int as the argument'
    # just attempt this, it wont work on EVERY system in existence
    attempt(lambda: os.sched_setaffinity(os.getpid(), (core_number,)))

def renice(new_niceness):
    ''' renice the current process calling this function to the new input '''
    assert type(new_niceness) == int, 'renice needs an int as its argument'
    # just attempt this, it wont work on EVERY system in existence
    attempt(lambda: os.nice(new_niceness))

pin_to_cpu(0)  # pin this main process to the first core
renice(15)  # renice this main process, idk why 15, but it gives room for priorities above and below

def shorten(string, max_length=80, trailing_chars=3):
    ''' trims the 'string' argument down to 'max_length' to make previews to long string values '''
    assert type(string).__name__ in {'str', 'unicode'}, 'shorten needs string to be a string, not {}'.format(type(string))
    assert type(max_length) == int, 'shorten needs max_length to be an int, not {}'.format(type(max_length))
    assert type(trailing_chars) == int, 'shorten needs trailing_chars to be an int, not {}'.format(type(trailing_chars))
    assert max_length > 0, 'shorten needs max_length to be positive, not {}'.format(max_length)
    assert trailing_chars >= 0, 'shorten needs trailing_chars to be greater than or equal to 0, not {}'.format(trailing_chars)

    return (
        string
    ) if len(string) <= max_length else (
        '{before:}...{after:}'.format(
            before=string[:max_length-(trailing_chars+3)],
            after=string[-trailing_chars:] if trailing_chars>0 else ''
        )
    )

class easy_street:
    ''' This is a namespace for high speed test generation of various types '''

    @staticmethod
    def chars():
        test_chars = ascii_letters + digits
        for _ in gen.loop():
            for combination in product(test_chars, repeat=4):
                for i in combination:
                    yield i

    @staticmethod
    def strings():
        test_strings = [
            '',
            'exit("######## WARNING this code is executing strings blindly ########")'
        ]
        # this snippet rips out every word from doc strings
        test_strings += list(set(findall(
            r'[a-zA-Z\_]{1,}',
            [v.__doc__ for v in globals().values() if hasattr(v, '__doc__')].__repr__()
        )))

        for _ in gen.loop():
            for combination in product(test_strings, repeat=4):
                for i in combination:
                    yield i

    @staticmethod
    def bools():
        booleans = (True, False)
        for _ in gen.loop():
            for combination in product(booleans, repeat=4):
                for i in combination:
                    yield i

    @staticmethod
    def ints():
        numbers = tuple(range(-33,65))
        for _ in gen.loop():
            for combination in product(numbers, repeat=3):
                for i in combination:
                    yield i

    @staticmethod
    def floats():
        non_zero_ints = (i for i in easy_street.ints() if i != 0)
        stream1 = gen.chain(i[:8] for i in gen.chunks(non_zero_ints, 10))
        stream2 = gen.chain(i[:8] for i in gen.chunks(non_zero_ints, 12))
        for i in stream1:
            yield next(stream2)/(1.0*i)

    @staticmethod
    def lists():
        strategies = easy_street.strings(), easy_street.ints(), easy_street.floats(), easy_street.bools()
        strategies = list(gen.chain(product(strategies, repeat=len(strategies))))
        lengths = cycle(list(range(0, 21)))

        for _ in gen.loop():
            for length in lengths:
                for strat in strategies:
                    yield [st for st in islice(strat, length)]

    @staticmethod
    def tuples():
        for i in easy_street.lists():
            yield tuple(i)

    @staticmethod
    def dicts():
        strategies = easy_street.strings(), easy_street.ints(), easy_street.floats(), easy_street.bools()
        strategies = list(gen.chain(product(strategies, repeat=len(strategies))))
        lengths = cycle(list(range(0, 21)))

        for _ in gen.loop():
            for length in lengths:
                for strat in strategies:
                    yield { k:v for k,v in gen.chunks(islice(strat,length*2), 2) }

    @staticmethod
    def sets():
        strategies = easy_street.strings(), easy_street.ints(), easy_street.floats(), easy_street.bools()
        strategies = list(gen.chain(product(strategies, repeat=len(strategies))))
        lengths = cycle(list(range(0, 21)))

        for _ in gen.loop():
            for length in lengths:
                for strat in strategies:
                    yield {i for i in islice(strat, length)}

    @staticmethod
    def garbage():
        while 1:
            strategies = (
                easy_street.strings(),
                easy_street.ints(),
                easy_street.floats(),
                easy_street.bools(),
                easy_street.dicts(),
                easy_street.sets(),
                easy_street.lists(),
                easy_street.tuples()
            )
            for strat in gen.chain(product(strategies, repeat=len(strategies))):
                yield next(strat)

class StrategyNotPopulatingBufferException(Exception):
    ''' raised if a hypothesis strategy is not populating a buffer at all '''

def strategy_stream(strat, buffer_limit=64):
    ''' emulates what strategy.example() use to do but now with a buffered generator '''

    # define a buffer to work with
    buffer = deque(maxlen=buffer_limit)


    # loop forever, clearing the buffer every time
    while buffer.clear() == None:

        # populate the buffer with a hypothesis test that runs enough to fill the buffer
        @settings(database=None, max_examples=buffer_limit)
        @given(strat)
        def buffer_append(data):
            nonlocal buffer
            return buffer.append(data)

        # run the hypothesis test
        buffer_append()
        
        if len(buffer) == 0:
            raise StrategyNotPopulatingBufferException(f'buffer was found empty, it appears strategy[{strat}] is not producing anything')
        
        # empty the buffer out
        yield from buffer


def background_strategy(strats, q):
    target_core = q.get()
    renice(20) # maximize niceness
    if not hardware.single_core:
        pin_to_cpu(target_core)
    q_put = q.put
    
    # create a stream that cycles through the provided strategies 
    stream = cycle(strategy_stream(s) for s in strats)

    # rotate through the strategies appending their outputs into the queue
    for strat in stream:
        try:
            q_put(next(strat))
        except:
            pass


def background_manager(child_queues, q):
    if not hardware.single_core:
        pin_to_cpu(1)
    renice(20)
    q_put = q.put
    for cq in cycle(child_queues):
        try:
            item = cq.get_nowait()
            q_put(item)
        except:
            sleep(0.0001)


def multiprocess_garbage():
    basics = (
        st.binary(),
        st.booleans(),
        st.characters(),
        st.complex_numbers(),
        st.floats(),
        st.uuids(),
        st.fractions(),
        st.integers(),
        st.decimals(),
        st.dates(),
        st.datetimes(),
        st.dates().map(str),
        st.datetimes().map(str),
        st.none(),
        st.text(),
        st.dictionaries(keys=st.text(), values=st.text())
    )


    hashables = tuple(s for s in basics if hashable_strategy(s))

    lists = tuple(st.lists(elements=i) for i in basics)
    tuples = tuple(st.lists(elements=i).map(tuple) for i in basics)
    sets = tuple(st.sets(elements=i) for i in hashables)
    dictionaries = tuple(st.dictionaries(keys=st.one_of(*hashables), values=i) for i in basics)

    strats = basics + lists + tuples + sets + dictionaries

    # add logic here that plays on `if hardware.single_core:` to set up single core stuff cleanly

    # if more than two cores, use special core logic
    # master has 0, collector has 1
    if hardware.cpu_count > 2: # logic for 3 or more cores
        cores_used_for_generation = hardware.cpu_count - 2
        specified_cores = cycle(range(2, hardware.cpu_count))
    else:
        cores_used_for_generation = 1
        if hardware.cpu_count == 2:
            # dual core has second core do generation
            specified_cores = cycle([1])
        else:
            # single core systems do everything on the same core
            specified_cores = cycle([0])

    jobs = cycle([[] for _ in range(cores_used_for_generation)])

    for s in strats:
        next(jobs).append(s)

    jobs = [(next(jobs), Queue(4)) for _ in range(cores_used_for_generation)]

    # add specific core to each job's queue
    for job, q in jobs:
        q.put(next(specified_cores))

    processes = [
        Process(target=background_strategy, args=j)
        for j in jobs
    ]

    for p in processes:
        p.start()

    gather_queue = Queue(16)
    gather_process = Process(target=background_manager, args=([q for _, q in jobs], gather_queue))
    gather_process.start()

    try:
        fast_alternative = easy_street.garbage()
        gather_queue_full = gather_queue.full
        gather_queue_get = gather_queue.get_nowait
        fast_alternative_next = getattr(fast_alternative, ('next' if hasattr(fast_alternative, 'next') else '__next__'))
        for _ in gen.loop():  # loop forever
            try:
                yield gather_queue_get()
            except:
                yield fast_alternative_next()
        '''if gather_queue_full(): # if the queue is full, yield the value
            yield gather_queue_get()
        else:
            for _ in range(4): # dont waste time looking for a full queue, be productive while you wait
                yield next(fast_alternative)'''
    except (KeyboardInterrupt, SystemExit, GeneratorExit, StopIteration):
        gather_process.terminate() ###MP  isn't this redundant with same sequence in finally?
        gather_process.join()
        for p in processes:
            p.terminate()
            p.join()
    finally:
        gather_process.terminate()
        gather_process.join()
        for p in processes:
            p.terminate()
            p.join()


class MaxExecutionTimeError(Exception):
    pass

class max_execution_time:
    def signal_handler(self, signum, frame):
        raise self.ex_type('operation timed out')

    def __init__(self, seconds, ex_type=MaxExecutionTimeError):
        #print('setting timeout for {} seconds'.format(seconds))
        self.seconds = 1 if seconds < 1 else seconds
        self.ex_type = ex_type

    def __enter__(self):
        signal.signal(signal.SIGALRM, self.signal_handler)
        signal.alarm(self.seconds)

    def __exit__(self, *a):
        signal.alarm(0) ###MP which signal is it? MAGIC NUMBERS, this is why signals have const'ed names


def hashable_strategy(s):   ###MP predicates are nice to indicate with <is_condition> or ? if you're weird enough
    """ Predicate stating a hash-able hypothesis strategy """
    assert hasattr(s, 'example'), 'hashable_strategy needs a strategy argument'   ###MP strategies are marked up with attributes not types/base class?
    stream = strategy_stream(s)
    for i in range(32):
        sample = next(stream)
        try:
            hash(sample)
            assert type(sample) != dict
        except:
            return False
    return True

def replace_strategy_repr(strat, new_repr):
    """ replaces a strategy's repr and str functions with a custom one """
    class custom_repr_strategy(type(strat)):
        __repr__ = new_repr
        __str__ = new_repr
    return custom_repr_strategy(strategies=strat.original_strategies)

def build_garbage_strategy():
    ''' builds battle_tested's primary strategy '''
    basics = (
        st.binary(),
        st.booleans(),
        st.characters(),
        st.complex_numbers(),
        st.floats(),
        st.fractions(),
        st.integers(),
        st.none(),
        st.text(),
        st.uuids(),
        st.dictionaries(keys=st.text(), values=st.text())
    )

    hashables = tuple(s for s in basics if hashable_strategy(s))

    # returns a strategy with only basic values
    any_basics = partial(st.one_of, *basics)
    # returns a strategy with only hashable values
    any_hashables = partial(st.one_of, *hashables)

    # returns a strategy of lists with basic values
    basic_lists = partial(st.lists, elements=any_basics())
    # returns a strategy of lists with hashable values
    hashable_lists = partial(st.lists, elements=any_basics())

    iterable_strategies = (
        # iterables with the same type inside
        st.builds(lambda a:[i for i in a if type(a[0])==type(i)], basic_lists(min_size=3)),
        st.builds(lambda a:tuple(i for i in a if type(a[0])==type(i)), basic_lists(min_size=3)),
        #st.builds(lambda a:{i for i in a if type(a[0])==type(i)}, hashable_lists(min_size=3)),
        st.iterables(elements=any_basics()),
        #st.builds(lambda a:(i for i in a if type(a[0])==type(i)), basic_lists(min_size=3)),
        # garbage filled iterables
        st.builds(tuple, basic_lists()),
        #st.builds(set, hashable_lists()),
        st.dictionaries(keys=any_hashables(), values=any_basics())
    )
    # returns a strategy with only iterable values
    any_iterables = partial(st.one_of, *iterable_strategies)

    return st.one_of(any_basics(), any_iterables())

garbage = replace_strategy_repr(build_garbage_strategy(), lambda s:'<garbage>')


class storage():
    """ where battle_tested stores things """
    test_inputs = deque()
    results = {}

    @staticmethod
    def build_new_examples(how_many=100):
        """ use this to add new examples to battle_tested's pre-loaded examples in storage.test_inputs """
        assert type(how_many) == int, 'build_new_examples needs a positive int as the argument'
        assert how_many > 0, 'build_new_examples needs a positive int as the argument'
        @settings(max_examples=how_many)
        @given(garbage)
        def garbage_filler(i):
            try:
                storage.test_inputs.append(i)
            except:
                pass
        try:
            garbage_filler()
        except:
            pass

    @staticmethod
    def refresh_test_inputs():
        """ wipe battle_tested test_inputs and cache new examples """
        storage.test_inputs.clear()
        try:
            # just fill test inputs with something to start with
            storage.test_inputs.append('waffles') # easter egg :)
            for i in islice(easy_street.garbage(), 64):
                storage.test_inputs.append(i)
            storage.build_new_examples()
        except Exception as e:
            pass

storage.build_new_examples.garbage = garbage

class io_example(object):
    """ demonstrates the behavior of input and output """
    def __init__(self, input_args, output):
        self.input = input_args
        self.output = output
    def __repr__(self):
        return '{} -> {}'.format(self.input,self.output)
    def __str__(self):
        return '{} -> {}'.format(self.input,self.output)  ### why not pull value of __repr__? .format cant be cheap, it's parsing and interpolation
    def __hash__(self):
        return hash('io_example') + hash(self.__repr__())
    def __eq__(self, target):
        return hasattr(target, '__hash__') and self.__hash__() == target.__hash__()

class suppress():   ###MP dead code?  i dont see it referenced anywhere?
    """ suppress exceptions coming from certain code blocks """
    def __init__(self, *exceptions):
        self._exceptions = exceptions
    def __enter__(self):
        pass
    def __exit__(self, exctype, excinst, exctb):
        return exctype is not None and issubclass(exctype, self._exceptions)

def is_py3():
    return sys.version_info >= (3, 0)

class UniqueCrashContainer(tuple):
    ''' a pretty printable container for crashes '''
    def __repr__(self):
        try:
            table = PrettyTable(('exception type','arg types','location','crash message'), sortby='exception type')
            table.align["exception type"] = "l"
            table.align["arg types"] = "l"
            table.align["location"] = "l"
            table.align["crash message"] = "l"
            for i in self:
                table.add_row((i.err_type.__name__,repr(tuple(i.__name__ for i in i.arg_types)),[x for x in i.trace.split(', ') if x.startswith('line ')][-1],i.message))
            return table.get_string()
        except:
            return tuple.__repr__(self)

class PrettyTuple(tuple):
    ''' tuples with better pretty printing '''
    def __repr__(self):
        if len(self) > 0:
            try:
                table = PrettyTable(None)
                try:
                    # attempt to sort the tuple
                    tup = tuple(sorted(self, key=repr))
                except:
                    tup = self
                # build up the PrettyTable
                for i in tup:
                    # logic for nested tuples
                    if isinstance(i, tuple):
                        # replace the <class 'int'> format for types with just the name of the type
                        t = tuple(x.__name__ if isinstance(x,type) and hasattr(x,'__name__') else repr(x) for x in i)
                        table.add_row(t)
                    else:
                        # replace the <class 'int'> format for types with just the name of the type
                        if isinstance(i, type):
                            if hasattr(i, '__name__'):
                                i = i.__name__
                            else:
                                i = repr(i)
                        table.add_row((i,))
                #table.align='l'
                return '\n'.join(table.get_string().splitlines()[2:])
            except:
                return tuple.__repr__(self)
        else:
            return '()'

class tb_controls():
    old_excepthook = sys.excepthook
    no_tracebacklimit_on_sys = 'tracebacklimit' not in dir(sys)
    old_tracebacklimit = (sys.tracebacklimit if 'tracebacklimit' in dir(sys) else None)
    traceback_disabled = False

    @staticmethod
    def disable_traceback():
        if is_py3():
            sys.tracebacklimit = None
        else:
            sys.excepthook = lambda t, v, n:tb_controls.old_excepthook(t, v, None)
        tb_controls.traceback_disabled = True

    @staticmethod
    def enable_traceback():
        if tb_controls.traceback_disabled:
            if is_py3():
                if tb_controls.no_tracebacklimit_on_sys:
                    del sys.tracebacklimit
                else:
                    sys.tracebacklimit = tb_controls.old_tracebacklimit
            else:
                sys.excepthook = tb_controls.old_excepthook
            tb_controls.traceback_disabled = False

def enable_traceback():
    """ disables tracebacks from being added to exception raises """
    tb_controls.enable_traceback()

def disable_traceback():
    """ enables tracebacks to be added to exception raises """
    tb_controls.disable_traceback()

def traceback_file_lines(trace_text=None):
    """ this returns a list of lines that start with file in the given traceback
        usage:
            traceback_steps(traceback.format_exc())
    """
    # split the text into traceback steps
    return [i for i in trace_text.splitlines() if i.startswith('  File "') and '", line' in i] ###MP extract out the condition for readability?

def traceback_steps(trace_text=None):
    """ this generates the steps in a traceback
        usage:
            traceback_steps(traceback.format_exc())
    """
    if trace_text == None:   ### is None?
        trace_text = traceback.format_exc()
    # get rid of the first line with traceback
    trace_text = ('\n'.join(trace_text.splitlines()[1:-1]))  ### split text to rejoin without first and last?  why not just slice the middle out?
    # split the text into traceback steps
    file_lines = [i for i in trace_text.splitlines() if '", line' in i and i.startswith('  File "') ]
    # build the output
    out = []
    for i in trace_text.splitlines():
        if i in file_lines:
            if len(out):
                yield '\n'.join(out)   ###MP why split then rejoin later again?
            out = [i]
        else:
            out.append(i)
    yield '\n'.join(out)

def traceback_text():
    """ this returns the traceback in text form """
    return('\n'.join(i for i in traceback_steps()))

def format_error_message(f_name, err_msg, trace_text, evil_args):
    top_line = " battle_tested crashed {f_name:}() ".format(f_name=f_name)
    while len(top_line) < 79:
        top_line = "-{}-".format(top_line)
    top_line = '\n\n{}'.format(top_line)
    bottom_line = '-'*len(top_line)

    break_path = trace_text.split('"')[1]
    break_line_number = int(trace_text.split(',')[1].split(' ')[-1])
    break_line_number_up = break_line_number-1
    break_line_number_down = break_line_number+1

    out = """{top_line:}

Error Message:

   {err_msg:}

Breakpoint: {break_path:} - line {break_line_number:}""".format(
        top_line=top_line,
        err_msg=err_msg,
        break_path=break_path,
        break_line_number=break_line_number
    )   ###MP put the fields in a dict, let format unpack it into the right fields

    try:
        with open(break_path) as f:
            for i, line in enumerate(f):
                i+=1
                if i == break_line_number_up:
                    line_above=line.replace('\n','')
                if i == break_line_number:
                    break_line=line.replace('\n','')
                if i == break_line_number_down:
                    line_below=line.replace('\n','')

        out += """
  {break_line_number_up:>{num_len:}}|{line_above:}
->{break_line_number:>{num_len:}}|{break_line:}
  {break_line_number_down:>{num_len:}}|{line_below:}""".format(
            break_line_number_up=break_line_number_up,
            break_line_number=break_line_number,
            break_line_number_down=break_line_number_down,
            line_above=line_above,
            line_below=line_below,
            break_line=break_line,
            num_len=len(str(break_line_number_down))+1
        )
    except Exception as ex:
        # i only want this part if the whole file read works
        pass
    out += """
To reproduce this error, run:

   {f_name:}{evil_args:}

{bottom_line:}
""".format(
        bottom_line=bottom_line,
        f_name=f_name,
        evil_args=evil_args,
        )
    return out


class generators(object):
    def started(generator_function):
        """ starts a generator when created """
        def wrapper(*args, **kwargs):
            g = generator_function(*args, **kwargs)
            next(g)
            return g
        return wrapper

    @staticmethod
    @started
    def sum():
        "generator that holds a sum"
        total = 0
        while 1:
            total += yield total

    @staticmethod
    @started
    def counter():  ###MP why does a counter need to be a generator?
        """generator that holds a sum"""
        c = 0
        while 1:
            i = yield c
            if i is None:
                c += 1
            else:
                c += i

    @staticmethod
    @started
    def avg():
        """ generator that holds a rolling average """
        count = 0.0
        total = generators.sum()
        i=0
        while 1:
            i = yield (((total.send(i)*1.0)/count) if count else 0)
            count += 1

    @staticmethod
    def timer():
        """ generator that tracks time """
        start_time = time()
        while 1:
            yield time()-start_time

    @staticmethod
    def countdown(seconds):
        """ yields True until time expires """
        start = time()
        while 1:
            yield time()-start < seconds

    @staticmethod
    def chunks(itr, size):      ###MP isn't this a copy of stuff from generators?
        """ yields a windowed chunk of a given size """
        out = deque(maxlen=size)
        for i in itr:
            out.append(i)
            if len(out) == size:
                yield tuple(out)
                out.clear()

    @staticmethod
    def chain(*a):      ###MP isn't this a copy of stuff from generators?
        """itertools.chain, just better"""
        for g in a:
            if hasattr(g, '__iter__'):
                # iterate through if its iterable
                for i in g:
                    yield i
            else:
                # just yield the whole thing if its not
                yield g

    @staticmethod
    def every_possible_object(iterable):
        """ like flatten, just more desperate """
        try:
            for i in iterable:
                yield i
                if isinstance(i, dict):
                    for k in i:
                        yield k
                    for v in i.values():
                        for i in generators.every_possible_object(v):
                            yield i
                elif isinstance(i, (list,tuple,set)):
                    for i in generators.every_possible_object(i):
                        yield i
        except TypeError:
            pass
        yield iterable


class FuzzTimeoutError(BaseException):
    pass

from threading import Timer

class IntervalTimer(object):    ###MP some classes are explicitly inheriting from object, others are not. Inconsistent
    """ run functions on intervals in the background
        by: Cody Kochmann
    """
    def __init__(self, seconds, function):
        assert type(seconds).__name__ in ('int','float')
        assert callable(function)
        self.seconds=seconds
        self.function=function
        self.stopped=False
        self.running=False
        self.thread=Timer(self.seconds,self.function)

    def start(self):
        if self.thread.is_alive():
            self.thread.join()
        if not self.stopped:
            if not self.running:
                self.function()
                self.running=True
            self.thread=Timer(self.seconds,self.function)
            self.thread.start()
            self.restart_thread=Timer(self.seconds, self.start)
            self.restart_thread.start()

    def stop(self):
        self.stopped = True
        self.running = False

        try:
            self.thread.cancel()
        except AttributeError: pass

        try:
            self.restart_thread.cancel()
        except AttributeError: pass

from io import StringIO
def run_silently(fn):
    """ runs a function silently with no stdout """
    stdout_holder = sys.stdout
    sys.stdout = StringIO()
    fn()
    sys.stdout = stdout_holder

class ipython_tools(object):
    """ tools to make battle_tested work with ipython nicely """
    detected = 'IPython' in sys.modules
    if detected:
        from IPython import get_ipython
        detected = get_ipython() is not None
    if detected:
        magic = get_ipython().magic

    @staticmethod
    def silence_traceback():
        """ silences ipythons verbose debugging temporarily """
        if ipython_tools.detected:
            # this hijacks stdout because there is a print in ipython.magic
            run_silently(lambda:ipython_tools.magic("xmode Plain"))

    @staticmethod
    def verbose_traceback():
        """ re-enables ipythons verbose tracebacks """
        if ipython_tools.detected:
            ipython_tools.magic("xmode Verbose")


def function_arg_count(fn):
    """ finds how many args a function has """
    assert callable(fn), 'function_arg_count needed a callable function, not {0}'.format(repr(fn))
    if hasattr(fn, '__code__') and hasattr(fn.__code__, 'co_argcount'):
        # normal functions
        return fn.__code__.co_argcount
    elif hasattr(fn, 'args') and hasattr(fn, 'func') and hasattr(fn, 'keywords'):
        # partials
        return function_arg_count(fn.func) - (len(fn.args)+len(fn.keywords))
    else:
        number_of_args_that_work = []
        for i in range(1,64):
            try:
                fn(*range(i))
            except TypeError as ex:
                search = findall(r'((takes (exactly )?(one|[0-9]{1,}))|(missing (one|[0-9]{1,})))', repr(ex))
                our_specific_type_error = len(repr(findall(r'((takes (exactly )?(one|[0-9]{1,}))|(missing (one|[0-9]{1,})))', repr(ex))))>10
                if not our_specific_type_error: # if you find something
                    number_of_args_that_work.append(i)
                pass
            except Exception:
                #number_of_args_that_work.append(i)
                pass
            else:
                number_of_args_that_work.append(i)
        if len(number_of_args_that_work):
            return min(number_of_args_that_work)
        #logging.warning('using backup plan')
        return 1 # not universal, but for now, enough... :/


class battle_tested(object):
    """
battle_tested - automated function fuzzing library to quickly test production
                code to prove it is "battle tested" and safe to use.

Examples of Primary Uses:

    from battle_tested import fuzz

    def my_adder(a, b):
        ''' switches the variables '''
        return b + a

    fuzz(my_adder) # returns a report of what works/breaks

Or:

    from battle_tested import battle_tested

    @battle_tested(keep_testing=False, allow=(AssertionError,))
    def my_strict_add(a, b):
        ''' adds a and b together '''
        assert isinstance(a, int), 'a needs to be an int'
        assert isinstance(b, int), 'b needs to be an int'
        return a + b

    # This runs tests and halts the program if there is an error if that error
    # isn't an AssertionError. This tests if you've written enough assertions.

Parameters:

    fn           - the function to be fuzzed (must accept at least one argument)
    seconds      - maximum time battle_tested is allowed to fuzz the function
    max_tests    - maximum number of tests battle_tested will run before exiting
                   (if the time limit doesn't come first)
    verbose      - setting this to False makes battle_tested raise the first
                   exception that wasn't specifically allowed in the allow option
    keep_testing - setting this to True allows battle_tested to keep testing
                   even after it finds the first falsifying example, the results
                   can be accessed with crash_map() and success_map()
    quiet        - setting this to True silences all of the outputs coming from
                   the test
    allow        - this can be a tuple of exception types that you want
                   battle_tested to skip over in its tests

"""

    def __init__(self, seconds=6, max_tests=1000000, keep_testing=True, verbose=False, quiet=False, allow=(), strategy=garbage, **kwargs):
        """ your general constructor to get things in line """

        # this is here if someone decides to use it as battle_tested(function)
        if callable(seconds):
            raise Exception('\n\n\tyou gave battle_tested() a function as the argument, did you mean battle_tested.fuzz()?')

        self.kwargs = kwargs
        self.tested = False

        # needed to determine how quiet it will be
        self.__verify_quiet__(quiet)
        self.quiet = quiet

        # needed to determine how verbosly it will work
        self.__verify_verbose__(verbose)
        self.verbose = False if self.quiet else verbose # quiet silences verbose mode

        # needed to determine the maximum time the tests can run
        self.__verify_seconds__(seconds)
        self.seconds = seconds

        # determine whether to keep testing after finding a crash
        self.__verify_keep_testing__(keep_testing)
        self.keep_testing = keep_testing

        # needed to determine maximum number of tests it can
        self.__verify_max_tests__(max_tests)
        self.max_tests = max_tests

        # determine what kind of exceptions are allowed
        self.__verify_allow__(allow)
        self.allow = allow

        # determine what kind of strategy to use
        self.__verify_strategy__(strategy)
        self.strategy = strategy

    @staticmethod
    def __verify_seconds__(seconds):
        assert type(seconds) == int, 'battle_tested needs seconds to be an int, not {0}'.format(repr(seconds))
        assert seconds > 0, 'battle_tested needs seconds to be a positive int, not {0}'.format(repr(seconds))

    @staticmethod
    def __verify_verbose__(verbose):
        """ asserts that verbose is valid """
        assert type(verbose) == bool, 'battle_tested needs verbose to be a bool, not {0}'.format(repr(verbose))

    @staticmethod
    def __verify_max_tests__(max_tests):
        """ asserts that max_tests is valid """
        assert type(max_tests) == int, 'battle_tested needs max_tests to be an int, not {0}'.format(repr(max_tests))
        assert max_tests > 0, 'battle_tested needs max_tests to be a positive int, not {0}'.format(repr(max_tests))

    @staticmethod
    def __verify_function__(fn):
        """ asserts that the input is a function """
        assert callable(fn), 'battle_tested needs a callable function, not {0}'.format(repr(fn))

    @staticmethod
    def __verify_tested__(fn):
        """ asserts that the function exists in battle_tested's results """
        battle_tested.__verify_function__(fn)
        assert fn in storage.results.keys(), '{} was not found in battle_tested\'s results, you probably haven\'t tested it yet'.format(fn)

    @staticmethod
    def __verify_keep_testing__(keep_testing):
        """ ensures keep_testing is a valid argument """
        assert type(keep_testing) == bool, 'keep_testing needs to be a bool'
        assert keep_testing == True or keep_testing == False, 'invalid value for keep_testing'

    @staticmethod
    def __verify_quiet__(quiet):
        """ ensures quiet is a valid argument """
        assert type(quiet) == bool, 'quiet needs to be a bool'
        assert quiet == True or quiet == False, 'invalid value for quiet'

    @staticmethod
    def __verify_allow__(allow):
        """ ensures allow is a valid argument """
        assert type(allow) == tuple, 'allow needs to be a tuple of exceptions'
        assert all(issubclass(i, BaseException) for i in allow), 'allow only accepts exceptions as its members'

    @staticmethod
    def __verify_args_needed__(args_needed):
        """ ensures args_needed is a valid number of args for a function """
        assert type(args_needed) == int, 'args_needed needs to be a positive int'
        assert args_needed > 0, 'args_needed needs to be a positive int'

    @staticmethod
    def __verify_strategy__(strategy):
        """ ensures strategy is a strategy or tuple of strategies """
        def is_strategy(strategy):
            assert 'strategy' in type(strategy).__name__.lower(), 'strategy needs to be a hypothesis strategy, not {}'.format(strategy)
            assert hasattr(strategy,'example'), 'strategy needs to be a hypothesis strategy, not {}'.format(strategy)
            return True
        if type(strategy) == tuple:
            assert len(strategy)>0, 'strategy cannot be an empty tuple, please define at least one'
            assert all(is_strategy(i) for i in strategy), 'not all members in strategy were valid hypothesis strategies'
        else:
            is_strategy(strategy)

    # results are composed like this
    # results[my_function]['unique_crashes']=[list_of_crashes]
    # results[my_function]['successes']=[list_of_successes]

    # safe container that holds crash results
    Crash = stricttuple(
        'Crash',
        arg_types = (
             lambda arg_types:type(arg_types)==tuple,
             lambda arg_types:len(arg_types)>0,
        ),
        args = (
            lambda args:type(args)==tuple,
            lambda args:len(args)>0,
        ),
        message = (
            lambda message:type(message).__name__ in 'str unicode NoneType' ,
        ),
        err_type = (
            lambda err_type:type(err_type)==type ,
        ),
        trace = (
            lambda trace:type(trace).__name__ in 'str unicode' ,
        )
    )

    class Result(object):
        ''' container that holds test results '''
        def __init__(self, successful_input_types, crash_input_types, iffy_input_types, output_types, exception_types, unique_crashes, successful_io, function):
            # assertions for successful_input_types
            assert type(successful_input_types)==PrettyTuple
            assert all(type(i)==tuple for i in successful_input_types)
            assert all(all(isinstance(x,type) for x in i) for i in successful_input_types)

            # assertions for crash_input_types
            assert type(crash_input_types)==PrettyTuple
            assert all(type(i)==tuple for i in crash_input_types)
            assert all(all(isinstance(x,type) for x in i) for i in crash_input_types)

            # assertions for iffy_input_types
            assert type(iffy_input_types)==PrettyTuple
            assert all(type(i)==tuple for i in iffy_input_types)
            assert all(all(isinstance(x,type) for x in i) for i in iffy_input_types)

            # assertions for output_types
            assert type(output_types)==PrettyTuple
            assert all(isinstance(i, type) for i in output_types)

            # assertions for exception_types
            assert type(exception_types)==PrettyTuple
            assert all(isinstance(i,Exception) or issubclass(i,Exception) for i in exception_types)

            # assertions for unique_crashes
            assert type(unique_crashes)==UniqueCrashContainer

            # assertions for successful_io
            assert type(successful_io)==deque
            assert all(type(i) == io_example for i in successful_io) if len(successful_io) else 1

            self.successful_input_types = successful_input_types
            self.crash_input_types = crash_input_types
            self.iffy_input_types = iffy_input_types
            self.output_types = output_types
            self.exception_types = exception_types
            self.unique_crashes = unique_crashes
            self.successful_io = successful_io
            self.function = function

            self.unittest = attempt(self._generate_unit_test)

            self._fields = 'successful_input_types', 'crash_input_types', 'iffy_input_types', 'output_types', 'exception_types', 'unique_crashes', 'successful_io'

        def __repr__(self):
            table = PrettyTable(None)
            for i in sorted(self._fields):
                new_lines_in_repr = repr(getattr(self,i)).count('\n')
                if new_lines_in_repr > 0:
                    ii = '{}{}'.format('\n'*int(new_lines_in_repr/2), i)
                else:
                    ii = i
                if i == 'successful_io':
                    table.add_row((ii, repr(list(getattr(self,i)))))
                else:
                    table.add_row((ii, getattr(self,i)))
            table.align='l'
            return '\n'.join(table.get_string().splitlines()[2:])

        def _generate_unit_test(self):
            ''' give this a function to fuzz and it will spit out a unittest file '''

            # I know the code in this function is a little hateful, its brand new
            # and I'll clean it up as soon as I'm certain it is where it needs to be

            # negative tests
            negative_tests = deque()
            for i in self.unique_crashes:
                #logging.warning(repr(i))
                invocation_code = '{}{}'.format(self.function.__name__, repr(i.args))
                tmp='def {}(*a,**k):pass\n'.format(self.function.__name__)+invocation_code
                if runnable(tmp) and compilable(tmp) and valid_repr(i.args):
                    #logging.warning(invocation_code)
                    test_name = 'raises_{}'.format(i.err_type.__name__)
                    negative_tests.append(unittest_builder.raises_test(test_name, invocation_code, i.err_type))
                #else:
                #    logging.warning('not runnable')
                #    logging.warning(repr(invocation_code))
            # positive tests
            positive_tests = deque()
            for c, io_object in enumerate(self.successful_io):
                io_object.input = tuple(float(i) if type(i)==builtins.float else i for i in io_object.input)
                io_object.output = attempt(
                    lambda:tuple(float(i) if type(i)==builtins.float else i for i in io_object.output) ,
                    default_output=io_object.output
                )
                io_object.input = tuple(complex(i) if type(i)==builtins.complex else i for i in io_object.input)
                io_object.output = attempt(
                    lambda:tuple(complex(i) if type(i)==builtins.complex else i for i in io_object.output) ,
                    default_output=io_object.output
                )
                if type(io_object.output) == builtins.complex:
                    io_object.output = complex(io_object.output)
                if type(io_object.output) == builtins.float:
                    io_object.output = float(io_object.output)
                invocation_code = '{}{}'.format(self.function.__name__, repr(io_object.input))
                tmp='def {}(*a,**k):pass\n'.format(self.function.__name__)+invocation_code
                if runnable(tmp) and compilable(tmp) and valid_repr(io_object.input) and valid_repr(io_object.output):
                    if all(runs_fine(repr(i)) for i in (io_object.input, io_object.output)):
                        positive_tests.append((invocation_code, io_object.output))
            positive_tests = [
                unittest_builder.equal_test('equals_{}'.format(i+1), *v)
                for i, v in enumerate(positive_tests)
            ]

            #print(negative_tests)
            #print(positive_tests)
            positive_tests = ''.join(sorted(positive_tests))
            negative_tests = ''.join(sorted(negative_tests))
            test_functions = negative_tests + positive_tests
            #print(test_functions)

            return unittest_builder.test_body(self.function, test_functions)


    @staticmethod
    def results(fn):
        '''returns the collected results of the given function'''
        battle_tested.__verify_tested__(fn)
        return storage.results[fn]

    @staticmethod
    def stats(fn):
        ''' returns the stats found when testing a function '''
        results = battle_tested.results(fn)
        return {k:len(getattr(results, k)) for k in results._fields}

    @staticmethod
    def print_stats(fn):
        ''' prints the stats on a tested function '''
        stats = battle_tested.stats(fn)
        fn_name = fn.__name__ if '__name__' in dir(fn) else fn
        s = 'fuzzing {}() found:'.format(fn_name)
        s += ' '*(79-len(s))
        print(s)
        t=PrettyTable(None)
        for k in sorted(stats.keys()):
            t.add_row((k,stats[k]))
        print('\n'.join(t.get_string().splitlines()[2:]))

    # these two are here so the maps can have doc strings
    class _crash_map(dict):
        '''a map of crashes generated by the previous test'''
    class _success_map(set):
        '''a map of data types that were able to get through the function without crashing'''
    crash_map = _crash_map()
    success_map = _success_map()

    @staticmethod
    def generate_examples(args_needed=1, strategy=None):
        """ this is the primary argument generator that battle_tested runs on """
        battle_tested.__verify_args_needed__(args_needed)
        if strategy is not None: # logic for a custom strategy
            battle_tested.__verify_strategy__(strategy)
            if type(strategy) == tuple:
                assert len(strategy) == args_needed, 'invalid number of strategies, needed {} got {}'.format(args_needed, len(strategy))
                print('using {} custom strategies - {}'.format(len(strategy),strategy))
                strategy = st.builds(lambda *x: list(x), *strategy)
                yield from strategy_stream(strategy)
            else:
                # generate lists containing output only from the given strategy
                stream = strategy_stream(strategy)
                for _ in gen.loop():
                    # build a list of args to test next
                    out = [stream_data for _, stream_data in zip(range(args_needed), stream)]
                    # yield all combinations of that list
                    yield from product(out, repeat=len(out))
        else: # logic for fuzzing approach
            # first run through the cache
            storage.refresh_test_inputs()
            for chunk in generators.chunks(chain(storage.test_inputs, reversed(storage.test_inputs)),size=args_needed):
                for combination in product(chunk, repeat=args_needed):
                    yield combination
            try:
                garbage = multiprocess_garbage()
                while 2:
                    out = [next(garbage) for i in range(args_needed)]
                    for i in product(out, repeat=len(out)):
                        yield i
            finally:
                garbage.close()

    @staticmethod
    def fuzz(fn, seconds=6, max_tests=1000000000, verbose=False, keep_testing=True, quiet=False, allow=(), strategy=garbage):
        """

fuzz - battle_tested's primary weapon for testing functions.

Example Usage:

    def my_adder(a, b):
        ''' switches the variables '''
        return b + a

    fuzz(my_adder) # returns a report of what works/breaks

    # or

    def my_strict_add(a, b):
        ''' adds a and b together '''
        assert isinstance(a, int), 'a needs to be an int'
        assert isinstance(b, int), 'b needs to be an int'
        return a + b

    # This runs tests and halts the program if there is an error if that error
    # isn't an AssertionError. This tests if you've written enough assertions.
    fuzz(my_strict_add, keep_testing=False, allow=(AssertionError,))

Parameters:

    fn           - the function to be fuzzed (must accept at least one argument)
    seconds      - maximum time battle_tested is allowed to fuzz the function
    max_tests    - maximum number of tests battle_tested will run before exiting
                   (if the time limit doesn't come first)
    verbose      - setting this to False makes battle_tested raise the first
                   exception that wasn't specifically allowed in the allow option
    keep_testing - setting this to True allows battle_tested to keep testing
                   even after it finds the first falsifying example, the results
                   can be accessed with crash_map() and success_map()
    quiet        - setting this to True silences all of the outputs coming from
                   the test
    allow        - this can be a tuple of exception types that you want
                   battle_tested to skip over in its tests
"""
        battle_tested.__verify_function__(fn)
        battle_tested.__verify_seconds__(seconds)
        battle_tested.__verify_verbose__(verbose)
        battle_tested.__verify_max_tests__(max_tests)
        battle_tested.__verify_keep_testing__(keep_testing)
        battle_tested.__verify_quiet__(quiet)
        battle_tested.__verify_allow__(allow)
        battle_tested.__verify_strategy__(strategy)

        using_native_garbage = hash(strategy) == hash(garbage)

        args_needed = function_arg_count(fn)

        # code for instance methods
        if hasattr(fn, '__self__'):
            # create a partial with fn.__self__ as the first arg
            #fn = partial(fn, fn.__self__)
            _name = repr(fn)
            _type = type(fn).__name__
            #print(dir(fn))
            # wrap the method in a hashable wrapper
            fn = partial(fn)
            fn.__name__ = _name
            # if fn is not a builtin, chop off one arg needed
            if 'builtin' not in _type and args_needed > 1:
                args_needed = args_needed-1
            del _name
            del _type

        #if type(strategy) == tuple:
        #    assert len(strategy) == args_needed, 'invalid number of strategies, needed {} got {}'.format(args_needed, len(strategy))
        #    print('using {} custom strategies - {}'.format(len(strategy),strategy))
        #    strategy = st.builds(lambda *x: list(x), *strategy)
        #else:
        #    # generate a strategy that creates a list of garbage variables for each argument
        #    strategy = st.lists(elements=strategy, max_size=args_needed, min_size=args_needed)

        if not quiet:
            print('testing: {0}()'.format(getattr(fn, '__name__', repr(fn))))

        battle_tested.crash_map.clear()
        battle_tested.success_map.clear()

        count = generators.counter()
        average = generators.avg()
        timer = generators.timer()


        def calculate_window_speed():
            w = calculate_window_speed.window
            w.append(_inner_window_speed())
            return int((1.0*sum(w))/len(w))
        calculate_window_speed.window = deque(maxlen=4)

        def _inner_window_speed():
            cw = display_stats.count_window
            tw = display_stats.time_window
            if len(cw) == 2:
                c = cw[1]-cw[0]
                t = tw[1]-tw[0]
                if c != 0 and t != 0:
                    out = int(c*(1/t))
                    return out if out > 0 else 1
            return 1

        def display_stats(overwrite_line=True):

            now = next(display_stats.timer)
            display_stats.remaining = display_stats.test_time-now
            if not display_stats.quiet:
                display_stats.count_window.append(display_stats.count)
                display_stats.time_window.append(now)
                print('tests: {:<8}  speed: {:>6}/sec  avg:{:>6}/sec {} {}s    '.format(
                    display_stats.count,
                    calculate_window_speed(),
                    int(display_stats.count/(now if now > 0 else 0.001)),
                    '-' if overwrite_line else 'in',
                    int(display_stats.test_time-now)+1 if overwrite_line else display_stats.test_time
                ), end=('\r' if overwrite_line else '\n'))
                sys.stdout.flush()

        display_stats.test_time = seconds
        display_stats.remaining = display_stats.test_time
        display_stats.count = 0
        display_stats.time_window = deque(maxlen=2)
        display_stats.count_window = deque(maxlen=2)
        display_stats.timer = generators.timer()
        display_stats.average = generators.avg()
        display_stats.interval = IntervalTimer(0.16, display_stats)
        display_stats.quiet = quiet or verbose
        display_stats.start = lambda:(next(display_stats.timer),display_stats.interval.start())

        ipython_tools.silence_traceback()

        storage.results[fn] = {
            'successful_input_types':deque(maxlen=512),
            'crash_input_types':set(),
            'iffy_input_types':set(), # types that both succeed and crash the function
            'output_types':set(),
            'exception_types':set(),
            'unique_crashes':dict(),
            'successful_io':deque(maxlen=512)
        }

        def fn_info():
            pass
        fn_info.fuzz_time = time()
        fn_info.fuzz_id = len(storage.results.keys())
        # stores examples that succeed and return something other than None
        fn_info.successful_io = deque(maxlen=512)
        # stores examples that return None
        fn_info.none_successful_io = deque(maxlen=512)

        gc_interval = IntervalTimer(3, gc)

        #@settings(perform_health_check=False, database_file=None, deadline=None, max_examples=max_tests, verbosity=(Verbosity.verbose if verbose else Verbosity.normal))
        #@given(strategy)
        def _fuzz(given_args):
            if _fuzz.first_run:
                _fuzz.first_run = False
                # start the display interval
                display_stats.start()
                # start the countdown for timeout
                _fuzz.timestopper.start()

            arg_list = tuple(given_args)
            #if len(arg_list) != fuzz.args_needed:
            #    exit('got {} args? {}'.format(len(arg_list),next(test_variables)))
            # unpack the arguments
            if not _fuzz.has_time:
                raise FuzzTimeoutError()
            display_stats.count += 1
            try:
                with max_execution_time(int(display_stats.remaining)):
                    out = fn(*arg_list)
                    # if out is a generator, empty it out.
                    if hasattr(out, '__iter__') and (hasattr(out,'__next__') or hasattr(out,'next')):
                        for i in out:
                            pass
                # the rest of this block is handling logging a success
                input_types = tuple(type(i) for i in arg_list)
                # if the input types have caused a crash before, add them to iffy_types
                if input_types in storage.results[fn]['crash_input_types']:
                    storage.results[fn]['iffy_input_types'].add(input_types)
                # add the input types to the successful collection
                if input_types not in storage.results[fn]['successful_input_types']:
                    storage.results[fn]['successful_input_types'].append(input_types)
                # add the output type to the output collection
                storage.results[fn]['output_types'].add(type(out))
                battle_tested.success_map.add(tuple(type(i) for i in arg_list))
                try:
                    (fn_info.none_successful_io if out is None else fn_info.successful_io).append(io_example(arg_list, out))
                    '''
                    # I want to add this, but it wrecks the fuzzer's performance :(

                    io_object = io_example(arg_list, out)
                    if out is None:
                        if io_object not in fn_info.none_successful_io:
                            fn_info.none_successful_io.append(io_object)
                    else:
                        if io_object not in fn_info.successful_io:
                            fn_info.successful_io.append(io_object)
                    '''
                except:
                    pass
            except MaxExecutionTimeError:
                pass
            except _fuzz.allow as ex:
                pass
            except Exception as ex:
                ex_message = ex.args[0] if (
                    hasattr(ex, 'args') and len(ex.args) > 0
                ) else (ex.message if (
                    hasattr(ex, 'message') and len(ex.message) > 0
                ) else '')

                storage.results[fn]['crash_input_types'].add(tuple(type(i) for i in arg_list))

                if keep_testing:
                    tb_text = traceback_text()
                    tb = '{}{}'.format(traceback_file_lines(tb_text),repr(type(ex)))
                    battle_tested.crash_map[tb]={'type':type(ex),'message':ex_message,'args':arg_list,'arg_types':tuple(type(i) for i in arg_list)}
                    storage.results[fn]['unique_crashes'][tb]=battle_tested.Crash(
                        err_type=type(ex),
                        message=repr(ex_message),
                        args=arg_list,
                        arg_types=tuple(type(i) for i in arg_list),
                        trace=str(tb_text)
                    )
                    storage.results[fn]['exception_types'].add(type(ex))
                else:
                    # get the step where the code broke
                    tb_steps_full = [i for i in traceback_steps()]
                    tb_steps_with_func_name = [i for i in tb_steps_full if i.splitlines()[0].endswith(fn.__name__)]

                    if len(tb_steps_with_func_name)>0:
                        tb = tb_steps_with_func_name[-1]
                    else:
                        tb = tb_steps_full[-1]

                    error_string = format_error_message(
                        fn.__name__,
                        '{} - {}'.format(type(ex).__name__,ex_message),
                        tb,
                        (arg_list if len(arg_list)!=1 else '({})'.format(repr(arg_list[0])))
                    )
                    ex.message = error_string
                    ex.args = error_string,
                    raise ex

        _fuzz.has_time = True
        _fuzz.first_run = True
        _fuzz.timestopper = Timer(seconds, lambda:setattr(_fuzz,'has_time',False))
        _fuzz.exceptions = deque()
        _fuzz.args_needed = args_needed
        _fuzz.allow = allow
        _fuzz.using_native_garbage = using_native_garbage

        # run the test
        test_gen = battle_tested.generate_examples(args_needed, None if using_native_garbage else strategy)
        next(test_gen) # start the test generator
        try:
            gc_interval.start()
            for test_args in test_gen:
                if verbose:
                    try:
                        s = '{}'.format(tuple(test_args))
                        s = s[:-2]+s[-1]
                        print('trying {}{}'.format(fn.__name__, s))
                    except: pass
                _fuzz(test_args)
                max_tests -= 1
                if max_tests <= 0:
                    break
        except FuzzTimeoutError:
            pass
        except KeyboardInterrupt:
            if not quiet:
                print('  stopping fuzz early...')
        finally:
            attempt(test_gen.close)
            display_stats.interval.stop()
            display_stats(False)
            gc_interval.stop()
            attempt(_fuzz.timestopper.cancel)

            if not display_stats.quiet:
                print('compiling results...')

            results_dict = storage.results[fn]
            results_dict['iffy_input_types'] = set(i for i in results_dict['crash_input_types'] if i in results_dict['successful_input_types'])

            # merge the io maps
            for i in fn_info.none_successful_io:
                #if len(fn_info.successful_io)<fn_info.successful_io.maxlen:
                fn_info.successful_io.append(i)
            # remove io map with None examples
            del fn_info.none_successful_io

            storage.results[fn] = battle_tested.Result(
                successful_input_types=PrettyTuple(set(i for i in results_dict['successful_input_types'] if i not in results_dict['iffy_input_types'] and i not in results_dict['crash_input_types'])),
                crash_input_types=PrettyTuple(results_dict['crash_input_types']),
                iffy_input_types=PrettyTuple(results_dict['iffy_input_types']),
                output_types=PrettyTuple(results_dict['output_types']),
                exception_types=PrettyTuple(results_dict['exception_types']),
                unique_crashes=UniqueCrashContainer(results_dict['unique_crashes'].values()),
                successful_io=fn_info.successful_io,
                function=fn
            )
            storage.results[fn].function = fn
            ## find the types that both crashed and succeeded
            #results_dict['iffy_input_types'] = set(i for i in results_dict['crash_input_types'] if i in results_dict['successful_input_types'])
            ## clean up the unique_crashes section
            #results_dict['unique_crashes'] = tuple(results_dict['unique_crashes'].values())
            ## remove duplicate successful input types
            #results_dict['successful_input_types'] = set(results_dict['successful_input_types'])
        if keep_testing:
            #examples_that_break = ('examples that break' if len(battle_tested.crash_map)>1 else 'example that broke')
            #print('found {} {} {}()'.format(len(battle_tested.crash_map),examples_that_break,fn.__name__))
            if not quiet:
                battle_tested.print_stats(fn)
            #print('run crash_map() or success_map() to access the test results')
        else:
            if not quiet:
                print('battle_tested: no falsifying examples found')



        # try to save the fields to the function object
        try:
            for f in storage.results[fn]._fields:
                setattr(fn, f, getattr(storage.results[fn], f))
        except: pass
        # try to store the unique crashes as readable attributes
        try:
            for crash in storage.results[fn].unique_crashes:
                try:
                    setattr(fn_info.unique_crashes, '{}_{}'.format(crash.err_type.__name__, [x.strip() for x in crash.trace.split(', ') if x.startswith('line ')][-1].replace(' ','_')), crash)
                except: pass
                try:
                    setattr(storage.results[fn].unique_crashes, '{}_{}'.format(crash.err_type.__name__, [x.strip() for x in crash.trace.split(', ') if x.startswith('line ')][-1].replace(' ','_')), crash)
                except: pass
        except: pass
        try:
            def dummy_function(): pass
            for a in dir(fn_info):
                if a not in dir(dummy_function):
                    try:
                        setattr(fn, a, getattr(fn_info, a))
                    except:
                        pass
        except: pass
        return storage.results[fn]


    def __call__(self, fn):
        """ runs before the decorated function is called """
        self.__verify_function__(fn)

        if fn not in storage.results:
            # only test the first time this function is called
            if not ('skip_test' in self.kwargs and self.kwargs['skip_test']):
                # skip the test if it is explicitly turned off
                self.fuzz(fn, seconds=self.seconds, max_tests=self.max_tests, keep_testing=self.keep_testing, verbose=self.verbose, quiet=self.quiet, allow=self.allow, strategy=self.strategy)
            #self.tested = True

        if any(i in self.kwargs for i in ('logger','default_output')):
            # only wrap if needed
            def wrapper(*args, **kwargs):
                try:
                    out = fn(*args, **kwargs)
                except Exception as e:
                    # log the error
                    if 'logger' in self.kwargs:
                        assert callable(self.kwargs['logger']), "battle_tested.logger needs to be a callable log function, not: {0}".format(repr(self.kwargs['logger']))
                        self.kwargs['logger'](e)
                    else:
                        logging.exception(e)
                    # only raise the error if there isnt a default_output
                    if 'default_output' in self.kwargs:
                        out = self.kwargs['default_output']
                    else:
                        raise e
                return out
            return wrapper
        else:
            return fn

# make fuzz its own independent function
fuzz = battle_tested.fuzz
results = battle_tested.results
stats = battle_tested.stats
print_stats = battle_tested.print_stats

def crash_map():
    '''returns a map of crashes generated by the previous test'''
    return tuple(sorted(battle_tested.crash_map.values(), key=lambda i:i['type'].__name__))

def success_map():
    '''returns a map of data types that were able to get through the function without crashing'''
    return tuple(sorted(battle_tested.success_map, key=lambda i:i[0].__name__))

def function_versions(fn):
    ''' returns all tested versions of the given function '''
    for f in storage.results.keys():
        if f.__name__ == fn.__name__ and f.__module__ == fn.__module__:
            yield f

def time_io(fn,args,rounds=1000):
    ''' time how long it takes for a function to run through given args '''
    tests = range(rounds)
    args = tuple(args) # solidify this so we can run it multiple times
    start = time()
    for t in tests:
        for a in args:
            fn(*a)
    return time()-start

def all_common_successful_io(*functions):
    ''' gets all io objects that works with all given '''
    for io in generators.chain(*(fn.successful_io for fn in functions)):
        succeeded = 0
        for fn in functions:
            try:
                out = fn(*io.input)
                if hasattr(out, '__iter__'):
                    for i in out:
                        pass
                succeeded += 1
            except:
                pass
        if succeeded == len(functions):
            yield io

def time_all_versions_of(fn):
    ''' time how long each version of a function takes to run through the saved io '''
    print('\ntiming all versions of {}'.format(fn.__name__))
    common_io = partial(all_common_successful_io, *list(function_versions(fn)))
    print('found {} inputs that all versions can run'.format(len(list(common_io()))))
    for f in function_versions(fn):
        print('\n{}\n\n{}'.format('-'*60,getsource(f)))
        print('{:.10f}'.format(time_io(f,(io.input for io in common_io()))),'seconds')
        #print(time_io(f,(io.input for io in f.successful_io)),'seconds with {} runs'.format(len(f.successful_io)*1000))
        #    for ff in function_versions(fn):
        #    #print(time_io(f,(io.input for io in ff.successful_io)),'seconds')
    print('\n{}'.format('-'*60))


def run_tests():
    ''' this is where all of the primary functionality of battle_tested is tested '''

    print('''
WARNING | This test exercises ALL functions of the fuzzer and WILL take a while.
        | Let it churn to see if its fully operational. This includes testing
        | on all available cpu cores and the full result rendering so just let
        | it scroll.
        |
        | If this battle test succeeds, you are running on all cylinders!!!
''')
    
    sleep(20)
    
    print('''
Initiating core battle test...
''')
    sleep(3)

    # test instance methods
    class TestClass(tuple):
        def testmethod(self,a,b,c,d,e):
            return a,b,c,d

    tc = TestClass([1,2,3])
    print(fuzz(tc.testmethod))

    l = list(range(10))
    print(fuzz(l.append))

    # test fuzzing all the types
    for i in (str, bool, bytearray, bytes, complex, dict, float, frozenset, int, list, object, set, str, tuple):
        print('testing: {}'.format(i))
        print(fuzz(i))

    def test_generator(a):
        for i in a:
            yield i
    print(fuzz(test_generator, seconds=10))
    def test_generator(a):
        for i in a:
            yield i,i
    print(fuzz(test_generator, seconds=10))

    print(time_all_versions_of(test_generator))

    # try the custom strategy syntax
    @battle_tested(strategy=st.text(),max_tests=50)
    def custom_text_strategy(a,b):
        if len(a) == 0:
            return None
        else:
            return a in b

    print(dir(custom_text_strategy))
    for i in ('successful_io','crash_input_types','exception_types','iffy_input_types','unique_crashes','output_types','successful_input_types'):
        assert hasattr(custom_text_strategy, i), 'custom_text_strategy doesnt have a {} attribute'.format(i)

    def custom_text_fuzz_strategy(a,b):
        return a in b
    fuzz(custom_text_fuzz_strategy, strategy=st.text())

    # try the multiple custom strategy syntax
    @battle_tested(strategy=(st.text(), st.integers()))
    def custom_text_int_strategy(a,b):
        assert isinstance(a, str), 'a needs to be text'
        assert isinstance(b, int), 'b needs to be an int'
        return a+b


    def custom_text_int_fuzz_strategy(a,b):
        return a in b
    r=fuzz(custom_text_fuzz_strategy, strategy=(st.integers(),st.text()))

    #======================================
    #  Examples using the wrapper syntax
    #======================================
    @battle_tested(default_output=[], seconds=1, max_tests=5)
    def sample(i):
        return []

    @battle_tested(keep_testing=False)
    def sample2(a,b,c,d=''):
        t = a, b, c, d

    # output for documentation
    def test(a):
        return int(a)
    print(repr(fuzz(test)))

    # test different speeds
    @battle_tested(seconds=1)
    def arg1_1sec(a):
        return a
    @battle_tested()
    def arg1(a):
        return a
    @battle_tested(seconds=1)
    def args2_1sec(a,b):
        return a+b
    @battle_tested()
    def args2(a,b):
        return a+b
    @battle_tested(seconds=1)
    def args3_1sec(a,b,c):
        return a+b+c
    @battle_tested()
    def args3(a,b,c):
        return a+b+c
    @battle_tested(seconds=1)
    def args4_1sec(a,b,c,d):
        return a+b+c+d
    @battle_tested()
    def args4(a,b,c,d):
        return a+b+c+d
    @battle_tested(seconds=1)
    def args5_1sec(a,b,c,d,e):
        return a+b+c+d+e
    @battle_tested()
    def args5(a,b,c,d,e):
        return a+b+c+d+e

    # test the allow option
    @battle_tested(allow=(AssertionError,))
    def allowed_to_assert(a,b):
        assert a==b, 'a needs to equal b'
    @battle_tested(allow=(AssertionError,), keep_testing=False)
    def allowed_to_assert_and_stop_on_fail(a,b):
        assert a==b, 'a needs to equal b'
    fuzz(max, allow=(ValueError,))
    fuzz(max, keep_testing=False, allow=(ValueError,TypeError))


    # test going quiet
    print('going quiet')
    def quiet_test_out():
        pass
    @battle_tested(keep_testing=False, quiet=True)
    def quiet_test(a,b,c):
        setattr(quiet_test_out, 'args', (a,b,c))
    assert len(quiet_test_out.args) == 3, 'fuzzing quiet test failed'

    quiet_lambda = lambda a,b,c:setattr(quiet_test_out, 'lambda_args', (a,b,c))
    r = fuzz(quiet_lambda, quiet=True, keep_testing=False)
    assert len(quiet_test_out.lambda_args) == 3, 'fuzzing quiet lambda failed'

    print('quiet test complete')

    # proof that they only get tested once
    print(sample(4))
    print(sample2(1,2,3,4))
    print(sample('i'))
    print(sample2('a','b',2,4))

    # prove that successes of any type are possible
    r = fuzz(lambda i:i , keep_testing=True, seconds=10)
    assert len(r.crash_input_types) == 0, 'fuzzing lambda() changed expected behavior'
    assert len(r.exception_types) == 0, 'fuzzing lambda() changed expected behavior'
    assert len(r.iffy_input_types) == 0, 'fuzzing lambda() changed expected behavior'
    assert len(r.unique_crashes) == 0, 'fuzzing lambda() changed expected behavior'
    assert len(r.output_types) > 10, 'fuzzing lambda() changed expected behavior'
    assert len(r.successful_input_types) > 10, 'fuzzing lambda() changed expected behavior'

    #======================================
    #  Examples using the function syntax
    #======================================

    def sample3(a,b):
        # this one blows up on purpose
        return a+b+1

    # this tests a long fuzz
    r=fuzz(sample3, seconds=20)
    assert len(r.successful_io)>0, 'succesful_io was empty'

    print(r.successful_io)

    crash_map()
    success_map()

    assert len(r.crash_input_types) > 10 , 'fuzzing sample3() changed expected behavior'
    assert len(r.exception_types) > 0, 'fuzzing sample3() changed expected behavior'
    assert len(r.unique_crashes) > 0, 'fuzzing sample3() changed expected behavior'
    assert len(r.output_types) > 1, 'fuzzing sample3() changed expected behavior'
    assert len(r.successful_input_types) > 10, 'fuzzing sample3() changed expected behavior'

    fuzz(lambda i:i)

    #======================================
    #   example harness
    #======================================
    def harness(key,value):
        global mydict
        global crash_examples
        global successful_types
        try:
            mydict[key]=value
            successful_types.add((type(key).name, type(value).name))
        except Exception as e:
            print('found one')
            crash_examples[e.args[0]]=(key,value)


    for tested_function in storage.results.keys():
        s = '\n'
        try:
            s+=tested_function.__module__
            s+=' ---> '
            s+=tested_function.__name__
            s+=' ---> '
            s+=str([i for i in dir(storage.results[tested_function]) if not i.startswith('_')])
        except:
            pass
        finally:
            print(s)

    print('''
SUCCESS | Core battle test complete. Go forth and fuzz the world!!!
''')


if __name__ == '__main__':
    run_tests()
