#!/usr/bin/env python3

# Boolector: Satisfiablity Modulo Theories (SMT) solver.
#
# Copyright (C) 2007-2021 by the authors listed in the AUTHORS file.
#
# This file is part of Boolector.
# See COPYING for more information on using this software.
#

import argparse
import subprocess
import os
import sys
from multiprocessing import Pool

g_args = None

ERR_KEYWORDS = ['Boolector under test changed',
                'not hashed',
                'but got',
                'ERROR: AddressSanitizer: heap-use-after-free',
                'ERROR: LeakSanitizer: detected memory leaks']

def get_num_lines(file):
    return sum(1 for line in open(file))

def progress(i):
    x = i % 4
    if x == 0:
        return '-'
    elif x == 1:
        return '\\'
    elif x == 2:
        return '|'
    return '/'

def untrace(trace):
    global g_args

    try:
        cmd = [g_args.untracer, trace]

        proc = subprocess.Popen(cmd,
                                stdin=subprocess.PIPE,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE)
        _, err = proc.communicate(timeout=g_args.timeout)

        err = err.decode()

        if 'BTORLEAK' in err:
            return None

        for kw in ERR_KEYWORDS:
            if kw in err:
                err = kw
                break

        #if 'Non-destructive substitution' in err:
        #    os.remove(trace)
        #    return None

        new_err = [e for e in err.split('\n') if 'WARNING' not in e]
        err = '\n'.join(new_err).strip()

        if err:
            num_lines = get_num_lines(trace)
            return (err, num_lines, trace)
        return None
    except KeyboardInterrupt:
        return None
    except subprocess.TimeoutExpired:
        proc.terminate()
        if g_args.skip_timeout:
            return None
        return ('timeout', get_num_lines(trace), trace)

def reduce(info):
    err = info[0]
    trace = info[1]
    script_dir = os.path.dirname(os.path.abspath(__file__))
    ddmbt_path = os.path.join(script_dir, 'ddmbt.py')
    try:
        split_path = os.path.split(trace)
        red_trace = \
            os.path.join(split_path[0], 'red-{}'.format(split_path[1]))
        cmd = [ddmbt_path, '-vv', trace, red_trace, g_args.untracer]

        proc = subprocess.Popen(cmd,
                                stdin=subprocess.PIPE,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE)
        _, _ = proc.communicate()

        return (err, get_num_lines(red_trace), red_trace)
    except KeyboardInterrupt:
        proc.terminate()
        return None


def main():
    global g_args

    ap = argparse.ArgumentParser()
    ap.add_argument('dir', nargs='+')
    ap.add_argument('-u', '--untracer', default='build/bin/btoruntrace',
                    help='Path to untracer')
    ap.add_argument('-r', '--reduce', default=False, action='store_true',
                    help='Reduce smallest trace for each group')
    ap.add_argument('-s', '--skip-timeout', default=False, action='store_true',
                    help='Ignore timeouts')
    ap.add_argument('-t', '--timeout', default=2, type=int,
                    help='btoruntrace timeout')
    ap.add_argument('-p', '--procs', default=1, type=int,
                    help='Number of parallel processes')

    g_args = ap.parse_args()

    errors = dict()
    traces = []
    for dir in g_args.dir:
        for root, dirs, files in os.walk(dir):
            for file in files:
                if file.endswith('.trace'):
                    traces.append(os.path.join(root, file))

    try:
        with Pool(processes=g_args.procs) as pool:
            for i, t in enumerate(pool.imap_unordered(untrace, traces, 1)):
                print('{} Checking {}/{}\r'.format(
                    progress(i), i, len(traces)),
                    file=sys.stderr, end='')
                if t:
                    err, num_lines, trace = t
                    files = errors.setdefault(err, [])
                    files.append((num_lines, trace))

    except KeyboardInterrupt:
        pass

    sorted_errors = sorted(errors.items(), key=lambda x: len(x[1]), reverse=True)

    if g_args.reduce:
        to_reduce = []
        for msg, traces in sorted_errors:
            traces = sorted(traces)
            trace = traces[0][1]
            to_reduce.append((msg, trace))

        try:
            with Pool(processes=g_args.procs) as pool:
                for i, t in enumerate(pool.imap_unordered(reduce, to_reduce, 1)):
                    print('{} Reducing {}/{}\r'.format(
                        progress(i), i, len(to_reduce)),
                        file=sys.stderr, end='')
                    if t:
                        err, num_lines, trace = t
                        files = errors.setdefault(err, [])
                        files.append((num_lines, trace))
        except KeyboardInterrupt:
            pool.terminate()
            pool.join()

        sorted_errors = sorted(errors.items(), key=lambda x: len(x[1]), reverse=True)


    for msg, traces in sorted_errors:
        print('{} {}'.format(len(traces), msg))
        traces_display = sorted(traces)[:5]
        print('\n{}\n'.format(
            '\n'.join('{} {}'.format(x, y) for x, y in traces_display)))


if __name__ == '__main__':
    main()
