freebsd-dev/contrib/kyua/model/test_program.cpp
Brooks Davis b0d29bc47d Import the kyua test framework.
Having kyua in the base system will simplify automated testing in CI and
eliminates bootstrapping issues on new platforms.

The build of kyua is controlled by WITH(OUT)_TESTS_SUPPORT.

Reviewed by:	emaste
Obtained from:	CheriBSD
Sponsored by:	DARPA
Differential Revision:	https://reviews.freebsd.org/D24103
2020-03-23 19:01:23 +00:00

453 lines
14 KiB
C++

// Copyright 2010 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 "model/test_program.hpp"
#include <map>
#include <sstream>
#include "model/exceptions.hpp"
#include "model/metadata.hpp"
#include "model/test_case.hpp"
#include "model/test_result.hpp"
#include "utils/format/containers.ipp"
#include "utils/format/macros.hpp"
#include "utils/fs/path.hpp"
#include "utils/noncopyable.hpp"
#include "utils/sanity.hpp"
#include "utils/text/operations.ipp"
namespace fs = utils::fs;
namespace text = utils::text;
using utils::none;
/// Internal implementation of a test_program.
struct model::test_program::impl : utils::noncopyable {
/// Name of the test program interface.
std::string interface_name;
/// Name of the test program binary relative to root.
fs::path binary;
/// Root of the test suite containing the test program.
fs::path root;
/// Name of the test suite this program belongs to.
std::string test_suite_name;
/// Metadata of the test program.
model::metadata md;
/// List of test cases in the test program.
///
/// Must be queried via the test_program::test_cases() method.
model::test_cases_map test_cases;
/// Constructor.
///
/// \param interface_name_ Name of the test program interface.
/// \param binary_ The name of the test program binary relative to root_.
/// \param root_ The root of the test suite containing the test program.
/// \param test_suite_name_ The name of the test suite this program
/// belongs to.
/// \param md_ Metadata of the test program.
/// \param test_cases_ The collection of test cases in the test program.
impl(const std::string& interface_name_, const fs::path& binary_,
const fs::path& root_, const std::string& test_suite_name_,
const model::metadata& md_, const model::test_cases_map& test_cases_) :
interface_name(interface_name_),
binary(binary_),
root(root_),
test_suite_name(test_suite_name_),
md(md_)
{
PRE_MSG(!binary.is_absolute(),
F("The program '%s' must be relative to the root of the test "
"suite '%s'") % binary % root);
set_test_cases(test_cases_);
}
/// Sets the list of test cases of the test program.
///
/// \param test_cases_ The new list of test cases.
void
set_test_cases(const model::test_cases_map& test_cases_)
{
for (model::test_cases_map::const_iterator iter = test_cases_.begin();
iter != test_cases_.end(); ++iter) {
const std::string& name = (*iter).first;
const model::test_case& test_case = (*iter).second;
PRE_MSG(name == test_case.name(),
F("The test case '%s' has been registered with the "
"non-matching name '%s'") % name % test_case.name());
test_cases.insert(model::test_cases_map::value_type(
name, test_case.apply_metadata_defaults(&md)));
}
INV(test_cases.size() == test_cases_.size());
}
};
/// Constructs a new test program.
///
/// \param interface_name_ Name of the test program interface.
/// \param binary_ The name of the test program binary relative to root_.
/// \param root_ The root of the test suite containing the test program.
/// \param test_suite_name_ The name of the test suite this program belongs to.
/// \param md_ Metadata of the test program.
/// \param test_cases_ The collection of test cases in the test program.
model::test_program::test_program(const std::string& interface_name_,
const fs::path& binary_,
const fs::path& root_,
const std::string& test_suite_name_,
const model::metadata& md_,
const model::test_cases_map& test_cases_) :
_pimpl(new impl(interface_name_, binary_, root_, test_suite_name_, md_,
test_cases_))
{
}
/// Destroys a test program.
model::test_program::~test_program(void)
{
}
/// Gets the name of the test program interface.
///
/// \return An interface name.
const std::string&
model::test_program::interface_name(void) const
{
return _pimpl->interface_name;
}
/// Gets the path to the test program relative to the root of the test suite.
///
/// \return The relative path to the test program binary.
const fs::path&
model::test_program::relative_path(void) const
{
return _pimpl->binary;
}
/// Gets the absolute path to the test program.
///
/// \return The absolute path to the test program binary.
const fs::path
model::test_program::absolute_path(void) const
{
const fs::path full_path = _pimpl->root / _pimpl->binary;
return full_path.is_absolute() ? full_path : full_path.to_absolute();
}
/// Gets the root of the test suite containing this test program.
///
/// \return The path to the root of the test suite.
const fs::path&
model::test_program::root(void) const
{
return _pimpl->root;
}
/// Gets the name of the test suite containing this test program.
///
/// \return The name of the test suite.
const std::string&
model::test_program::test_suite_name(void) const
{
return _pimpl->test_suite_name;
}
/// Gets the metadata of the test program.
///
/// \return The metadata.
const model::metadata&
model::test_program::get_metadata(void) const
{
return _pimpl->md;
}
/// Gets a test case by its name.
///
/// \param name The name of the test case to locate.
///
/// \return The requested test case.
///
/// \throw not_found_error If the specified test case is not in the test
/// program.
const model::test_case&
model::test_program::find(const std::string& name) const
{
const test_cases_map& tcs = test_cases();
const test_cases_map::const_iterator iter = tcs.find(name);
if (iter == tcs.end())
throw not_found_error(F("Unknown test case %s in test program %s") %
name % relative_path());
return (*iter).second;
}
/// Gets the list of test cases from the test program.
///
/// \return The list of test cases provided by the test program.
const model::test_cases_map&
model::test_program::test_cases(void) const
{
return _pimpl->test_cases;
}
/// Sets the list of test cases of the test program.
///
/// This can only be called once and it may only be called from within
/// overridden test_cases() before that method ever returns a value for the
/// first time. Any other invocations will result in inconsistent program
/// state.
///
/// \param test_cases_ The new list of test cases.
void
model::test_program::set_test_cases(const model::test_cases_map& test_cases_)
{
PRE(_pimpl->test_cases.empty());
_pimpl->set_test_cases(test_cases_);
}
/// Equality comparator.
///
/// \param other The other object to compare this one to.
///
/// \return True if this object and other are equal; false otherwise.
bool
model::test_program::operator==(const test_program& other) const
{
return _pimpl == other._pimpl || (
_pimpl->interface_name == other._pimpl->interface_name &&
_pimpl->binary == other._pimpl->binary &&
_pimpl->root == other._pimpl->root &&
_pimpl->test_suite_name == other._pimpl->test_suite_name &&
_pimpl->md == other._pimpl->md &&
test_cases() == other.test_cases());
}
/// Inequality comparator.
///
/// \param other The other object to compare this one to.
///
/// \return True if this object and other are different; false otherwise.
bool
model::test_program::operator!=(const test_program& other) const
{
return !(*this == other);
}
/// Less-than comparator.
///
/// A test program is considered to be less than another if and only if the
/// former's absolute path is less than the absolute path of the latter. In
/// other words, the absolute path is used here as the test program's
/// identifier.
///
/// This simplistic less-than operator overload is provided so that test
/// programs can be held in sets and other containers.
///
/// \param other The other object to compare this one to.
///
/// \return True if this object sorts before the other object; false otherwise.
bool
model::test_program::operator<(const test_program& other) const
{
return absolute_path() < other.absolute_path();
}
/// Injects the object into a stream.
///
/// \param output The stream into which to inject the object.
/// \param object The object to format.
///
/// \return The output stream.
std::ostream&
model::operator<<(std::ostream& output, const test_program& object)
{
output << F("test_program{interface=%s, binary=%s, root=%s, test_suite=%s, "
"metadata=%s, test_cases=%s}")
% text::quote(object.interface_name(), '\'')
% text::quote(object.relative_path().str(), '\'')
% text::quote(object.root().str(), '\'')
% text::quote(object.test_suite_name(), '\'')
% object.get_metadata()
% object.test_cases();
return output;
}
/// Internal implementation of the test_program_builder class.
struct model::test_program_builder::impl : utils::noncopyable {
/// Partially-constructed program with only the required properties.
model::test_program prototype;
/// Optional metadata for the test program.
model::metadata metadata;
/// Collection of test cases.
model::test_cases_map test_cases;
/// Whether we have created a test_program object or not.
bool built;
/// Constructor.
///
/// \param prototype_ The partially constructed program with only the
/// required properties.
impl(const model::test_program& prototype_) :
prototype(prototype_),
metadata(model::metadata_builder().build()),
built(false)
{
}
};
/// Constructs a new builder with non-optional values.
///
/// \param interface_name_ Name of the test program interface.
/// \param binary_ The name of the test program binary relative to root_.
/// \param root_ The root of the test suite containing the test program.
/// \param test_suite_name_ The name of the test suite this program belongs to.
model::test_program_builder::test_program_builder(
const std::string& interface_name_, const fs::path& binary_,
const fs::path& root_, const std::string& test_suite_name_) :
_pimpl(new impl(model::test_program(interface_name_, binary_, root_,
test_suite_name_,
model::metadata_builder().build(),
model::test_cases_map())))
{
}
/// Destructor.
model::test_program_builder::~test_program_builder(void)
{
}
/// Accumulates an additional test case with default metadata.
///
/// \param test_case_name The name of the test case.
///
/// \return A reference to this builder.
model::test_program_builder&
model::test_program_builder::add_test_case(const std::string& test_case_name)
{
return add_test_case(test_case_name, model::metadata_builder().build());
}
/// Accumulates an additional test case.
///
/// \param test_case_name The name of the test case.
/// \param metadata The test case metadata.
///
/// \return A reference to this builder.
model::test_program_builder&
model::test_program_builder::add_test_case(const std::string& test_case_name,
const model::metadata& metadata)
{
const model::test_case test_case(test_case_name, metadata);
PRE_MSG(_pimpl->test_cases.find(test_case_name) == _pimpl->test_cases.end(),
F("Attempted to re-register test case '%s'") % test_case_name);
_pimpl->test_cases.insert(model::test_cases_map::value_type(
test_case_name, test_case));
return *this;
}
/// Sets the test program metadata.
///
/// \return metadata The metadata for the test program.
///
/// \return A reference to this builder.
model::test_program_builder&
model::test_program_builder::set_metadata(const model::metadata& metadata)
{
_pimpl->metadata = metadata;
return *this;
}
/// Creates a new test_program object.
///
/// \pre This has not yet been called. We only support calling this function
/// once.
///
/// \return The constructed test_program object.
model::test_program
model::test_program_builder::build(void) const
{
PRE(!_pimpl->built);
_pimpl->built = true;
return test_program(_pimpl->prototype.interface_name(),
_pimpl->prototype.relative_path(),
_pimpl->prototype.root(),
_pimpl->prototype.test_suite_name(),
_pimpl->metadata,
_pimpl->test_cases);
}
/// Creates a new dynamically-allocated test_program object.
///
/// \pre This has not yet been called. We only support calling this function
/// once.
///
/// \return The constructed test_program object.
model::test_program_ptr
model::test_program_builder::build_ptr(void) const
{
const test_program result = build();
return test_program_ptr(new test_program(result));
}