422 lines
16 KiB
C++
422 lines
16 KiB
C++
|
// Copyright 2011 The Kyua Authors.
|
||
|
// All rights reserved.
|
||
|
//
|
||
|
// Redistribution and use in source and binary forms, with or without
|
||
|
// modification, are permitted provided that the following conditions are
|
||
|
// met:
|
||
|
//
|
||
|
// * Redistributions of source code must retain the above copyright
|
||
|
// notice, this list of conditions and the following disclaimer.
|
||
|
// * Redistributions in binary form must reproduce the above copyright
|
||
|
// notice, this list of conditions and the following disclaimer in the
|
||
|
// documentation and/or other materials provided with the distribution.
|
||
|
// * Neither the name of Google Inc. nor the names of its contributors
|
||
|
// may be used to endorse or promote products derived from this software
|
||
|
// without specific prior written permission.
|
||
|
//
|
||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||
|
|
||
|
#include "cli/cmd_report.hpp"
|
||
|
|
||
|
#include <algorithm>
|
||
|
#include <cstddef>
|
||
|
#include <cstdlib>
|
||
|
#include <map>
|
||
|
#include <ostream>
|
||
|
#include <string>
|
||
|
#include <vector>
|
||
|
|
||
|
#include "cli/common.ipp"
|
||
|
#include "drivers/scan_results.hpp"
|
||
|
#include "model/context.hpp"
|
||
|
#include "model/metadata.hpp"
|
||
|
#include "model/test_case.hpp"
|
||
|
#include "model/test_program.hpp"
|
||
|
#include "model/test_result.hpp"
|
||
|
#include "model/types.hpp"
|
||
|
#include "store/layout.hpp"
|
||
|
#include "store/read_transaction.hpp"
|
||
|
#include "utils/cmdline/exceptions.hpp"
|
||
|
#include "utils/cmdline/options.hpp"
|
||
|
#include "utils/cmdline/parser.ipp"
|
||
|
#include "utils/cmdline/ui.hpp"
|
||
|
#include "utils/datetime.hpp"
|
||
|
#include "utils/defs.hpp"
|
||
|
#include "utils/format/macros.hpp"
|
||
|
#include "utils/fs/path.hpp"
|
||
|
#include "utils/optional.ipp"
|
||
|
#include "utils/sanity.hpp"
|
||
|
#include "utils/stream.hpp"
|
||
|
#include "utils/text/operations.ipp"
|
||
|
|
||
|
namespace cmdline = utils::cmdline;
|
||
|
namespace config = utils::config;
|
||
|
namespace datetime = utils::datetime;
|
||
|
namespace fs = utils::fs;
|
||
|
namespace layout = store::layout;
|
||
|
namespace text = utils::text;
|
||
|
|
||
|
using cli::cmd_report;
|
||
|
using utils::optional;
|
||
|
|
||
|
|
||
|
namespace {
|
||
|
|
||
|
|
||
|
/// Generates a plain-text report intended to be printed to the console.
|
||
|
class report_console_hooks : public drivers::scan_results::base_hooks {
|
||
|
/// Stream to which to write the report.
|
||
|
std::ostream& _output;
|
||
|
|
||
|
/// Whether to include details in the report or not.
|
||
|
const bool _verbose;
|
||
|
|
||
|
/// Collection of result types to include in the report.
|
||
|
const cli::result_types& _results_filters;
|
||
|
|
||
|
/// Path to the results file being read.
|
||
|
const fs::path& _results_file;
|
||
|
|
||
|
/// The start time of the first test.
|
||
|
optional< utils::datetime::timestamp > _start_time;
|
||
|
|
||
|
/// The end time of the last test.
|
||
|
optional< utils::datetime::timestamp > _end_time;
|
||
|
|
||
|
/// The total run time of the tests. Note that we cannot subtract _end_time
|
||
|
/// from _start_time to compute this due to parallel execution.
|
||
|
utils::datetime::delta _runtime;
|
||
|
|
||
|
/// Representation of a single result.
|
||
|
struct result_data {
|
||
|
/// The relative path to the test program.
|
||
|
utils::fs::path binary_path;
|
||
|
|
||
|
/// The name of the test case.
|
||
|
std::string test_case_name;
|
||
|
|
||
|
/// The result of the test case.
|
||
|
model::test_result result;
|
||
|
|
||
|
/// The duration of the test case execution.
|
||
|
utils::datetime::delta duration;
|
||
|
|
||
|
/// Constructs a new results data.
|
||
|
///
|
||
|
/// \param binary_path_ The relative path to the test program.
|
||
|
/// \param test_case_name_ The name of the test case.
|
||
|
/// \param result_ The result of the test case.
|
||
|
/// \param duration_ The duration of the test case execution.
|
||
|
result_data(const utils::fs::path& binary_path_,
|
||
|
const std::string& test_case_name_,
|
||
|
const model::test_result& result_,
|
||
|
const utils::datetime::delta& duration_) :
|
||
|
binary_path(binary_path_), test_case_name(test_case_name_),
|
||
|
result(result_), duration(duration_)
|
||
|
{
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/// Results received, broken down by their type.
|
||
|
///
|
||
|
/// Note that this may not include all results, as keeping the whole list in
|
||
|
/// memory may be too much.
|
||
|
std::map< model::test_result_type, std::vector< result_data > > _results;
|
||
|
|
||
|
/// Pretty-prints the value of an environment variable.
|
||
|
///
|
||
|
/// \param indent Prefix for the lines to print. Continuation lines
|
||
|
/// use this indentation twice.
|
||
|
/// \param name Name of the variable.
|
||
|
/// \param value Value of the variable. Can have newlines.
|
||
|
void
|
||
|
print_env_var(const char* indent, const std::string& name,
|
||
|
const std::string& value)
|
||
|
{
|
||
|
const std::vector< std::string > lines = text::split(value, '\n');
|
||
|
if (lines.size() == 0) {
|
||
|
_output << F("%s%s=\n") % indent % name;;
|
||
|
} else {
|
||
|
_output << F("%s%s=%s\n") % indent % name % lines[0];
|
||
|
for (std::vector< std::string >::size_type i = 1;
|
||
|
i < lines.size(); ++i) {
|
||
|
_output << F("%s%s%s\n") % indent % indent % lines[i];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Prints the execution context to the output.
|
||
|
///
|
||
|
/// \param context The context to dump.
|
||
|
void
|
||
|
print_context(const model::context& context)
|
||
|
{
|
||
|
_output << "===> Execution context\n";
|
||
|
|
||
|
_output << F("Current directory: %s\n") % context.cwd();
|
||
|
const std::map< std::string, std::string >& env = context.env();
|
||
|
if (env.empty())
|
||
|
_output << "No environment variables recorded\n";
|
||
|
else {
|
||
|
_output << "Environment variables:\n";
|
||
|
for (std::map< std::string, std::string >::const_iterator
|
||
|
iter = env.begin(); iter != env.end(); iter++) {
|
||
|
print_env_var(" ", (*iter).first, (*iter).second);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Dumps a detailed view of the test case.
|
||
|
///
|
||
|
/// \param result_iter Results iterator pointing at the test case to be
|
||
|
/// dumped.
|
||
|
void
|
||
|
print_test_case_and_result(const store::results_iterator& result_iter)
|
||
|
{
|
||
|
const model::test_case& test_case =
|
||
|
result_iter.test_program()->find(result_iter.test_case_name());
|
||
|
const model::properties_map props =
|
||
|
test_case.get_metadata().to_properties();
|
||
|
|
||
|
_output << F("===> %s:%s\n") %
|
||
|
result_iter.test_program()->relative_path() %
|
||
|
result_iter.test_case_name();
|
||
|
_output << F("Result: %s\n") %
|
||
|
cli::format_result(result_iter.result());
|
||
|
_output << F("Start time: %s\n") %
|
||
|
result_iter.start_time().to_iso8601_in_utc();
|
||
|
_output << F("End time: %s\n") %
|
||
|
result_iter.end_time().to_iso8601_in_utc();
|
||
|
_output << F("Duration: %s\n") %
|
||
|
cli::format_delta(result_iter.end_time() -
|
||
|
result_iter.start_time());
|
||
|
|
||
|
_output << "\n";
|
||
|
_output << "Metadata:\n";
|
||
|
for (model::properties_map::const_iterator iter = props.begin();
|
||
|
iter != props.end(); ++iter) {
|
||
|
if ((*iter).second.empty()) {
|
||
|
_output << F(" %s is empty\n") % (*iter).first;
|
||
|
} else {
|
||
|
_output << F(" %s = %s\n") % (*iter).first % (*iter).second;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const std::string stdout_contents = result_iter.stdout_contents();
|
||
|
if (!stdout_contents.empty()) {
|
||
|
_output << "\n"
|
||
|
<< "Standard output:\n"
|
||
|
<< stdout_contents;
|
||
|
}
|
||
|
|
||
|
const std::string stderr_contents = result_iter.stderr_contents();
|
||
|
if (!stderr_contents.empty()) {
|
||
|
_output << "\n"
|
||
|
<< "Standard error:\n"
|
||
|
<< stderr_contents;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Counts how many results of a given type have been received.
|
||
|
///
|
||
|
/// \param type Test result type to count results for.
|
||
|
///
|
||
|
/// \return The number of test results with \p type.
|
||
|
std::size_t
|
||
|
count_results(const model::test_result_type type)
|
||
|
{
|
||
|
const std::map< model::test_result_type,
|
||
|
std::vector< result_data > >::const_iterator iter =
|
||
|
_results.find(type);
|
||
|
if (iter == _results.end())
|
||
|
return 0;
|
||
|
else
|
||
|
return (*iter).second.size();
|
||
|
}
|
||
|
|
||
|
/// Prints a set of results.
|
||
|
///
|
||
|
/// \param type Test result type to print results for.
|
||
|
/// \param title Title used when printing results.
|
||
|
void
|
||
|
print_results(const model::test_result_type type,
|
||
|
const char* title)
|
||
|
{
|
||
|
const std::map< model::test_result_type,
|
||
|
std::vector< result_data > >::const_iterator iter2 =
|
||
|
_results.find(type);
|
||
|
if (iter2 == _results.end())
|
||
|
return;
|
||
|
const std::vector< result_data >& all = (*iter2).second;
|
||
|
|
||
|
_output << F("===> %s\n") % title;
|
||
|
for (std::vector< result_data >::const_iterator iter = all.begin();
|
||
|
iter != all.end(); iter++) {
|
||
|
_output << F("%s:%s -> %s [%s]\n") % (*iter).binary_path %
|
||
|
(*iter).test_case_name %
|
||
|
cli::format_result((*iter).result) %
|
||
|
cli::format_delta((*iter).duration);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public:
|
||
|
/// Constructor for the hooks.
|
||
|
///
|
||
|
/// \param [out] output_ Stream to which to write the report.
|
||
|
/// \param verbose_ Whether to include details in the output or not.
|
||
|
/// \param results_filters_ The result types to include in the report.
|
||
|
/// Cannot be empty.
|
||
|
/// \param results_file_ Path to the results file being read.
|
||
|
report_console_hooks(std::ostream& output_, const bool verbose_,
|
||
|
const cli::result_types& results_filters_,
|
||
|
const fs::path& results_file_) :
|
||
|
_output(output_),
|
||
|
_verbose(verbose_),
|
||
|
_results_filters(results_filters_),
|
||
|
_results_file(results_file_)
|
||
|
{
|
||
|
PRE(!results_filters_.empty());
|
||
|
}
|
||
|
|
||
|
/// Callback executed when the context is loaded.
|
||
|
///
|
||
|
/// \param context The context loaded from the database.
|
||
|
void
|
||
|
got_context(const model::context& context)
|
||
|
{
|
||
|
if (_verbose)
|
||
|
print_context(context);
|
||
|
}
|
||
|
|
||
|
/// Callback executed when a test results is found.
|
||
|
///
|
||
|
/// \param iter Container for the test result's data.
|
||
|
void
|
||
|
got_result(store::results_iterator& iter)
|
||
|
{
|
||
|
if (!_start_time || _start_time.get() > iter.start_time())
|
||
|
_start_time = iter.start_time();
|
||
|
if (!_end_time || _end_time.get() < iter.end_time())
|
||
|
_end_time = iter.end_time();
|
||
|
|
||
|
const datetime::delta duration = iter.end_time() - iter.start_time();
|
||
|
|
||
|
_runtime += duration;
|
||
|
const model::test_result result = iter.result();
|
||
|
_results[result.type()].push_back(
|
||
|
result_data(iter.test_program()->relative_path(),
|
||
|
iter.test_case_name(), iter.result(), duration));
|
||
|
|
||
|
if (_verbose) {
|
||
|
// TODO(jmmv): _results_filters is a list and is small enough for
|
||
|
// std::find to not be an expensive operation here (probably). But
|
||
|
// we should be using a std::set instead.
|
||
|
if (std::find(_results_filters.begin(), _results_filters.end(),
|
||
|
iter.result().type()) != _results_filters.end()) {
|
||
|
print_test_case_and_result(iter);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Prints the tests summary.
|
||
|
void
|
||
|
end(const drivers::scan_results::result& /* r */)
|
||
|
{
|
||
|
typedef std::map< model::test_result_type, const char* > types_map;
|
||
|
|
||
|
types_map titles;
|
||
|
titles[model::test_result_broken] = "Broken tests";
|
||
|
titles[model::test_result_expected_failure] = "Expected failures";
|
||
|
titles[model::test_result_failed] = "Failed tests";
|
||
|
titles[model::test_result_passed] = "Passed tests";
|
||
|
titles[model::test_result_skipped] = "Skipped tests";
|
||
|
|
||
|
for (cli::result_types::const_iterator iter = _results_filters.begin();
|
||
|
iter != _results_filters.end(); ++iter) {
|
||
|
const types_map::const_iterator match = titles.find(*iter);
|
||
|
INV_MSG(match != titles.end(), "Conditional does not match user "
|
||
|
"input validation in parse_types()");
|
||
|
print_results((*match).first, (*match).second);
|
||
|
}
|
||
|
|
||
|
const std::size_t broken = count_results(model::test_result_broken);
|
||
|
const std::size_t failed = count_results(model::test_result_failed);
|
||
|
const std::size_t passed = count_results(model::test_result_passed);
|
||
|
const std::size_t skipped = count_results(model::test_result_skipped);
|
||
|
const std::size_t xfail = count_results(
|
||
|
model::test_result_expected_failure);
|
||
|
const std::size_t total = broken + failed + passed + skipped + xfail;
|
||
|
|
||
|
_output << "===> Summary\n";
|
||
|
_output << F("Results read from %s\n") % _results_file;
|
||
|
_output << F("Test cases: %s total, %s skipped, %s expected failures, "
|
||
|
"%s broken, %s failed\n") %
|
||
|
total % skipped % xfail % broken % failed;
|
||
|
if (_verbose && _start_time) {
|
||
|
INV(_end_time);
|
||
|
_output << F("Start time: %s\n") %
|
||
|
_start_time.get().to_iso8601_in_utc();
|
||
|
_output << F("End time: %s\n") %
|
||
|
_end_time.get().to_iso8601_in_utc();
|
||
|
}
|
||
|
_output << F("Total time: %s\n") % cli::format_delta(_runtime);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
|
||
|
} // anonymous namespace
|
||
|
|
||
|
|
||
|
/// Default constructor for cmd_report.
|
||
|
cmd_report::cmd_report(void) : cli_command(
|
||
|
"report", "", 0, -1,
|
||
|
"Generates a report with the results of a test suite run")
|
||
|
{
|
||
|
add_option(results_file_open_option);
|
||
|
add_option(cmdline::bool_option(
|
||
|
"verbose", "Include the execution context and the details of each test "
|
||
|
"case in the report"));
|
||
|
add_option(cmdline::path_option("output", "Path to the output file", "path",
|
||
|
"/dev/stdout"));
|
||
|
add_option(results_filter_option);
|
||
|
}
|
||
|
|
||
|
|
||
|
/// Entry point for the "report" subcommand.
|
||
|
///
|
||
|
/// \param ui Object to interact with the I/O of the program.
|
||
|
/// \param cmdline Representation of the command line to the subcommand.
|
||
|
///
|
||
|
/// \return 0 if everything is OK, 1 if the statement is invalid or if there is
|
||
|
/// any other problem.
|
||
|
int
|
||
|
cmd_report::run(cmdline::ui* ui,
|
||
|
const cmdline::parsed_cmdline& cmdline,
|
||
|
const config::tree& /* user_config */)
|
||
|
{
|
||
|
std::auto_ptr< std::ostream > output = utils::open_ostream(
|
||
|
cmdline.get_option< cmdline::path_option >("output"));
|
||
|
|
||
|
const fs::path results_file = layout::find_results(
|
||
|
results_file_open(cmdline));
|
||
|
|
||
|
const result_types types = get_result_types(cmdline);
|
||
|
report_console_hooks hooks(*output.get(), cmdline.has_option("verbose"),
|
||
|
types, results_file);
|
||
|
const drivers::scan_results::result result = drivers::scan_results::drive(
|
||
|
results_file, parse_filters(cmdline.arguments()), hooks);
|
||
|
|
||
|
return report_unused_filters(result.unused_filters, ui) ?
|
||
|
EXIT_FAILURE : EXIT_SUCCESS;
|
||
|
}
|