#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# This file is part of Checkbox.
#
# Copyright 2014 Canonical Ltd.
#
# Authors
#  Jeff Lane <jeffrey.lane@canonical.com>
#  Daniel Manrique<daniel.manrique@canonical.com>
#
# Checkbox is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3,
# as published by the Free Software Foundation.
#
# Checkbox is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Checkbox.  If not, see <http://www.gnu.org/licenses/>.


'''
This script is used to parse the log generated by fwts and check it for certain
errors detected during testing.  It expects that this is a log file created by
fwts at runtime using the -l <log name> option.

It's written now specifically for checking ater the fwts s3 and s4 tests but
can be adapted to look for other tests, or all tests.
'''

import sys
import collections
import re

from argparse import ArgumentParser
import logging

# Definitions of when a level starts, how a failure looks,
# and when a level ends.
start_level_re = r'^(?P<level>.+) failures: (?P<numfails>NONE|\d+)$'
start_level_re = re.compile(start_level_re)
failure_re = re.compile(r'^ (?P<test>(s3|s4)): (?P<details>.+)$')
end_level_re = re.compile(r"$^")


def parse_summary(summary, results):
    """
    Parses an entire "Test Failure Summary" section, which contains a short
    summary of failures observed per level. Returns nothing, but adds the
    results to the passed results dictionary.

    :param summary:
        A list of lines comprised in this summary section

    :param results:
        The results dictionary into which to put the end result. Should be a
        dict with keys for each level, the values are dicts with keys for each
        test (s3, s4) which in turn contain a list of all the failures observed
        for that level and test.
    """
    current_level = None
    current_acum = []

    for logline in summary:
        level_matches = start_level_re.search(logline)
        if level_matches:
            logging.debug("Found a level: %s", level_matches.group('level'))
            current_level = level_matches.group('level')
        elif end_level_re.search(logline) and current_level:
            if current_level:
                logging.debug("Current level (%s) has %s",
                              current_level, current_acum)
                # By passing results[current_level] a key in results will be
                # created for every level we see, regardless of whether it
                # reports failures or not.  This is OK because we can later
                # check results' keys to ensure we saw at least one level; if
                # results has no keys, it could mean a malformed fwts log file.
                parse_level(current_acum, results[current_level])
            else:
                logging.debug("Discarding junk")
            current_acum = []
            current_level = None
        else:
            current_acum.append(logline)


def parse_level(level_lines, level_results):
    """
    Parses the level's lines, appending the failures to the level's results.
    level_results is a dictionary with a key per test type (s3, s4, and so on).
    Returns nothing, but adds the results to the passed results dictionary for
    this level.

    :param level_lines:
        A list of lines comprised in this level's list of failures.

    : param level_results:
        A dictionary containing this level's results. Should be a dict with
        keys for each test, to which the failures for the level will be
        appended.
    """
    for failureline in level_lines:
        failure_matches = failure_re.search(failureline)
        if failure_matches:
            test = failure_matches.group('test')
            details = failure_matches.group('details')
            logging.debug("fail %s was %s", test, details)
            level_results[test].append(details)


def main():
    parser = ArgumentParser()
    parser.add_argument('-d', '--debug',
                        action='store_const',
                        const=logging.DEBUG,
                        default=logging.INFO,
                        help="Show debugging information.")
    parser.add_argument('-v', '--verbose',
                        action='store_true',
                        default=False,
                        help="Display each error discovered. May provide \
                              very long output. Also, this option will only \
                              provide a list of UNIQUE errors encountered in \
                              the log file. It will not display duplicates. \
                              Default is [%(default)s]")
    parser.add_argument('test',
                        action='store',
                        choices=['s3', 's4'],
                        help='The test to check (s3 or s4)')
    parser.add_argument('logfile',
                        action='store',
                        help='The log file to parse')

    args = parser.parse_args()

    logging.basicConfig(level=args.debug)

    #Create a generator and get our lines
    log = (line.rstrip() for line in open(args.logfile, 'rt', encoding="UTF-8"))

    # End result will be a dictionary with a key per level, value is another
    # dictionary with a key per test (s3, s4, ...) and a list of all failures
    # for each test.  Duplicates are possible, because we should also indicate
    # the number of instances for each failure.
    results = collections.defaultdict(lambda: collections.defaultdict(list))

    sum_acum = []
    summaries_found = 0

    # Start parsing the fwts log. Gather each "Test Failure Summary" section
    # and when it's complete, pass it to the parse_summary function to extract
    # levels and tests.
    for logline in log:
        if "Test Failure Summary" in logline:
            parse_summary(sum_acum, results)
            summaries_found += 1
            sum_acum = []
        else:
            sum_acum.append(logline)
    # We reached the end, so add the last accumulated summary
    if sum_acum:
        parse_summary(sum_acum, results)

    # Report what I found
    for level in sorted(results.keys()):
        if results[level]:  # Yes, we can have an empty level. We may have
                            # seen the levelheader but had it report no
                            # failures.
            print("{} failures:".format(level))
            for test in results[level].keys():
                print("  {}: {} failures".format(test,
                                                 len(results[level][test])))
                if args.verbose:
                    print('='*40)
                    counts = collections.Counter(results[level][test])
                    for failure in counts:
                        print("    {} (x {})".format(failure, counts[failure]))

    # Decide on the outcome based on the collected information
    if not summaries_found:
        logging.error("No fwts test summaries found, "
                      "possible malformed fwts log file")
        return_code = 2
    elif not results.keys():  # If it has no keys, means we didn't see any
                              # FWTS levels
        logging.error("None of the summaries contained failure levels, "
                      "possible malformed fwts log file")
        return_code = 2
    elif any(results.values()):  # If any of the results' levels has errors
        return_code = 1
    else:
        print("No errors detected")
        return_code = 0

    return return_code

if __name__ == '__main__':
    sys.exit(main())
