/************************************************************************
 *
 * output.cpp - Definitions of the result parsing subsystem
 *
 * $Id: output.cpp 580483 2007-09-28 20:55:52Z sebor $
 *
 ************************************************************************
 *
 * Licensed to the Apache Software  Foundation (ASF) under one or more
 * contributor  license agreements.  See  the NOTICE  file distributed
 * with  this  work  for  additional information  regarding  copyright
 * ownership.   The ASF  licenses this  file to  you under  the Apache
 * License, Version  2.0 (the  "License"); you may  not use  this file
 * except in  compliance with the License.   You may obtain  a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the  License is distributed on an  "AS IS" BASIS,
 * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY  KIND, either  express or
 * implied.   See  the License  for  the  specific language  governing
 * permissions and limitations under the License.
 * 
 **************************************************************************/

#include <assert.h>
#include <errno.h>
#include <stdio.h> /* for printf, puts, FILE, f* */
#include <stdlib.h> /* for exit, free */
#include <string.h> /* for str* */

#include <sys/types.h>

#include <sys/stat.h>

#include "cmdopt.h"
#include "display.h"
#include "util.h"

#include "output.h"

#ifndef ENOENT
#  define ENOENT 2
#endif   /* ENOENT */


/**
   Parses contents of the open file handle data for test target_name.
   
   This method reads the data file handle, looking for the output summary from 
   the test driver.  The results of this search (and the parsing of this 
   summary, if found) are stored in the status structure.

   @param data pointer to file structure for output file being parsed
   @param status status object to record results in.
*/
static void
check_test (FILE* data, struct target_status* status)
{
    unsigned r_lvl    = 0;   /* diagnostic severity level */
    unsigned r_active = 0;   /* number of active diagnostics */
    unsigned r_total  = 0;   /* total number of diagnostics */

    int fmt_ok = 0;          /* is output format okay? */

    unsigned fsm = 0;        /* state */
    char tok;                /* current character */

    const char* const target_name = get_target ();

    assert (0 != target_name);
    assert (0 != data);
    assert (0 != status);

    tok = fgetc (data);

    if (feof (data)) {
        /* target produced no output (regression test?) */
        status->status = ST_NO_OUTPUT;
        return;
    }

    for ( ; fsm < 6 && !feof (data); tok = fgetc (data)) {
        switch (tok) {
        case '\n':
            fsm = 1;
            break;
        case '#':
            fsm = (1 == fsm) ? 2 : 0;
            break;
        case ' ':
            fsm = (2 == fsm) ? 3 : ((4 == fsm) ? 5 : 0);
            break;
        case '|':
            fsm = (3 == fsm) ? 4 : 0;
            break;
        case '(':
            if (5 == fsm) {
                fseek (data, -6, SEEK_CUR);
                fsm++;
                break;
            }
        default:
            fsm = 0;
        }
    }

    if (!feof (data)) {
        while (3 == fscanf (data, "# | (S%u) %*s | %u | %u | %*u%% |\n", 
                            &r_lvl, &r_active, &r_total)) {
            if (6 < r_lvl) {
                /* The 0.new tests produces errors, and are all 
                   expected to be active, so invert the total */
                if (8 == r_lvl && 0 == strcmp (target_name, "0.new"))
                    r_active = r_total-r_active;
                status->failed += r_active;
                status->assert += r_total;
                if (   status->failed < r_active 
                    || status->assert < r_total
                    || (unsigned int)-1 == status->assert) {
                    status->status = ST_OVERFLOW;
                    return;
                }
            }
            else if (1 < r_lvl) {
                status->t_warn += r_active;
                if (status->t_warn < r_active) {
                    status->t_warn = (unsigned int)-1;
                }
            }
            fmt_ok = 1;
        }
    }
    
    if (!fmt_ok)
        status->status = ST_FORMAT;
}

/**
   Parses contents of the open file handle data for test target_name.
   
   This method is a reimplementation of check_test ().  The difference between
   this method and check_test () is how it parses the results.  This version
   parses compatability layer output, rather than the test driver output.

   @param data pointer to file structure for output file being parsed
   @see check_test ()
*/
static void
check_compat_test (FILE* data, struct target_status* status)
{
    int read = 0;
    unsigned fsm = 0;
    char tok;

    assert (0 != data);
    assert (0 != status);

    tok = fgetc (data);

    if (feof (data)) {
        /* target produced no output (regression test?) */
        status->status = ST_NO_OUTPUT;
        return;
    }

    for ( ; !feof (data); tok = fgetc (data)) {
        switch (tok) {
        case '\n':
            fsm = 1;
            break;
        case '#':
            if (1 == fsm || 2 == fsm)
                ++fsm;
            else
                fsm = 0;
            break;
        case ' ':
            if (3 == fsm) 
                ++fsm;
            else
                fsm = 0;
            break;
        case 'W':
            if (4 == fsm && !feof (data)) /* leading "## W" eaten */
                read = fscanf (data, "arnings = %u\n## Assertions = %u\n"
                       "## FailedAssertions = %u",
                       &status->t_warn, &status->assert, &status->failed);
        default:
            fsm = 0;
        }
    }
    if (3 != read) {
        status->status = ST_FORMAT;
    }
}

/**
   Arbitrary constant controling static read buffer size.

   @see check_example ()
*/
#define DELTA_BUF_LEN 64

/**
   Parses output file out_name for the example target_name.
   
   This method tries to compare out_name against a reference output file and
   reports the result of the comparison.  If the comparison fails to result in
   a match, status->status is set to the appropriate error state.  The 
   reference file name is generated by the reference_name function.

   Reference file locations:
     - [data_dir]/manual/out/[target_name].out
     - [data_dir]/tutorial/out/[target_name].out

   @todo add logic to handle differing line endings (CR vs LF vs CRLF)

   @param out_name the name of the output file
   @param fout pointer to file structure for output file being parsed
   @param status status object to record results in.
   @see reference_name
*/
static void
check_example (const char* const data_dir, char* const out_name, FILE* fout, 
               struct target_status* status)
{
    struct stat file_info;
    char* ref_name;

    FILE* fref;   /* reference file (expected output) */

    assert (0 != out_name);
    assert (0 != fout);
    assert (0 != status);

    /* Mark as an example by setting assertions to -1 */
    status->assert = (unsigned)-1;

    /* Try data_dir/manual/out/target_name.out */
    ref_name = reference_name (data_dir, "manual", "out");

    if (0 > stat (ref_name, &file_info)) {
        if (ENOENT != errno) {
            warn ("stat (%s) error: %s\n", exe_name, ref_name, 
                  strerror (errno));
            status->status = ST_BAD_REF;
            free (ref_name);
            return;
        }
                        
        /* If that doesn't exist, try 
           data_dir/tutorial/out/target_name.out */
        free (ref_name);
        ref_name = reference_name (data_dir, "tutorial", "out");

        if (0 > stat (ref_name, &file_info)) {
            if (ENOENT != errno) {
                warn ("stat (%s) error: %s\n", exe_name, ref_name, 
                      strerror (errno));
                status->status = ST_BAD_REF;
            }
            else
                status->status = ST_NO_REF;

            free (ref_name);
            return;
        }
    }

    fref = fopen (ref_name, "r");

    if (0 == fref) {
        int cache = errno; /* caching errno, as puts could alter it */
        if (ENOENT != cache)
            warn ("Error opening %s: %s\n", ref_name, strerror (cache));
        status->status = ST_BAD_REF;
    }
    else {
        int match = 1;   /* do the two files match? */

        while (!feof (fref) && !feof (fout)) {

            char buf [2][DELTA_BUF_LEN];

            size_t nread [2];   /* bytes read from the output/ref file */

            /* First, read a block from the files into the buffer */
            nread [0] = fread (buf [0], 1, sizeof buf [0], fout);
            if (ferror (fout)) {
                warn ("Error reading %s: %s\n", out_name, strerror (errno));
                match = 0;
                break;
            }

            nread [1] = fread (buf [1], 1, sizeof buf [1], fref);
            if (ferror (fref)) {
                warn ("Error reading %s: %s\n", ref_name, strerror (errno));
                match = 0;
                break;
            }

            /* Then, check if the amount of data read or the state of the 
               files differs
            */
            if (nread [0] != nread [1] || feof (fref) != feof (fout)) {
                match = 0;
                break;
            }

            /* Finally, check if the contents of the buffers differ */
            if (0 != memcmp (buf [0], buf [1], nread [0])) {
                match = 0;
                break;
            }
        }

        if (!match)
            status->status = ST_BAD_OUTPUT;

        fclose (fref);
    }
    free (ref_name);
}

void
parse_output (const struct target_opts* options, struct target_status* status)
{
    char* out_name;
    FILE* data;

    assert (0 != status);
    assert (0 != options->argv [0]);
    out_name = output_name (options->argv [0]);

    data = fopen (out_name, "r");

    if (0 == data) {
        if (ENOENT != errno)
            warn ("Error opening %s: %s\n", out_name, strerror (errno));
        status->status = ST_NO_OUTPUT;
    }
    else {
        if (0 == options->data_dir || '\0' == *options->data_dir) {
            /* If there is not an input directory, look at the assertion tags */

            if (!options->compat)
                check_test (data, status);
            else
                check_compat_test (data, status);
        }
        else {
            /* Otherwise, diff against the output file */
            check_example (options->data_dir, out_name, data, status);
        }
        fclose (data);
    }
    free (out_name);
}
