aboutsummaryrefslogtreecommitdiffstats
path: root/model
diff options
context:
space:
mode:
authorBrooks Davis <brooks@FreeBSD.org>2020-03-17 16:56:50 +0000
committerBrooks Davis <brooks@FreeBSD.org>2020-03-17 16:56:50 +0000
commit08334c51dbb99d9ecd2bb86a2d94ed06da9e167a (patch)
treec43eb24d59bd5c963583a5190caef80fc8387322 /model
downloadsrc-08334c51dbb99d9ecd2bb86a2d94ed06da9e167a.tar.gz
src-08334c51dbb99d9ecd2bb86a2d94ed06da9e167a.zip
Import the kyua testing framework for infrastructure softwarevendor/kyua/0.13-a685f91vendor/kyua
Imported at 0.13 plus assumulated changes to git hash a685f91. Obtained from: https://github.com/jmmv/kyua Sponsored by: DARPA
Notes
Notes: svn path=/vendor/kyua/dist/; revision=359042 svn path=/vendor/kyua/0.13-a685f91/; revision=359043; tag=vendor/kyua/0.13-a685f91
Diffstat (limited to 'model')
-rw-r--r--model/Kyuafile10
-rw-r--r--model/Makefile.am.inc89
-rw-r--r--model/README11
-rw-r--r--model/context.cpp159
-rw-r--r--model/context.hpp76
-rw-r--r--model/context_fwd.hpp43
-rw-r--r--model/context_test.cpp106
-rw-r--r--model/exceptions.cpp76
-rw-r--r--model/exceptions.hpp71
-rw-r--r--model/exceptions_test.cpp65
-rw-r--r--model/metadata.cpp1068
-rw-r--r--model/metadata.hpp130
-rw-r--r--model/metadata_fwd.hpp44
-rw-r--r--model/metadata_test.cpp461
-rw-r--r--model/test_case.cpp339
-rw-r--r--model/test_case.hpp98
-rw-r--r--model/test_case_fwd.hpp51
-rw-r--r--model/test_case_test.cpp263
-rw-r--r--model/test_program.cpp452
-rw-r--r--model/test_program.hpp110
-rw-r--r--model/test_program_fwd.hpp55
-rw-r--r--model/test_program_test.cpp711
-rw-r--r--model/test_result.cpp142
-rw-r--r--model/test_result.hpp79
-rw-r--r--model/test_result_fwd.hpp53
-rw-r--r--model/test_result_test.cpp185
-rw-r--r--model/types.hpp61
27 files changed, 5008 insertions, 0 deletions
diff --git a/model/Kyuafile b/model/Kyuafile
new file mode 100644
index 000000000000..9dae3b9c64ce
--- /dev/null
+++ b/model/Kyuafile
@@ -0,0 +1,10 @@
+syntax(2)
+
+test_suite("kyua")
+
+atf_test_program{name="context_test"}
+atf_test_program{name="exceptions_test"}
+atf_test_program{name="metadata_test"}
+atf_test_program{name="test_case_test"}
+atf_test_program{name="test_program_test"}
+atf_test_program{name="test_result_test"}
diff --git a/model/Makefile.am.inc b/model/Makefile.am.inc
new file mode 100644
index 000000000000..2bd33914f680
--- /dev/null
+++ b/model/Makefile.am.inc
@@ -0,0 +1,89 @@
+# Copyright 2014 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.
+
+MODEL_CFLAGS = $(UTILS_CFLAGS)
+MODEL_LIBS = libmodel.a $(UTILS_LIBS)
+
+noinst_LIBRARIES += libmodel.a
+libmodel_a_CPPFLAGS = $(UTILS_CFLAGS)
+libmodel_a_SOURCES = model/context.cpp
+libmodel_a_SOURCES += model/context.hpp
+libmodel_a_SOURCES += model/context_fwd.hpp
+libmodel_a_SOURCES += model/exceptions.cpp
+libmodel_a_SOURCES += model/exceptions.hpp
+libmodel_a_SOURCES += model/metadata.cpp
+libmodel_a_SOURCES += model/metadata.hpp
+libmodel_a_SOURCES += model/metadata_fwd.hpp
+libmodel_a_SOURCES += model/test_case.cpp
+libmodel_a_SOURCES += model/test_case.hpp
+libmodel_a_SOURCES += model/test_case_fwd.hpp
+libmodel_a_SOURCES += model/test_program.cpp
+libmodel_a_SOURCES += model/test_program.hpp
+libmodel_a_SOURCES += model/test_program_fwd.hpp
+libmodel_a_SOURCES += model/test_result.cpp
+libmodel_a_SOURCES += model/test_result.hpp
+libmodel_a_SOURCES += model/test_result_fwd.hpp
+libmodel_a_SOURCES += model/types.hpp
+
+if WITH_ATF
+tests_modeldir = $(pkgtestsdir)/model
+
+tests_model_DATA = model/Kyuafile
+EXTRA_DIST += $(tests_model_DATA)
+
+tests_model_PROGRAMS = model/context_test
+model_context_test_SOURCES = model/context_test.cpp
+model_context_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS)
+model_context_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS)
+
+tests_model_PROGRAMS += model/exceptions_test
+model_exceptions_test_SOURCES = model/exceptions_test.cpp
+model_exceptions_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS)
+model_exceptions_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS)
+
+tests_model_PROGRAMS += model/metadata_test
+model_metadata_test_SOURCES = model/metadata_test.cpp
+model_metadata_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS)
+model_metadata_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS)
+
+tests_model_PROGRAMS += model/test_case_test
+model_test_case_test_SOURCES = model/test_case_test.cpp
+model_test_case_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS)
+model_test_case_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS)
+
+tests_model_PROGRAMS += model/test_program_test
+model_test_program_test_SOURCES = model/test_program_test.cpp
+model_test_program_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS)
+model_test_program_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS)
+
+tests_model_PROGRAMS += model/test_result_test
+model_test_result_test_SOURCES = model/test_result_test.cpp
+model_test_result_test_CXXFLAGS = $(MODEL_CFLAGS) $(ATF_CXX_CFLAGS)
+model_test_result_test_LDADD = $(MODEL_LIBS) $(ATF_CXX_LIBS)
+
+endif
diff --git a/model/README b/model/README
new file mode 100644
index 000000000000..cf13a82b7338
--- /dev/null
+++ b/model/README
@@ -0,0 +1,11 @@
+This directory contains the classes that form the data model of Kyua.
+
+The classes in this directory are intended to be pure data types without
+any complex logic. As such, they are simple containers and support the
+common operations you would expect from them: in particular, comparisons
+and formatting for debugging purposes.
+
+All the classes in the data model have to have an on-disk representation
+provided by the store module; if they don't, they don't belong in the
+model. Some of these classes may also have special behavior at run-time,
+and this is provided by the engine module.
diff --git a/model/context.cpp b/model/context.cpp
new file mode 100644
index 000000000000..5afe89759d94
--- /dev/null
+++ b/model/context.cpp
@@ -0,0 +1,159 @@
+// 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 "model/context.hpp"
+
+#include "utils/format/macros.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/noncopyable.hpp"
+#include "utils/text/operations.ipp"
+
+namespace fs = utils::fs;
+namespace text = utils::text;
+
+
+/// Internal implementation of a context.
+struct model::context::impl : utils::noncopyable {
+ /// The current working directory.
+ fs::path _cwd;
+
+ /// The environment variables.
+ std::map< std::string, std::string > _env;
+
+ /// Constructor.
+ ///
+ /// \param cwd_ The current working directory.
+ /// \param env_ The environment variables.
+ impl(const fs::path& cwd_,
+ const std::map< std::string, std::string >& env_) :
+ _cwd(cwd_),
+ _env(env_)
+ {
+ }
+
+ /// Equality comparator.
+ ///
+ /// \param other The object to compare to.
+ ///
+ /// \return True if the two objects are equal; false otherwise.
+ bool
+ operator==(const impl& other) const
+ {
+ return _cwd == other._cwd && _env == other._env;
+ }
+};
+
+
+/// Constructs a new context.
+///
+/// \param cwd_ The current working directory.
+/// \param env_ The environment variables.
+model::context::context(const fs::path& cwd_,
+ const std::map< std::string, std::string >& env_) :
+ _pimpl(new impl(cwd_, env_))
+{
+}
+
+
+/// Destructor.
+model::context::~context(void)
+{
+}
+
+
+/// Returns the current working directory of the context.
+///
+/// \return A path.
+const fs::path&
+model::context::cwd(void) const
+{
+ return _pimpl->_cwd;
+}
+
+
+/// Returns the environment variables of the context.
+///
+/// \return A variable name to variable value mapping.
+const std::map< std::string, std::string >&
+model::context::env(void) const
+{
+ return _pimpl->_env;
+}
+
+
+/// 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::context::operator==(const context& other) const
+{
+ return *_pimpl == *other._pimpl;
+}
+
+
+/// 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::context::operator!=(const context& other) const
+{
+ return !(*this == other);
+}
+
+
+/// 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 context& object)
+{
+ output << F("context{cwd=%s, env=[")
+ % text::quote(object.cwd().str(), '\'');
+
+ const std::map< std::string, std::string >& env = object.env();
+ bool first = true;
+ for (std::map< std::string, std::string >::const_iterator
+ iter = env.begin(); iter != env.end(); ++iter) {
+ if (!first)
+ output << ", ";
+ first = false;
+
+ output << F("%s=%s") % (*iter).first
+ % text::quote((*iter).second, '\'');
+ }
+
+ output << "]}";
+ return output;
+}
diff --git a/model/context.hpp b/model/context.hpp
new file mode 100644
index 000000000000..d11ae8ba80b9
--- /dev/null
+++ b/model/context.hpp
@@ -0,0 +1,76 @@
+// 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.
+
+/// \file model/context.hpp
+/// Representation of runtime contexts.
+
+#if !defined(MODEL_CONTEXT_HPP)
+#define MODEL_CONTEXT_HPP
+
+#include "model/context_fwd.hpp"
+
+#include <map>
+#include <memory>
+#include <ostream>
+#include <string>
+
+#include "utils/fs/path_fwd.hpp"
+
+namespace model {
+
+
+/// Representation of a runtime context.
+///
+/// The instances of this class are unique (i.e. copying the objects only yields
+/// a shallow copy that shares the same internal implementation). This is a
+/// requirement for the 'store' API model.
+class context {
+ struct impl;
+
+ /// Pointer to the shared internal implementation.
+ std::shared_ptr< impl > _pimpl;
+
+public:
+ context(const utils::fs::path&,
+ const std::map< std::string, std::string >&);
+ ~context(void);
+
+ const utils::fs::path& cwd(void) const;
+ const std::map< std::string, std::string >& env(void) const;
+
+ bool operator==(const context&) const;
+ bool operator!=(const context&) const;
+};
+
+
+std::ostream& operator<<(std::ostream&, const context&);
+
+
+} // namespace model
+
+#endif // !defined(MODEL_CONTEXT_HPP)
diff --git a/model/context_fwd.hpp b/model/context_fwd.hpp
new file mode 100644
index 000000000000..000ed864e948
--- /dev/null
+++ b/model/context_fwd.hpp
@@ -0,0 +1,43 @@
+// Copyright 2014 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.
+
+/// \file model/context_fwd.hpp
+/// Forward declarations for model/context.hpp
+
+#if !defined(MODEL_CONTEXT_FWD_HPP)
+#define MODEL_CONTEXT_FWD_HPP
+
+namespace model {
+
+
+class context;
+
+
+} // namespace model
+
+#endif // !defined(MODEL_CONTEXT_FWD_HPP)
diff --git a/model/context_test.cpp b/model/context_test.cpp
new file mode 100644
index 000000000000..8990990710f2
--- /dev/null
+++ b/model/context_test.cpp
@@ -0,0 +1,106 @@
+// 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 "model/context.hpp"
+
+#include <map>
+#include <sstream>
+#include <string>
+
+#include <atf-c++.hpp>
+
+#include "utils/fs/path.hpp"
+
+namespace fs = utils::fs;
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(ctor_and_getters);
+ATF_TEST_CASE_BODY(ctor_and_getters)
+{
+ std::map< std::string, std::string > env;
+ env["foo"] = "first";
+ env["bar"] = "second";
+ const model::context context(fs::path("/foo/bar"), env);
+ ATF_REQUIRE_EQ(fs::path("/foo/bar"), context.cwd());
+ ATF_REQUIRE(env == context.env());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne);
+ATF_TEST_CASE_BODY(operators_eq_and_ne)
+{
+ std::map< std::string, std::string > env;
+ env["foo"] = "first";
+ const model::context context1(fs::path("/foo/bar"), env);
+ const model::context context2(fs::path("/foo/bar"), env);
+ const model::context context3(fs::path("/foo/baz"), env);
+ env["bar"] = "second";
+ const model::context context4(fs::path("/foo/bar"), env);
+ ATF_REQUIRE( context1 == context2);
+ ATF_REQUIRE(!(context1 != context2));
+ ATF_REQUIRE(!(context1 == context3));
+ ATF_REQUIRE( context1 != context3);
+ ATF_REQUIRE(!(context1 == context4));
+ ATF_REQUIRE( context1 != context4);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(output__empty_env);
+ATF_TEST_CASE_BODY(output__empty_env)
+{
+ const std::map< std::string, std::string > env;
+ const model::context context(fs::path("/foo/bar"), env);
+
+ std::ostringstream str;
+ str << context;
+ ATF_REQUIRE_EQ("context{cwd='/foo/bar', env=[]}", str.str());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(output__some_env);
+ATF_TEST_CASE_BODY(output__some_env)
+{
+ std::map< std::string, std::string > env;
+ env["foo"] = "first";
+ env["bar"] = "second' var";
+ const model::context context(fs::path("/foo/bar"), env);
+
+ std::ostringstream str;
+ str << context;
+ ATF_REQUIRE_EQ("context{cwd='/foo/bar', env=[bar='second\\' var', "
+ "foo='first']}", str.str());
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, ctor_and_getters);
+ ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne);
+ ATF_ADD_TEST_CASE(tcs, output__empty_env);
+ ATF_ADD_TEST_CASE(tcs, output__some_env);
+}
diff --git a/model/exceptions.cpp b/model/exceptions.cpp
new file mode 100644
index 000000000000..dc511a2b7e8f
--- /dev/null
+++ b/model/exceptions.cpp
@@ -0,0 +1,76 @@
+// 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/exceptions.hpp"
+
+#include "utils/format/macros.hpp"
+
+
+/// Constructs a new error with a plain-text message.
+///
+/// \param message The plain-text error message.
+model::error::error(const std::string& message) :
+ std::runtime_error(message)
+{
+}
+
+
+/// Destructor for the error.
+model::error::~error(void) throw()
+{
+}
+
+
+/// Constructs a new format_error.
+///
+/// \param message The plain-text error message.
+model::format_error::format_error(const std::string& message) :
+ error(message)
+{
+}
+
+
+/// Destructor for the error.
+model::format_error::~format_error(void) throw()
+{
+}
+
+
+/// Constructs a new not_found_error.
+///
+/// \param message The plain-text error message.
+model::not_found_error::not_found_error(const std::string& message) :
+ error(message)
+{
+}
+
+
+/// Destructor for the error.
+model::not_found_error::~not_found_error(void) throw()
+{
+}
diff --git a/model/exceptions.hpp b/model/exceptions.hpp
new file mode 100644
index 000000000000..ff4970fc37d7
--- /dev/null
+++ b/model/exceptions.hpp
@@ -0,0 +1,71 @@
+// 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.
+
+/// \file model/exceptions.hpp
+/// Exception types raised by the model module.
+///
+/// There is no model/exceptions_fwd.hpp counterpart because this file is
+/// inteded to be used only from within .cpp files to either raise or
+/// handle raised exceptions, neither of which are possible with just
+/// forward declarations.
+
+#if !defined(MODEL_EXCEPTIONS_HPP)
+#define MODEL_EXCEPTIONS_HPP
+
+#include <stdexcept>
+
+namespace model {
+
+
+/// Base exception for model errors.
+class error : public std::runtime_error {
+public:
+ explicit error(const std::string&);
+ virtual ~error(void) throw();
+};
+
+
+/// Error while parsing external data.
+class format_error : public error {
+public:
+ explicit format_error(const std::string&);
+ virtual ~format_error(void) throw();
+};
+
+
+/// A requested element could not be found.
+class not_found_error : public error {
+public:
+ explicit not_found_error(const std::string&);
+ virtual ~not_found_error(void) throw();
+};
+
+
+} // namespace model
+
+#endif // !defined(MODEL_EXCEPTIONS_HPP)
diff --git a/model/exceptions_test.cpp b/model/exceptions_test.cpp
new file mode 100644
index 000000000000..e9c17c0cc19a
--- /dev/null
+++ b/model/exceptions_test.cpp
@@ -0,0 +1,65 @@
+// 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/exceptions.hpp"
+
+#include <cstring>
+
+#include <atf-c++.hpp>
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(error);
+ATF_TEST_CASE_BODY(error)
+{
+ const model::error e("Some text");
+ ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(format_error);
+ATF_TEST_CASE_BODY(format_error)
+{
+ const model::format_error e("Some other text");
+ ATF_REQUIRE(std::strcmp("Some other text", e.what()) == 0);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(not_found_error);
+ATF_TEST_CASE_BODY(not_found_error)
+{
+ const model::not_found_error e("Missing foo");
+ ATF_REQUIRE(std::strcmp("Missing foo", e.what()) == 0);
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, error);
+ ATF_ADD_TEST_CASE(tcs, format_error);
+ ATF_ADD_TEST_CASE(tcs, not_found_error);
+}
diff --git a/model/metadata.cpp b/model/metadata.cpp
new file mode 100644
index 000000000000..d27e3237dcf2
--- /dev/null
+++ b/model/metadata.cpp
@@ -0,0 +1,1068 @@
+// Copyright 2012 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/metadata.hpp"
+
+#include <memory>
+
+#include "model/exceptions.hpp"
+#include "model/types.hpp"
+#include "utils/config/exceptions.hpp"
+#include "utils/config/nodes.ipp"
+#include "utils/config/tree.ipp"
+#include "utils/datetime.hpp"
+#include "utils/defs.hpp"
+#include "utils/format/macros.hpp"
+#include "utils/fs/exceptions.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/noncopyable.hpp"
+#include "utils/optional.ipp"
+#include "utils/sanity.hpp"
+#include "utils/text/exceptions.hpp"
+#include "utils/text/operations.hpp"
+#include "utils/units.hpp"
+
+namespace config = utils::config;
+namespace datetime = utils::datetime;
+namespace fs = utils::fs;
+namespace text = utils::text;
+namespace units = utils::units;
+
+using utils::optional;
+
+
+namespace {
+
+
+/// Global instance of defaults.
+///
+/// This exists so that the getters in metadata can return references instead
+/// of object copies. Use get_defaults() to query.
+static optional< config::tree > defaults;
+
+
+/// A leaf node that holds a bytes quantity.
+class bytes_node : public config::native_leaf_node< units::bytes > {
+public:
+ /// Copies the node.
+ ///
+ /// \return A dynamically-allocated node.
+ virtual base_node*
+ deep_copy(void) const
+ {
+ std::auto_ptr< bytes_node > new_node(new bytes_node());
+ new_node->_value = _value;
+ return new_node.release();
+ }
+
+ /// Pushes the node's value onto the Lua stack.
+ void
+ push_lua(lutok::state& /* state */) const
+ {
+ UNREACHABLE;
+ }
+
+ /// Sets the value of the node from an entry in the Lua stack.
+ void
+ set_lua(lutok::state& /* state */, const int /* index */)
+ {
+ UNREACHABLE;
+ }
+};
+
+
+/// A leaf node that holds a time delta.
+class delta_node : public config::typed_leaf_node< datetime::delta > {
+public:
+ /// Copies the node.
+ ///
+ /// \return A dynamically-allocated node.
+ virtual base_node*
+ deep_copy(void) const
+ {
+ std::auto_ptr< delta_node > new_node(new delta_node());
+ new_node->_value = _value;
+ return new_node.release();
+ }
+
+ /// Sets the value of the node from a raw string representation.
+ ///
+ /// \param raw_value The value to set the node to.
+ ///
+ /// \throw value_error If the value is invalid.
+ void
+ set_string(const std::string& raw_value)
+ {
+ unsigned int seconds;
+ try {
+ seconds = text::to_type< unsigned int >(raw_value);
+ } catch (const text::error& e) {
+ throw config::value_error(F("Invalid time delta %s") % raw_value);
+ }
+ set(datetime::delta(seconds, 0));
+ }
+
+ /// Converts the contents of the node to a string.
+ ///
+ /// \pre The node must have a value.
+ ///
+ /// \return A string representation of the value held by the node.
+ std::string
+ to_string(void) const
+ {
+ return F("%s") % value().seconds;
+ }
+
+ /// Pushes the node's value onto the Lua stack.
+ void
+ push_lua(lutok::state& /* state */) const
+ {
+ UNREACHABLE;
+ }
+
+ /// Sets the value of the node from an entry in the Lua stack.
+ void
+ set_lua(lutok::state& /* state */, const int /* index */)
+ {
+ UNREACHABLE;
+ }
+};
+
+
+/// A leaf node that holds a "required user" property.
+///
+/// This node is just a string, but it provides validation of the only allowed
+/// values.
+class user_node : public config::string_node {
+ /// Copies the node.
+ ///
+ /// \return A dynamically-allocated node.
+ virtual base_node*
+ deep_copy(void) const
+ {
+ std::auto_ptr< user_node > new_node(new user_node());
+ new_node->_value = _value;
+ return new_node.release();
+ }
+
+ /// Checks a given user textual representation for validity.
+ ///
+ /// \param user The value to validate.
+ ///
+ /// \throw config::value_error If the value is not valid.
+ void
+ validate(const value_type& user) const
+ {
+ if (!user.empty() && user != "root" && user != "unprivileged")
+ throw config::value_error("Invalid required user value");
+ }
+};
+
+
+/// A leaf node that holds a set of paths.
+///
+/// This node type is used to represent the value of the required files and
+/// required programs, for example, and these do not allow relative paths. We
+/// check this here.
+class paths_set_node : public config::base_set_node< fs::path > {
+ /// Copies the node.
+ ///
+ /// \return A dynamically-allocated node.
+ virtual base_node*
+ deep_copy(void) const
+ {
+ std::auto_ptr< paths_set_node > new_node(new paths_set_node());
+ new_node->_value = _value;
+ return new_node.release();
+ }
+
+ /// Converts a single path to the native type.
+ ///
+ /// \param raw_value The value to parse.
+ ///
+ /// \return The parsed value.
+ ///
+ /// \throw config::value_error If the value is invalid.
+ fs::path
+ parse_one(const std::string& raw_value) const
+ {
+ try {
+ return fs::path(raw_value);
+ } catch (const fs::error& e) {
+ throw config::value_error(e.what());
+ }
+ }
+
+ /// Checks a collection of paths for validity.
+ ///
+ /// \param paths The value to validate.
+ ///
+ /// \throw config::value_error If the value is not valid.
+ void
+ validate(const value_type& paths) const
+ {
+ for (value_type::const_iterator iter = paths.begin();
+ iter != paths.end(); ++iter) {
+ const fs::path& path = *iter;
+ if (!path.is_absolute() && path.ncomponents() > 1)
+ throw config::value_error(F("Relative path '%s' not allowed") %
+ *iter);
+ }
+ }
+};
+
+
+/// Initializes a tree to hold test case requirements.
+///
+/// \param [in,out] tree The tree to initialize.
+static void
+init_tree(config::tree& tree)
+{
+ tree.define< config::strings_set_node >("allowed_architectures");
+ tree.define< config::strings_set_node >("allowed_platforms");
+ tree.define_dynamic("custom");
+ tree.define< config::string_node >("description");
+ tree.define< config::bool_node >("has_cleanup");
+ tree.define< config::bool_node >("is_exclusive");
+ tree.define< config::strings_set_node >("required_configs");
+ tree.define< bytes_node >("required_disk_space");
+ tree.define< paths_set_node >("required_files");
+ tree.define< bytes_node >("required_memory");
+ tree.define< paths_set_node >("required_programs");
+ tree.define< user_node >("required_user");
+ tree.define< delta_node >("timeout");
+}
+
+
+/// Sets default values on a tree object.
+///
+/// \param [in,out] tree The tree to configure.
+static void
+set_defaults(config::tree& tree)
+{
+ tree.set< config::strings_set_node >("allowed_architectures",
+ model::strings_set());
+ tree.set< config::strings_set_node >("allowed_platforms",
+ model::strings_set());
+ tree.set< config::string_node >("description", "");
+ tree.set< config::bool_node >("has_cleanup", false);
+ tree.set< config::bool_node >("is_exclusive", false);
+ tree.set< config::strings_set_node >("required_configs",
+ model::strings_set());
+ tree.set< bytes_node >("required_disk_space", units::bytes(0));
+ tree.set< paths_set_node >("required_files", model::paths_set());
+ tree.set< bytes_node >("required_memory", units::bytes(0));
+ tree.set< paths_set_node >("required_programs", model::paths_set());
+ tree.set< user_node >("required_user", "");
+ // TODO(jmmv): We shouldn't be setting a default timeout like this. See
+ // Issue 5 for details.
+ tree.set< delta_node >("timeout", datetime::delta(300, 0));
+}
+
+
+/// Queries the global defaults tree object with lazy initialization.
+///
+/// \return A metadata tree. This object is statically allocated so it is
+/// acceptable to obtain references to it and its members.
+const config::tree&
+get_defaults(void)
+{
+ if (!defaults) {
+ config::tree props;
+ init_tree(props);
+ set_defaults(props);
+ defaults = props;
+ }
+ return defaults.get();
+}
+
+
+/// Looks up a value in a tree with error rewriting.
+///
+/// \tparam NodeType The type of the node.
+/// \param tree The tree in which to insert the value.
+/// \param key The key to set.
+///
+/// \return A read-write reference to the value in the node.
+///
+/// \throw model::error If the key is not known or if the value is not valid.
+template< class NodeType >
+typename NodeType::value_type&
+lookup_rw(config::tree& tree, const std::string& key)
+{
+ try {
+ return tree.lookup_rw< NodeType >(key);
+ } catch (const config::unknown_key_error& e) {
+ throw model::error(F("Unknown metadata property %s") % key);
+ } catch (const config::value_error& e) {
+ throw model::error(F("Invalid value for metadata property %s: %s") %
+ key % e.what());
+ }
+}
+
+
+/// Sets a value in a tree with error rewriting.
+///
+/// \tparam NodeType The type of the node.
+/// \param tree The tree in which to insert the value.
+/// \param key The key to set.
+/// \param value The value to set the node to.
+///
+/// \throw model::error If the key is not known or if the value is not valid.
+template< class NodeType >
+void
+set(config::tree& tree, const std::string& key,
+ const typename NodeType::value_type& value)
+{
+ try {
+ tree.set< NodeType >(key, value);
+ } catch (const config::unknown_key_error& e) {
+ throw model::error(F("Unknown metadata property %s") % key);
+ } catch (const config::value_error& e) {
+ throw model::error(F("Invalid value for metadata property %s: %s") %
+ key % e.what());
+ }
+}
+
+
+} // anonymous namespace
+
+
+/// Internal implementation of the metadata class.
+struct model::metadata::impl : utils::noncopyable {
+ /// Metadata properties.
+ config::tree props;
+
+ /// Constructor.
+ ///
+ /// \param props_ Metadata properties of the test.
+ impl(const utils::config::tree& props_) :
+ props(props_)
+ {
+ }
+
+ /// Equality comparator.
+ ///
+ /// \param other The other object to compare this one to.
+ ///
+ /// \return True if this object and other are equal; false otherwise.
+ bool
+ operator==(const impl& other) const
+ {
+ return (get_defaults().combine(props) ==
+ get_defaults().combine(other.props));
+ }
+};
+
+
+/// Constructor.
+///
+/// \param props Metadata properties of the test.
+model::metadata::metadata(const utils::config::tree& props) :
+ _pimpl(new impl(props))
+{
+}
+
+
+/// Destructor.
+model::metadata::~metadata(void)
+{
+}
+
+
+/// Applies a set of overrides to this metadata object.
+///
+/// \param overrides The overrides to apply. Any values explicitly set in this
+/// other object will override any possible values set in this object.
+///
+/// \return A new metadata object with the combination.
+model::metadata
+model::metadata::apply_overrides(const metadata& overrides) const
+{
+ return metadata(_pimpl->props.combine(overrides._pimpl->props));
+}
+
+
+/// Returns the architectures allowed by the test.
+///
+/// \return Set of architectures, or empty if this does not apply.
+const model::strings_set&
+model::metadata::allowed_architectures(void) const
+{
+ if (_pimpl->props.is_set("allowed_architectures")) {
+ return _pimpl->props.lookup< config::strings_set_node >(
+ "allowed_architectures");
+ } else {
+ return get_defaults().lookup< config::strings_set_node >(
+ "allowed_architectures");
+ }
+}
+
+
+/// Returns the platforms allowed by the test.
+///
+/// \return Set of platforms, or empty if this does not apply.
+const model::strings_set&
+model::metadata::allowed_platforms(void) const
+{
+ if (_pimpl->props.is_set("allowed_platforms")) {
+ return _pimpl->props.lookup< config::strings_set_node >(
+ "allowed_platforms");
+ } else {
+ return get_defaults().lookup< config::strings_set_node >(
+ "allowed_platforms");
+ }
+}
+
+
+/// Returns all the user-defined metadata properties.
+///
+/// \return A key/value map of properties.
+model::properties_map
+model::metadata::custom(void) const
+{
+ return _pimpl->props.all_properties("custom", true);
+}
+
+
+/// Returns the description of the test.
+///
+/// \return Textual description; may be empty.
+const std::string&
+model::metadata::description(void) const
+{
+ if (_pimpl->props.is_set("description")) {
+ return _pimpl->props.lookup< config::string_node >("description");
+ } else {
+ return get_defaults().lookup< config::string_node >("description");
+ }
+}
+
+
+/// Returns whether the test has a cleanup part or not.
+///
+/// \return True if there is a cleanup part; false otherwise.
+bool
+model::metadata::has_cleanup(void) const
+{
+ if (_pimpl->props.is_set("has_cleanup")) {
+ return _pimpl->props.lookup< config::bool_node >("has_cleanup");
+ } else {
+ return get_defaults().lookup< config::bool_node >("has_cleanup");
+ }
+}
+
+
+/// Returns whether the test is exclusive or not.
+///
+/// \return True if the test has to be run on its own, not concurrently with any
+/// other tests; false otherwise.
+bool
+model::metadata::is_exclusive(void) const
+{
+ if (_pimpl->props.is_set("is_exclusive")) {
+ return _pimpl->props.lookup< config::bool_node >("is_exclusive");
+ } else {
+ return get_defaults().lookup< config::bool_node >("is_exclusive");
+ }
+}
+
+
+/// Returns the list of configuration variables needed by the test.
+///
+/// \return Set of configuration variables.
+const model::strings_set&
+model::metadata::required_configs(void) const
+{
+ if (_pimpl->props.is_set("required_configs")) {
+ return _pimpl->props.lookup< config::strings_set_node >(
+ "required_configs");
+ } else {
+ return get_defaults().lookup< config::strings_set_node >(
+ "required_configs");
+ }
+}
+
+
+/// Returns the amount of free disk space required by the test.
+///
+/// \return Number of bytes, or 0 if this does not apply.
+const units::bytes&
+model::metadata::required_disk_space(void) const
+{
+ if (_pimpl->props.is_set("required_disk_space")) {
+ return _pimpl->props.lookup< bytes_node >("required_disk_space");
+ } else {
+ return get_defaults().lookup< bytes_node >("required_disk_space");
+ }
+}
+
+
+/// Returns the list of files needed by the test.
+///
+/// \return Set of paths.
+const model::paths_set&
+model::metadata::required_files(void) const
+{
+ if (_pimpl->props.is_set("required_files")) {
+ return _pimpl->props.lookup< paths_set_node >("required_files");
+ } else {
+ return get_defaults().lookup< paths_set_node >("required_files");
+ }
+}
+
+
+/// Returns the amount of memory required by the test.
+///
+/// \return Number of bytes, or 0 if this does not apply.
+const units::bytes&
+model::metadata::required_memory(void) const
+{
+ if (_pimpl->props.is_set("required_memory")) {
+ return _pimpl->props.lookup< bytes_node >("required_memory");
+ } else {
+ return get_defaults().lookup< bytes_node >("required_memory");
+ }
+}
+
+
+/// Returns the list of programs needed by the test.
+///
+/// \return Set of paths.
+const model::paths_set&
+model::metadata::required_programs(void) const
+{
+ if (_pimpl->props.is_set("required_programs")) {
+ return _pimpl->props.lookup< paths_set_node >("required_programs");
+ } else {
+ return get_defaults().lookup< paths_set_node >("required_programs");
+ }
+}
+
+
+/// Returns the user required by the test.
+///
+/// \return One of unprivileged, root or empty.
+const std::string&
+model::metadata::required_user(void) const
+{
+ if (_pimpl->props.is_set("required_user")) {
+ return _pimpl->props.lookup< user_node >("required_user");
+ } else {
+ return get_defaults().lookup< user_node >("required_user");
+ }
+}
+
+
+/// Returns the timeout of the test.
+///
+/// \return A time delta; should be compared to default_timeout to see if it has
+/// been overriden.
+const datetime::delta&
+model::metadata::timeout(void) const
+{
+ if (_pimpl->props.is_set("timeout")) {
+ return _pimpl->props.lookup< delta_node >("timeout");
+ } else {
+ return get_defaults().lookup< delta_node >("timeout");
+ }
+}
+
+
+/// Externalizes the metadata to a set of key/value textual pairs.
+///
+/// \return A key/value representation of the metadata.
+model::properties_map
+model::metadata::to_properties(void) const
+{
+ const config::tree fully_specified = get_defaults().combine(_pimpl->props);
+ return fully_specified.all_properties();
+}
+
+
+/// 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::metadata::operator==(const metadata& other) const
+{
+ return _pimpl == other._pimpl || *_pimpl == *other._pimpl;
+}
+
+
+/// 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::metadata::operator!=(const metadata& other) const
+{
+ return !(*this == other);
+}
+
+
+/// 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 metadata& object)
+{
+ output << "metadata{";
+
+ bool first = true;
+ const model::properties_map props = object.to_properties();
+ for (model::properties_map::const_iterator iter = props.begin();
+ iter != props.end(); ++iter) {
+ if (!first)
+ output << ", ";
+ output << F("%s=%s") % (*iter).first %
+ text::quote((*iter).second, '\'');
+ first = false;
+ }
+
+ output << "}";
+ return output;
+}
+
+
+/// Internal implementation of the metadata_builder class.
+struct model::metadata_builder::impl : utils::noncopyable {
+ /// Collection of requirements.
+ config::tree props;
+
+ /// Whether we have created a metadata object or not.
+ bool built;
+
+ /// Constructor.
+ impl(void) :
+ built(false)
+ {
+ init_tree(props);
+ }
+
+ /// Constructor.
+ ///
+ /// \param base The base model to construct a copy from.
+ impl(const model::metadata& base) :
+ props(base._pimpl->props.deep_copy()),
+ built(false)
+ {
+ }
+};
+
+
+/// Constructor.
+model::metadata_builder::metadata_builder(void) :
+ _pimpl(new impl())
+{
+}
+
+
+/// Constructor.
+model::metadata_builder::metadata_builder(const model::metadata& base) :
+ _pimpl(new impl(base))
+{
+}
+
+
+/// Destructor.
+model::metadata_builder::~metadata_builder(void)
+{
+}
+
+
+/// Accumulates an additional allowed architecture.
+///
+/// \param arch The architecture.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::add_allowed_architecture(const std::string& arch)
+{
+ if (!_pimpl->props.is_set("allowed_architectures")) {
+ _pimpl->props.set< config::strings_set_node >(
+ "allowed_architectures",
+ get_defaults().lookup< config::strings_set_node >(
+ "allowed_architectures"));
+ }
+ lookup_rw< config::strings_set_node >(
+ _pimpl->props, "allowed_architectures").insert(arch);
+ return *this;
+}
+
+
+/// Accumulates an additional allowed platform.
+///
+/// \param platform The platform.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::add_allowed_platform(const std::string& platform)
+{
+ if (!_pimpl->props.is_set("allowed_platforms")) {
+ _pimpl->props.set< config::strings_set_node >(
+ "allowed_platforms",
+ get_defaults().lookup< config::strings_set_node >(
+ "allowed_platforms"));
+ }
+ lookup_rw< config::strings_set_node >(
+ _pimpl->props, "allowed_platforms").insert(platform);
+ return *this;
+}
+
+
+/// Accumulates a single user-defined property.
+///
+/// \param key Name of the property to define.
+/// \param value Value of the property.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::add_custom(const std::string& key,
+ const std::string& value)
+{
+ _pimpl->props.set_string(F("custom.%s") % key, value);
+ return *this;
+}
+
+
+/// Accumulates an additional required configuration variable.
+///
+/// \param var The name of the configuration variable.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::add_required_config(const std::string& var)
+{
+ if (!_pimpl->props.is_set("required_configs")) {
+ _pimpl->props.set< config::strings_set_node >(
+ "required_configs",
+ get_defaults().lookup< config::strings_set_node >(
+ "required_configs"));
+ }
+ lookup_rw< config::strings_set_node >(
+ _pimpl->props, "required_configs").insert(var);
+ return *this;
+}
+
+
+/// Accumulates an additional required file.
+///
+/// \param path The path to the file.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::add_required_file(const fs::path& path)
+{
+ if (!_pimpl->props.is_set("required_files")) {
+ _pimpl->props.set< paths_set_node >(
+ "required_files",
+ get_defaults().lookup< paths_set_node >("required_files"));
+ }
+ lookup_rw< paths_set_node >(_pimpl->props, "required_files").insert(path);
+ return *this;
+}
+
+
+/// Accumulates an additional required program.
+///
+/// \param path The path to the program.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::add_required_program(const fs::path& path)
+{
+ if (!_pimpl->props.is_set("required_programs")) {
+ _pimpl->props.set< paths_set_node >(
+ "required_programs",
+ get_defaults().lookup< paths_set_node >("required_programs"));
+ }
+ lookup_rw< paths_set_node >(_pimpl->props,
+ "required_programs").insert(path);
+ return *this;
+}
+
+
+/// Sets the architectures allowed by the test.
+///
+/// \param as Set of architectures.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::set_allowed_architectures(
+ const model::strings_set& as)
+{
+ set< config::strings_set_node >(_pimpl->props, "allowed_architectures", as);
+ return *this;
+}
+
+
+/// Sets the platforms allowed by the test.
+///
+/// \return ps Set of platforms.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::set_allowed_platforms(const model::strings_set& ps)
+{
+ set< config::strings_set_node >(_pimpl->props, "allowed_platforms", ps);
+ return *this;
+}
+
+
+/// Sets the user-defined properties.
+///
+/// \param props The custom properties to set.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::set_custom(const model::properties_map& props)
+{
+ for (model::properties_map::const_iterator iter = props.begin();
+ iter != props.end(); ++iter)
+ _pimpl->props.set_string(F("custom.%s") % (*iter).first,
+ (*iter).second);
+ return *this;
+}
+
+
+/// Sets the description of the test.
+///
+/// \param description Textual description of the test.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::set_description(const std::string& description)
+{
+ set< config::string_node >(_pimpl->props, "description", description);
+ return *this;
+}
+
+
+/// Sets whether the test has a cleanup part or not.
+///
+/// \param cleanup True if the test has a cleanup part; false otherwise.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::set_has_cleanup(const bool cleanup)
+{
+ set< config::bool_node >(_pimpl->props, "has_cleanup", cleanup);
+ return *this;
+}
+
+
+/// Sets whether the test is exclusive or not.
+///
+/// \param exclusive True if the test is exclusive; false otherwise.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::set_is_exclusive(const bool exclusive)
+{
+ set< config::bool_node >(_pimpl->props, "is_exclusive", exclusive);
+ return *this;
+}
+
+
+/// Sets the list of configuration variables needed by the test.
+///
+/// \param vars Set of configuration variables.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::set_required_configs(const model::strings_set& vars)
+{
+ set< config::strings_set_node >(_pimpl->props, "required_configs", vars);
+ return *this;
+}
+
+
+/// Sets the amount of free disk space required by the test.
+///
+/// \param bytes Number of bytes.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::set_required_disk_space(const units::bytes& bytes)
+{
+ set< bytes_node >(_pimpl->props, "required_disk_space", bytes);
+ return *this;
+}
+
+
+/// Sets the list of files needed by the test.
+///
+/// \param files Set of paths.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::set_required_files(const model::paths_set& files)
+{
+ set< paths_set_node >(_pimpl->props, "required_files", files);
+ return *this;
+}
+
+
+/// Sets the amount of memory required by the test.
+///
+/// \param bytes Number of bytes.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::set_required_memory(const units::bytes& bytes)
+{
+ set< bytes_node >(_pimpl->props, "required_memory", bytes);
+ return *this;
+}
+
+
+/// Sets the list of programs needed by the test.
+///
+/// \param progs Set of paths.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::set_required_programs(const model::paths_set& progs)
+{
+ set< paths_set_node >(_pimpl->props, "required_programs", progs);
+ return *this;
+}
+
+
+/// Sets the user required by the test.
+///
+/// \param user One of unprivileged, root or empty.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::set_required_user(const std::string& user)
+{
+ set< user_node >(_pimpl->props, "required_user", user);
+ return *this;
+}
+
+
+/// Sets a metadata property by name from its textual representation.
+///
+/// \param key The property to set.
+/// \param value The value to set the property to.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid or the key does not exist.
+model::metadata_builder&
+model::metadata_builder::set_string(const std::string& key,
+ const std::string& value)
+{
+ try {
+ _pimpl->props.set_string(key, value);
+ } catch (const config::unknown_key_error& e) {
+ throw model::format_error(F("Unknown metadata property %s") % key);
+ } catch (const config::value_error& e) {
+ throw model::format_error(
+ F("Invalid value for metadata property %s: %s") % key % e.what());
+ }
+ return *this;
+}
+
+
+/// Sets the timeout of the test.
+///
+/// \param timeout The timeout to set.
+///
+/// \return A reference to this builder.
+///
+/// \throw model::error If the value is invalid.
+model::metadata_builder&
+model::metadata_builder::set_timeout(const datetime::delta& timeout)
+{
+ set< delta_node >(_pimpl->props, "timeout", timeout);
+ return *this;
+}
+
+
+/// Creates a new metadata object.
+///
+/// \pre This has not yet been called. We only support calling this function
+/// once due to the way the internal tree works: we pass around references, not
+/// deep copies, so if we allowed a second build, we'd encourage reusing the
+/// same builder to construct different metadata objects, and this could have
+/// unintended consequences.
+///
+/// \return The constructed metadata object.
+model::metadata
+model::metadata_builder::build(void) const
+{
+ PRE(!_pimpl->built);
+ _pimpl->built = true;
+
+ return metadata(_pimpl->props);
+}
diff --git a/model/metadata.hpp b/model/metadata.hpp
new file mode 100644
index 000000000000..c7dd4519f122
--- /dev/null
+++ b/model/metadata.hpp
@@ -0,0 +1,130 @@
+// Copyright 2012 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.
+
+/// \file model/metadata.hpp
+/// Definition of the "test metadata" concept.
+
+#if !defined(MODEL_METADATA_HPP)
+#define MODEL_METADATA_HPP
+
+#include "model/metadata_fwd.hpp"
+
+#include <memory>
+#include <ostream>
+#include <string>
+
+#include "model/types.hpp"
+#include "utils/config/tree_fwd.hpp"
+#include "utils/datetime_fwd.hpp"
+#include "utils/fs/path_fwd.hpp"
+#include "utils/noncopyable.hpp"
+#include "utils/units_fwd.hpp"
+
+namespace model {
+
+
+/// Collection of metadata properties of a test.
+class metadata {
+ struct impl;
+
+ /// Pointer to the shared internal implementation.
+ std::shared_ptr< impl > _pimpl;
+
+ friend class metadata_builder;
+
+public:
+ metadata(const utils::config::tree&);
+ ~metadata(void);
+
+ metadata apply_overrides(const metadata&) const;
+
+ const strings_set& allowed_architectures(void) const;
+ const strings_set& allowed_platforms(void) const;
+ model::properties_map custom(void) const;
+ const std::string& description(void) const;
+ bool has_cleanup(void) const;
+ bool is_exclusive(void) const;
+ const strings_set& required_configs(void) const;
+ const utils::units::bytes& required_disk_space(void) const;
+ const paths_set& required_files(void) const;
+ const utils::units::bytes& required_memory(void) const;
+ const paths_set& required_programs(void) const;
+ const std::string& required_user(void) const;
+ const utils::datetime::delta& timeout(void) const;
+
+ model::properties_map to_properties(void) const;
+
+ bool operator==(const metadata&) const;
+ bool operator!=(const metadata&) const;
+};
+
+
+std::ostream& operator<<(std::ostream&, const metadata&);
+
+
+/// Builder for a metadata object.
+class metadata_builder : utils::noncopyable {
+ struct impl;
+
+ /// Pointer to the shared internal implementation.
+ std::auto_ptr< impl > _pimpl;
+
+public:
+ metadata_builder(void);
+ explicit metadata_builder(const metadata&);
+ ~metadata_builder(void);
+
+ metadata_builder& add_allowed_architecture(const std::string&);
+ metadata_builder& add_allowed_platform(const std::string&);
+ metadata_builder& add_custom(const std::string&, const std::string&);
+ metadata_builder& add_required_config(const std::string&);
+ metadata_builder& add_required_file(const utils::fs::path&);
+ metadata_builder& add_required_program(const utils::fs::path&);
+
+ metadata_builder& set_allowed_architectures(const strings_set&);
+ metadata_builder& set_allowed_platforms(const strings_set&);
+ metadata_builder& set_custom(const model::properties_map&);
+ metadata_builder& set_description(const std::string&);
+ metadata_builder& set_has_cleanup(const bool);
+ metadata_builder& set_is_exclusive(const bool);
+ metadata_builder& set_required_configs(const strings_set&);
+ metadata_builder& set_required_disk_space(const utils::units::bytes&);
+ metadata_builder& set_required_files(const paths_set&);
+ metadata_builder& set_required_memory(const utils::units::bytes&);
+ metadata_builder& set_required_programs(const paths_set&);
+ metadata_builder& set_required_user(const std::string&);
+ metadata_builder& set_string(const std::string&, const std::string&);
+ metadata_builder& set_timeout(const utils::datetime::delta&);
+
+ metadata build(void) const;
+};
+
+
+} // namespace model
+
+#endif // !defined(MODEL_METADATA_HPP)
diff --git a/model/metadata_fwd.hpp b/model/metadata_fwd.hpp
new file mode 100644
index 000000000000..8a8c5c09d77b
--- /dev/null
+++ b/model/metadata_fwd.hpp
@@ -0,0 +1,44 @@
+// Copyright 2014 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.
+
+/// \file model/metadata_fwd.hpp
+/// Forward declarations for model/metadata.hpp
+
+#if !defined(MODEL_METADATA_FWD_HPP)
+#define MODEL_METADATA_FWD_HPP
+
+namespace model {
+
+
+class metadata;
+class metadata_builder;
+
+
+} // namespace model
+
+#endif // !defined(MODEL_METADATA_FWD_HPP)
diff --git a/model/metadata_test.cpp b/model/metadata_test.cpp
new file mode 100644
index 000000000000..7b22653ec1a2
--- /dev/null
+++ b/model/metadata_test.cpp
@@ -0,0 +1,461 @@
+// Copyright 2012 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/metadata.hpp"
+
+#include <sstream>
+
+#include <atf-c++.hpp>
+
+#include "model/types.hpp"
+#include "utils/datetime.hpp"
+#include "utils/format/containers.ipp"
+#include "utils/fs/path.hpp"
+#include "utils/units.hpp"
+
+namespace datetime = utils::datetime;
+namespace fs = utils::fs;
+namespace units = utils::units;
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(defaults);
+ATF_TEST_CASE_BODY(defaults)
+{
+ const model::metadata md = model::metadata_builder().build();
+ ATF_REQUIRE(md.allowed_architectures().empty());
+ ATF_REQUIRE(md.allowed_platforms().empty());
+ ATF_REQUIRE(md.allowed_platforms().empty());
+ ATF_REQUIRE(md.custom().empty());
+ ATF_REQUIRE(md.description().empty());
+ ATF_REQUIRE(!md.has_cleanup());
+ ATF_REQUIRE(!md.is_exclusive());
+ ATF_REQUIRE(md.required_configs().empty());
+ ATF_REQUIRE_EQ(units::bytes(0), md.required_disk_space());
+ ATF_REQUIRE(md.required_files().empty());
+ ATF_REQUIRE_EQ(units::bytes(0), md.required_memory());
+ ATF_REQUIRE(md.required_programs().empty());
+ ATF_REQUIRE(md.required_user().empty());
+ ATF_REQUIRE(datetime::delta(300, 0) == md.timeout());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(add);
+ATF_TEST_CASE_BODY(add)
+{
+ model::strings_set architectures;
+ architectures.insert("1-architecture");
+ architectures.insert("2-architecture");
+
+ model::strings_set platforms;
+ platforms.insert("1-platform");
+ platforms.insert("2-platform");
+
+ model::properties_map custom;
+ custom["1-custom"] = "first";
+ custom["2-custom"] = "second";
+
+ model::strings_set configs;
+ configs.insert("1-config");
+ configs.insert("2-config");
+
+ model::paths_set files;
+ files.insert(fs::path("1-file"));
+ files.insert(fs::path("2-file"));
+
+ model::paths_set programs;
+ programs.insert(fs::path("1-program"));
+ programs.insert(fs::path("2-program"));
+
+ const model::metadata md = model::metadata_builder()
+ .add_allowed_architecture("1-architecture")
+ .add_allowed_platform("1-platform")
+ .add_custom("1-custom", "first")
+ .add_custom("2-custom", "second")
+ .add_required_config("1-config")
+ .add_required_file(fs::path("1-file"))
+ .add_required_program(fs::path("1-program"))
+ .add_allowed_architecture("2-architecture")
+ .add_allowed_platform("2-platform")
+ .add_required_config("2-config")
+ .add_required_file(fs::path("2-file"))
+ .add_required_program(fs::path("2-program"))
+ .build();
+
+ ATF_REQUIRE(architectures == md.allowed_architectures());
+ ATF_REQUIRE(platforms == md.allowed_platforms());
+ ATF_REQUIRE(custom == md.custom());
+ ATF_REQUIRE(configs == md.required_configs());
+ ATF_REQUIRE(files == md.required_files());
+ ATF_REQUIRE(programs == md.required_programs());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(copy);
+ATF_TEST_CASE_BODY(copy)
+{
+ const model::metadata md1 = model::metadata_builder()
+ .add_allowed_architecture("1-architecture")
+ .add_allowed_platform("1-platform")
+ .build();
+
+ const model::metadata md2 = model::metadata_builder(md1)
+ .add_allowed_architecture("2-architecture")
+ .build();
+
+ ATF_REQUIRE_EQ(1, md1.allowed_architectures().size());
+ ATF_REQUIRE_EQ(2, md2.allowed_architectures().size());
+ ATF_REQUIRE_EQ(1, md1.allowed_platforms().size());
+ ATF_REQUIRE_EQ(1, md2.allowed_platforms().size());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(apply_overrides);
+ATF_TEST_CASE_BODY(apply_overrides)
+{
+ const model::metadata md1 = model::metadata_builder()
+ .add_allowed_architecture("1-architecture")
+ .add_allowed_platform("1-platform")
+ .set_description("Explicit description")
+ .build();
+
+ const model::metadata md2 = model::metadata_builder()
+ .add_allowed_architecture("2-architecture")
+ .set_description("")
+ .set_timeout(datetime::delta(500, 0))
+ .build();
+
+ const model::metadata merge_1_2 = model::metadata_builder()
+ .add_allowed_architecture("2-architecture")
+ .add_allowed_platform("1-platform")
+ .set_description("")
+ .set_timeout(datetime::delta(500, 0))
+ .build();
+ ATF_REQUIRE_EQ(merge_1_2, md1.apply_overrides(md2));
+
+ const model::metadata merge_2_1 = model::metadata_builder()
+ .add_allowed_architecture("1-architecture")
+ .add_allowed_platform("1-platform")
+ .set_description("Explicit description")
+ .set_timeout(datetime::delta(500, 0))
+ .build();
+ ATF_REQUIRE_EQ(merge_2_1, md2.apply_overrides(md1));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(override_all_with_setters);
+ATF_TEST_CASE_BODY(override_all_with_setters)
+{
+ model::strings_set architectures;
+ architectures.insert("the-architecture");
+
+ model::strings_set platforms;
+ platforms.insert("the-platforms");
+
+ model::properties_map custom;
+ custom["first"] = "hello";
+ custom["second"] = "bye";
+
+ const std::string description = "Some long text";
+
+ model::strings_set configs;
+ configs.insert("the-configs");
+
+ model::paths_set files;
+ files.insert(fs::path("the-files"));
+
+ const units::bytes disk_space(6789);
+
+ const units::bytes memory(12345);
+
+ model::paths_set programs;
+ programs.insert(fs::path("the-programs"));
+
+ const std::string user = "root";
+
+ const datetime::delta timeout(123, 0);
+
+ const model::metadata md = model::metadata_builder()
+ .set_allowed_architectures(architectures)
+ .set_allowed_platforms(platforms)
+ .set_custom(custom)
+ .set_description(description)
+ .set_has_cleanup(true)
+ .set_is_exclusive(true)
+ .set_required_configs(configs)
+ .set_required_disk_space(disk_space)
+ .set_required_files(files)
+ .set_required_memory(memory)
+ .set_required_programs(programs)
+ .set_required_user(user)
+ .set_timeout(timeout)
+ .build();
+
+ ATF_REQUIRE(architectures == md.allowed_architectures());
+ ATF_REQUIRE(platforms == md.allowed_platforms());
+ ATF_REQUIRE(custom == md.custom());
+ ATF_REQUIRE_EQ(description, md.description());
+ ATF_REQUIRE(md.has_cleanup());
+ ATF_REQUIRE(md.is_exclusive());
+ ATF_REQUIRE(configs == md.required_configs());
+ ATF_REQUIRE_EQ(disk_space, md.required_disk_space());
+ ATF_REQUIRE(files == md.required_files());
+ ATF_REQUIRE_EQ(memory, md.required_memory());
+ ATF_REQUIRE(programs == md.required_programs());
+ ATF_REQUIRE_EQ(user, md.required_user());
+ ATF_REQUIRE(timeout == md.timeout());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(override_all_with_set_string);
+ATF_TEST_CASE_BODY(override_all_with_set_string)
+{
+ model::strings_set architectures;
+ architectures.insert("a1");
+ architectures.insert("a2");
+
+ model::strings_set platforms;
+ platforms.insert("p1");
+ platforms.insert("p2");
+
+ model::properties_map custom;
+ custom["user-defined"] = "the-value";
+
+ const std::string description = "Another long text";
+
+ model::strings_set configs;
+ configs.insert("config-var");
+
+ model::paths_set files;
+ files.insert(fs::path("plain"));
+ files.insert(fs::path("/absolute/path"));
+
+ const units::bytes disk_space(
+ static_cast< uint64_t >(16) * 1024 * 1024 * 1024);
+
+ const units::bytes memory(1024 * 1024);
+
+ model::paths_set programs;
+ programs.insert(fs::path("program"));
+ programs.insert(fs::path("/absolute/prog"));
+
+ const std::string user = "unprivileged";
+
+ const datetime::delta timeout(45, 0);
+
+ const model::metadata md = model::metadata_builder()
+ .set_string("allowed_architectures", "a1 a2")
+ .set_string("allowed_platforms", "p1 p2")
+ .set_string("custom.user-defined", "the-value")
+ .set_string("description", "Another long text")
+ .set_string("has_cleanup", "true")
+ .set_string("is_exclusive", "true")
+ .set_string("required_configs", "config-var")
+ .set_string("required_disk_space", "16G")
+ .set_string("required_files", "plain /absolute/path")
+ .set_string("required_memory", "1M")
+ .set_string("required_programs", "program /absolute/prog")
+ .set_string("required_user", "unprivileged")
+ .set_string("timeout", "45")
+ .build();
+
+ ATF_REQUIRE(architectures == md.allowed_architectures());
+ ATF_REQUIRE(platforms == md.allowed_platforms());
+ ATF_REQUIRE(custom == md.custom());
+ ATF_REQUIRE_EQ(description, md.description());
+ ATF_REQUIRE(md.has_cleanup());
+ ATF_REQUIRE(md.is_exclusive());
+ ATF_REQUIRE(configs == md.required_configs());
+ ATF_REQUIRE_EQ(disk_space, md.required_disk_space());
+ ATF_REQUIRE(files == md.required_files());
+ ATF_REQUIRE_EQ(memory, md.required_memory());
+ ATF_REQUIRE(programs == md.required_programs());
+ ATF_REQUIRE_EQ(user, md.required_user());
+ ATF_REQUIRE(timeout == md.timeout());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(to_properties);
+ATF_TEST_CASE_BODY(to_properties)
+{
+ const model::metadata md = model::metadata_builder()
+ .add_allowed_architecture("abc")
+ .add_required_file(fs::path("foo"))
+ .add_required_file(fs::path("bar"))
+ .set_required_memory(units::bytes(1024))
+ .add_custom("foo", "bar")
+ .build();
+
+ model::properties_map props;
+ props["allowed_architectures"] = "abc";
+ props["allowed_platforms"] = "";
+ props["custom.foo"] = "bar";
+ props["description"] = "";
+ props["has_cleanup"] = "false";
+ props["is_exclusive"] = "false";
+ props["required_configs"] = "";
+ props["required_disk_space"] = "0";
+ props["required_files"] = "bar foo";
+ props["required_memory"] = "1.00K";
+ props["required_programs"] = "";
+ props["required_user"] = "";
+ props["timeout"] = "300";
+ ATF_REQUIRE_EQ(props, md.to_properties());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__empty);
+ATF_TEST_CASE_BODY(operators_eq_and_ne__empty)
+{
+ const model::metadata md1 = model::metadata_builder().build();
+ const model::metadata md2 = model::metadata_builder().build();
+ ATF_REQUIRE( md1 == md2);
+ ATF_REQUIRE(!(md1 != md2));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__copy);
+ATF_TEST_CASE_BODY(operators_eq_and_ne__copy)
+{
+ const model::metadata md1 = model::metadata_builder()
+ .add_custom("foo", "bar")
+ .build();
+ const model::metadata md2 = md1;
+ ATF_REQUIRE( md1 == md2);
+ ATF_REQUIRE(!(md1 != md2));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__equal);
+ATF_TEST_CASE_BODY(operators_eq_and_ne__equal)
+{
+ const model::metadata md1 = model::metadata_builder()
+ .add_allowed_architecture("a")
+ .add_allowed_architecture("b")
+ .add_custom("foo", "bar")
+ .build();
+ const model::metadata md2 = model::metadata_builder()
+ .add_allowed_architecture("b")
+ .add_allowed_architecture("a")
+ .add_custom("foo", "bar")
+ .build();
+ ATF_REQUIRE( md1 == md2);
+ ATF_REQUIRE(!(md1 != md2));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__equal_overriden_defaults);
+ATF_TEST_CASE_BODY(operators_eq_and_ne__equal_overriden_defaults)
+{
+ const model::metadata defaults = model::metadata_builder().build();
+
+ const model::metadata md1 = model::metadata_builder()
+ .add_allowed_architecture("a")
+ .build();
+ const model::metadata md2 = model::metadata_builder()
+ .add_allowed_architecture("a")
+ .set_timeout(defaults.timeout())
+ .build();
+ ATF_REQUIRE( md1 == md2);
+ ATF_REQUIRE(!(md1 != md2));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__different);
+ATF_TEST_CASE_BODY(operators_eq_and_ne__different)
+{
+ const model::metadata md1 = model::metadata_builder()
+ .add_custom("foo", "bar")
+ .build();
+ const model::metadata md2 = model::metadata_builder()
+ .add_custom("foo", "bar")
+ .add_custom("baz", "foo bar")
+ .build();
+ ATF_REQUIRE(!(md1 == md2));
+ ATF_REQUIRE( md1 != md2);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(output__defaults);
+ATF_TEST_CASE_BODY(output__defaults)
+{
+ std::ostringstream str;
+ str << model::metadata_builder().build();
+ ATF_REQUIRE_EQ("metadata{allowed_architectures='', allowed_platforms='', "
+ "description='', has_cleanup='false', is_exclusive='false', "
+ "required_configs='', "
+ "required_disk_space='0', required_files='', "
+ "required_memory='0', "
+ "required_programs='', required_user='', timeout='300'}",
+ str.str());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(output__some_values);
+ATF_TEST_CASE_BODY(output__some_values)
+{
+ std::ostringstream str;
+ str << model::metadata_builder()
+ .add_allowed_architecture("abc")
+ .add_required_file(fs::path("foo"))
+ .add_required_file(fs::path("bar"))
+ .set_is_exclusive(true)
+ .set_required_memory(units::bytes(1024))
+ .build();
+ ATF_REQUIRE_EQ(
+ "metadata{allowed_architectures='abc', allowed_platforms='', "
+ "description='', has_cleanup='false', is_exclusive='true', "
+ "required_configs='', "
+ "required_disk_space='0', required_files='bar foo', "
+ "required_memory='1.00K', "
+ "required_programs='', required_user='', timeout='300'}",
+ str.str());
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, defaults);
+ ATF_ADD_TEST_CASE(tcs, add);
+ ATF_ADD_TEST_CASE(tcs, copy);
+ ATF_ADD_TEST_CASE(tcs, apply_overrides);
+ ATF_ADD_TEST_CASE(tcs, override_all_with_setters);
+ ATF_ADD_TEST_CASE(tcs, override_all_with_set_string);
+ ATF_ADD_TEST_CASE(tcs, to_properties);
+
+ ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__empty);
+ ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__copy);
+ ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__equal);
+ ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__equal_overriden_defaults);
+ ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__different);
+
+ ATF_ADD_TEST_CASE(tcs, output__defaults);
+ ATF_ADD_TEST_CASE(tcs, output__some_values);
+
+ // TODO(jmmv): Add tests for error conditions (invalid keys and invalid
+ // values).
+}
diff --git a/model/test_case.cpp b/model/test_case.cpp
new file mode 100644
index 000000000000..f5f6a979eed3
--- /dev/null
+++ b/model/test_case.cpp
@@ -0,0 +1,339 @@
+// 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_case.hpp"
+
+#include "model/metadata.hpp"
+#include "model/test_result.hpp"
+#include "utils/format/macros.hpp"
+#include "utils/noncopyable.hpp"
+#include "utils/optional.ipp"
+#include "utils/text/operations.ipp"
+
+namespace text = utils::text;
+
+using utils::none;
+using utils::optional;
+
+
+/// Internal implementation for a test_case.
+struct model::test_case::impl : utils::noncopyable {
+ /// Name of the test case; must be unique within the test program.
+ std::string name;
+
+ /// Metadata of the container test program.
+ ///
+ /// Yes, this is a pointer. Yes, we do not own the object pointed to.
+ /// However, because this is only intended to point to the metadata object
+ /// of test programs _containing_ this test case, we can assume that the
+ /// referenced object will be alive for the lifetime of this test case.
+ const model::metadata* md_defaults;
+
+ /// Test case metadata.
+ model::metadata md;
+
+ /// Fake result to return instead of running the test case.
+ optional< model::test_result > fake_result;
+
+ /// Constructor.
+ ///
+ /// \param name_ The name of the test case within the test program.
+ /// \param md_defaults_ Metadata of the container test program.
+ /// \param md_ Metadata of the test case.
+ /// \param fake_result_ Fake result to return instead of running the test
+ /// case.
+ impl(const std::string& name_,
+ const model::metadata* md_defaults_,
+ const model::metadata& md_,
+ const optional< model::test_result >& fake_result_) :
+ name(name_),
+ md_defaults(md_defaults_),
+ md(md_),
+ fake_result(fake_result_)
+ {
+ }
+
+ /// Gets the test case metadata.
+ ///
+ /// This combines the test case's metadata with any possible test program
+ /// metadata, using the latter as defaults.
+ ///
+ /// \return The test case metadata.
+ model::metadata
+ get_metadata(void) const
+ {
+ if (md_defaults != NULL) {
+ return md_defaults->apply_overrides(md);
+ } else {
+ return md;
+ }
+ }
+
+ /// Equality comparator.
+ ///
+ /// \param other The other object to compare this one to.
+ ///
+ /// \return True if this object and other are equal; false otherwise.
+ bool
+ operator==(const impl& other) const
+ {
+ return (name == other.name &&
+ get_metadata() == other.get_metadata() &&
+ fake_result == other.fake_result);
+ }
+};
+
+
+/// Constructs a new test case from an already-built impl oject.
+///
+/// \param pimpl_ The internal representation of the test case.
+model::test_case::test_case(std::shared_ptr< impl > pimpl_) :
+ _pimpl(pimpl_)
+{
+}
+
+
+/// Constructs a new test case.
+///
+/// \param name_ The name of the test case within the test program.
+/// \param md_ Metadata of the test case.
+model::test_case::test_case(const std::string& name_,
+ const model::metadata& md_) :
+ _pimpl(new impl(name_, NULL, md_, none))
+{
+}
+
+
+
+/// Constructs a new fake test case.
+///
+/// A fake test case is a test case that is not really defined by the test
+/// program. Such test cases have a name surrounded by '__' and, when executed,
+/// they return a fixed, pre-recorded result.
+///
+/// This is necessary for the cases where listing the test cases of a test
+/// program fails. In this scenario, we generate a single test case within
+/// the test program that unconditionally returns a failure.
+///
+/// TODO(jmmv): Need to get rid of this. We should be able to report the
+/// status of test programs independently of test cases, as some interfaces
+/// don't know about the latter at all.
+///
+/// \param name_ The name to give to this fake test case. This name has to be
+/// prefixed and suffixed by '__' to clearly denote that this is internal.
+/// \param description_ The description of the test case, if any.
+/// \param test_result_ The fake result to return when this test case is run.
+model::test_case::test_case(
+ const std::string& name_,
+ const std::string& description_,
+ const model::test_result& test_result_) :
+ _pimpl(new impl(
+ name_,
+ NULL,
+ model::metadata_builder().set_description(description_).build(),
+ utils::make_optional(test_result_)))
+{
+ PRE_MSG(name_.length() > 4 && name_.substr(0, 2) == "__" &&
+ name_.substr(name_.length() - 2) == "__",
+ "Invalid fake name provided to fake test case");
+}
+
+
+/// Destroys a test case.
+model::test_case::~test_case(void)
+{
+}
+
+
+/// Constructs a new test case applying metadata defaults.
+///
+/// This method is intended to be used by the container test program when
+/// ownership of the test is given to it. At that point, the test case receives
+/// the default metadata properties of the test program, not the global
+/// defaults.
+///
+/// \param defaults The metadata properties to use as defaults. The provided
+/// object's lifetime MUST extend the lifetime of the test case. Because
+/// this is only intended to point at the metadata of the test program
+/// containing this test case, this assumption should hold.
+///
+/// \return A new test case.
+model::test_case
+model::test_case::apply_metadata_defaults(const metadata* defaults) const
+{
+ return test_case(std::shared_ptr< impl >(new impl(
+ _pimpl->name,
+ defaults,
+ _pimpl->md,
+ _pimpl->fake_result)));
+}
+
+
+/// Gets the test case name.
+///
+/// \return The test case name, relative to the test program.
+const std::string&
+model::test_case::name(void) const
+{
+ return _pimpl->name;
+}
+
+
+/// Gets the test case metadata.
+///
+/// This combines the test case's metadata with any possible test program
+/// metadata, using the latter as defaults. You should use this method in
+/// generaland not get_raw_metadata().
+///
+/// \return The test case metadata.
+model::metadata
+model::test_case::get_metadata(void) const
+{
+ return _pimpl->get_metadata();
+}
+
+
+/// Gets the original test case metadata without test program overrides.
+///
+/// This method should be used for storage purposes as serialized test cases
+/// should record exactly whatever the test case reported and not what the test
+/// program may have provided. The final values will be reconstructed at load
+/// time.
+///
+/// \return The test case metadata.
+const model::metadata&
+model::test_case::get_raw_metadata(void) const
+{
+ return _pimpl->md;
+}
+
+
+/// Gets the fake result pre-stored for this test case.
+///
+/// \return A fake result, or none if not defined.
+optional< model::test_result >
+model::test_case::fake_result(void) const
+{
+ return _pimpl->fake_result;
+}
+
+
+/// Equality comparator.
+///
+/// \warning Because test cases reference their container test programs, and
+/// test programs include test cases, we cannot perform a full comparison here:
+/// otherwise, we'd enter an inifinte loop. Therefore, out of necessity, this
+/// does NOT compare whether the container test programs of the affected test
+/// cases are the same.
+///
+/// \param other The other object to compare this one to.
+///
+/// \return True if this object and other are equal; false otherwise.
+bool
+model::test_case::operator==(const test_case& other) const
+{
+ return _pimpl == other._pimpl || *_pimpl == *other._pimpl;
+}
+
+
+/// 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_case::operator!=(const test_case& other) const
+{
+ return !(*this == other);
+}
+
+
+/// 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_case& object)
+{
+ output << F("test_case{name=%s, metadata=%s}")
+ % text::quote(object.name(), '\'')
+ % object.get_metadata();
+ return output;
+}
+
+
+/// Adds an already-constructed test case.
+///
+/// \param test_case The test case to add.
+///
+/// \return A reference to this builder.
+model::test_cases_map_builder&
+model::test_cases_map_builder::add(const test_case& test_case)
+{
+ _test_cases.insert(
+ test_cases_map::value_type(test_case.name(), test_case));
+ return *this;
+}
+
+
+/// Constructs and adds a new test case with default metadata.
+///
+/// \param test_case_name The name of the test case to add.
+///
+/// \return A reference to this builder.
+model::test_cases_map_builder&
+model::test_cases_map_builder::add(const std::string& test_case_name)
+{
+ return add(test_case(test_case_name, metadata_builder().build()));
+}
+
+
+/// Constructs and adds a new test case with explicit metadata.
+///
+/// \param test_case_name The name of the test case to add.
+/// \param metadata The metadata of the test case.
+///
+/// \return A reference to this builder.
+model::test_cases_map_builder&
+model::test_cases_map_builder::add(const std::string& test_case_name,
+ const metadata& metadata)
+{
+ return add(test_case(test_case_name, metadata));
+}
+
+
+/// Creates a new test_cases_map.
+///
+/// \return The constructed test_cases_map.
+model::test_cases_map
+model::test_cases_map_builder::build(void) const
+{
+ return _test_cases;
+}
diff --git a/model/test_case.hpp b/model/test_case.hpp
new file mode 100644
index 000000000000..3c6fe32c8e62
--- /dev/null
+++ b/model/test_case.hpp
@@ -0,0 +1,98 @@
+// 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.
+
+/// \file model/test_case.hpp
+/// Definition of the "test case" concept.
+
+#if !defined(MODEL_TEST_CASE_HPP)
+#define MODEL_TEST_CASE_HPP
+
+#include "model/test_case_fwd.hpp"
+
+#include <memory>
+#include <ostream>
+#include <string>
+
+#include "model/metadata_fwd.hpp"
+#include "model/test_result_fwd.hpp"
+#include "utils/noncopyable.hpp"
+#include "utils/optional_fwd.hpp"
+
+namespace model {
+
+
+/// Representation of a test case.
+///
+/// Test cases, on their own, are useless. They only make sense in the context
+/// of the container test program and as such this class should not be used
+/// directly.
+class test_case {
+ struct impl;
+
+ /// Pointer to the shared internal implementation.
+ std::shared_ptr< impl > _pimpl;
+
+ test_case(std::shared_ptr< impl >);
+
+public:
+ test_case(const std::string&, const metadata&);
+ test_case(const std::string&, const std::string&, const test_result&);
+ ~test_case(void);
+
+ test_case apply_metadata_defaults(const metadata*) const;
+
+ const std::string& name(void) const;
+ metadata get_metadata(void) const;
+ const metadata& get_raw_metadata(void) const;
+ utils::optional< test_result > fake_result(void) const;
+
+ bool operator==(const test_case&) const;
+ bool operator!=(const test_case&) const;
+};
+
+
+/// Builder for a test_cases_map object.
+class test_cases_map_builder : utils::noncopyable {
+ /// Accumulator for the map being built.
+ test_cases_map _test_cases;
+
+public:
+ test_cases_map_builder& add(const test_case&);
+ test_cases_map_builder& add(const std::string&);
+ test_cases_map_builder& add(const std::string&, const metadata&);
+
+ test_cases_map build(void) const;
+};
+
+
+std::ostream& operator<<(std::ostream&, const test_case&);
+
+
+} // namespace model
+
+#endif // !defined(MODEL_TEST_CASE_HPP)
diff --git a/model/test_case_fwd.hpp b/model/test_case_fwd.hpp
new file mode 100644
index 000000000000..72a40e513083
--- /dev/null
+++ b/model/test_case_fwd.hpp
@@ -0,0 +1,51 @@
+// Copyright 2014 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.
+
+/// \file model/test_case_fwd.hpp
+/// Forward declarations for model/test_case.hpp
+
+#if !defined(MODEL_TEST_CASE_FWD_HPP)
+#define MODEL_TEST_CASE_FWD_HPP
+
+#include <map>
+#include <string>
+
+namespace model {
+
+
+class test_case;
+class test_cases_map_builder;
+
+
+/// Collection of test cases keyed by their name.
+typedef std::map< std::string, model::test_case > test_cases_map;
+
+
+} // namespace model
+
+#endif // !defined(MODEL_TEST_CASE_FWD_HPP)
diff --git a/model/test_case_test.cpp b/model/test_case_test.cpp
new file mode 100644
index 000000000000..1a55de0fab42
--- /dev/null
+++ b/model/test_case_test.cpp
@@ -0,0 +1,263 @@
+// 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_case.hpp"
+
+#include <sstream>
+
+#include <atf-c++.hpp>
+
+#include "model/metadata.hpp"
+#include "model/test_result.hpp"
+#include "utils/datetime.hpp"
+#include "utils/format/containers.ipp"
+#include "utils/fs/path.hpp"
+#include "utils/optional.ipp"
+
+namespace datetime = utils::datetime;
+namespace fs = utils::fs;
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(test_case__ctor_and_getters)
+ATF_TEST_CASE_BODY(test_case__ctor_and_getters)
+{
+ const model::metadata md = model::metadata_builder()
+ .add_custom("first", "value")
+ .build();
+ const model::test_case test_case("foo", md);
+ ATF_REQUIRE_EQ("foo", test_case.name());
+ ATF_REQUIRE_EQ(md, test_case.get_metadata());
+ ATF_REQUIRE_EQ(md, test_case.get_raw_metadata());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(test_case__fake_result)
+ATF_TEST_CASE_BODY(test_case__fake_result)
+{
+ const model::test_result result(model::test_result_skipped,
+ "Some reason");
+ const model::test_case test_case("__foo__", "Some description", result);
+ ATF_REQUIRE_EQ("__foo__", test_case.name());
+ ATF_REQUIRE_EQ(result, test_case.fake_result().get());
+
+ const model::metadata exp_metadata = model::metadata_builder()
+ .set_description("Some description")
+ .build();
+ ATF_REQUIRE_EQ(exp_metadata, test_case.get_metadata());
+ ATF_REQUIRE_EQ(exp_metadata, test_case.get_raw_metadata());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(test_case__apply_metadata_overrides__real_test_case)
+ATF_TEST_CASE_BODY(test_case__apply_metadata_overrides__real_test_case)
+{
+ const model::metadata overrides = model::metadata_builder()
+ .add_required_config("the-variable")
+ .set_description("The test case")
+ .build();
+ const model::test_case base_test_case("foo", overrides);
+
+ const model::metadata defaults = model::metadata_builder()
+ .set_description("Default description")
+ .set_timeout(datetime::delta(10, 0))
+ .build();
+
+ const model::test_case test_case = base_test_case.apply_metadata_defaults(
+ &defaults);
+
+ const model::metadata expected = model::metadata_builder()
+ .add_required_config("the-variable")
+ .set_description("The test case")
+ .set_timeout(datetime::delta(10, 0))
+ .build();
+ ATF_REQUIRE_EQ(expected, test_case.get_metadata());
+ ATF_REQUIRE_EQ(overrides, test_case.get_raw_metadata());
+
+ // Ensure the original (although immutable) test case was not touched.
+ ATF_REQUIRE_EQ(overrides, base_test_case.get_metadata());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(test_case__apply_metadata_overrides__fake_test_case)
+ATF_TEST_CASE_BODY(test_case__apply_metadata_overrides__fake_test_case)
+{
+ const model::test_result result(model::test_result_skipped, "Irrelevant");
+ const model::test_case base_test_case("__foo__", "Fake test", result);
+
+ const model::metadata overrides = model::metadata_builder()
+ .set_description("Fake test")
+ .build();
+
+ const model::metadata defaults = model::metadata_builder()
+ .add_allowed_platform("some-value")
+ .set_description("Default description")
+ .build();
+
+ const model::test_case test_case = base_test_case.apply_metadata_defaults(
+ &defaults);
+
+ const model::metadata expected = model::metadata_builder()
+ .add_allowed_platform("some-value")
+ .set_description("Fake test")
+ .build();
+ ATF_REQUIRE_EQ(expected, test_case.get_metadata());
+ ATF_REQUIRE_EQ(overrides, test_case.get_raw_metadata());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(test_case__operators_eq_and_ne__copy);
+ATF_TEST_CASE_BODY(test_case__operators_eq_and_ne__copy)
+{
+ const model::test_case tc1("name", model::metadata_builder().build());
+ const model::test_case tc2 = tc1;
+ ATF_REQUIRE( tc1 == tc2);
+ ATF_REQUIRE(!(tc1 != tc2));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(test_case__operators_eq_and_ne__not_copy);
+ATF_TEST_CASE_BODY(test_case__operators_eq_and_ne__not_copy)
+{
+ const std::string base_name("name");
+ const model::metadata base_metadata = model::metadata_builder()
+ .add_custom("foo", "bar")
+ .build();
+
+ const model::test_case base_tc(base_name, base_metadata);
+
+ // Construct with all same values.
+ {
+ const model::test_case other_tc(base_name, base_metadata);
+
+ ATF_REQUIRE( base_tc == other_tc);
+ ATF_REQUIRE(!(base_tc != other_tc));
+ }
+
+ // Construct with all same values but different metadata objects.
+ {
+ const model::metadata other_metadata = model::metadata_builder()
+ .add_custom("foo", "bar")
+ .set_timeout(base_metadata.timeout())
+ .build();
+ const model::test_case other_tc(base_name, other_metadata);
+
+ ATF_REQUIRE( base_tc == other_tc);
+ ATF_REQUIRE(!(base_tc != other_tc));
+ }
+
+ // Different name.
+ {
+ const model::test_case other_tc("other", base_metadata);
+
+ ATF_REQUIRE(!(base_tc == other_tc));
+ ATF_REQUIRE( base_tc != other_tc);
+ }
+
+ // Different metadata.
+ {
+ const model::test_case other_tc(base_name,
+ model::metadata_builder().build());
+
+ ATF_REQUIRE(!(base_tc == other_tc));
+ ATF_REQUIRE( base_tc != other_tc);
+ }
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(test_case__output);
+ATF_TEST_CASE_BODY(test_case__output)
+{
+ const model::test_case tc1(
+ "the-name", model::metadata_builder()
+ .add_allowed_platform("foo").add_custom("bar", "baz").build());
+ std::ostringstream str;
+ str << tc1;
+ ATF_REQUIRE_EQ(
+ "test_case{name='the-name', "
+ "metadata=metadata{allowed_architectures='', allowed_platforms='foo', "
+ "custom.bar='baz', description='', has_cleanup='false', "
+ "is_exclusive='false', "
+ "required_configs='', required_disk_space='0', required_files='', "
+ "required_memory='0', "
+ "required_programs='', required_user='', timeout='300'}}",
+ str.str());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(test_cases_map__builder);
+ATF_TEST_CASE_BODY(test_cases_map__builder)
+{
+ model::test_cases_map_builder builder;
+ model::test_cases_map exp_test_cases;
+
+ ATF_REQUIRE_EQ(exp_test_cases, builder.build());
+
+ builder.add("default-metadata");
+ {
+ const model::test_case tc1("default-metadata",
+ model::metadata_builder().build());
+ exp_test_cases.insert(
+ model::test_cases_map::value_type(tc1.name(), tc1));
+ }
+ ATF_REQUIRE_EQ(exp_test_cases, builder.build());
+
+ builder.add("with-metadata",
+ model::metadata_builder().set_description("text").build());
+ {
+ const model::test_case tc1("with-metadata",
+ model::metadata_builder()
+ .set_description("text").build());
+ exp_test_cases.insert(
+ model::test_cases_map::value_type(tc1.name(), tc1));
+ }
+ ATF_REQUIRE_EQ(exp_test_cases, builder.build());
+
+ const model::test_case tc1("fully_built",
+ model::metadata_builder()
+ .set_description("something else").build());
+ builder.add(tc1);
+ exp_test_cases.insert(model::test_cases_map::value_type(tc1.name(), tc1));
+ ATF_REQUIRE_EQ(exp_test_cases, builder.build());
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, test_case__ctor_and_getters);
+ ATF_ADD_TEST_CASE(tcs, test_case__fake_result);
+
+ ATF_ADD_TEST_CASE(tcs, test_case__apply_metadata_overrides__real_test_case);
+ ATF_ADD_TEST_CASE(tcs, test_case__apply_metadata_overrides__fake_test_case);
+
+ ATF_ADD_TEST_CASE(tcs, test_case__operators_eq_and_ne__copy);
+ ATF_ADD_TEST_CASE(tcs, test_case__operators_eq_and_ne__not_copy);
+
+ ATF_ADD_TEST_CASE(tcs, test_case__output);
+
+ ATF_ADD_TEST_CASE(tcs, test_cases_map__builder);
+}
diff --git a/model/test_program.cpp b/model/test_program.cpp
new file mode 100644
index 000000000000..b9181aa49537
--- /dev/null
+++ b/model/test_program.cpp
@@ -0,0 +1,452 @@
+// 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));
+}
diff --git a/model/test_program.hpp b/model/test_program.hpp
new file mode 100644
index 000000000000..974ec2a12d19
--- /dev/null
+++ b/model/test_program.hpp
@@ -0,0 +1,110 @@
+// 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.
+
+/// \file model/test_program.hpp
+/// Definition of the "test program" concept.
+
+#if !defined(MODEL_TEST_PROGRAM_HPP)
+#define MODEL_TEST_PROGRAM_HPP
+
+#include "model/test_program_fwd.hpp"
+
+#include <memory>
+#include <ostream>
+#include <string>
+#include <vector>
+
+#include "model/metadata_fwd.hpp"
+#include "model/test_case_fwd.hpp"
+#include "utils/fs/path_fwd.hpp"
+#include "utils/noncopyable.hpp"
+
+namespace model {
+
+
+/// Representation of a test program.
+class test_program {
+ struct impl;
+
+ /// Pointer to the shared internal implementation.
+ std::shared_ptr< impl > _pimpl;
+
+protected:
+ void set_test_cases(const model::test_cases_map&);
+
+public:
+ test_program(const std::string&, const utils::fs::path&,
+ const utils::fs::path&, const std::string&,
+ const model::metadata&, const model::test_cases_map&);
+ virtual ~test_program(void);
+
+ const std::string& interface_name(void) const;
+ const utils::fs::path& root(void) const;
+ const utils::fs::path& relative_path(void) const;
+ const utils::fs::path absolute_path(void) const;
+ const std::string& test_suite_name(void) const;
+ const model::metadata& get_metadata(void) const;
+
+ const model::test_case& find(const std::string&) const;
+ virtual const model::test_cases_map& test_cases(void) const;
+
+ bool operator==(const test_program&) const;
+ bool operator!=(const test_program&) const;
+ bool operator<(const test_program&) const;
+};
+
+
+std::ostream& operator<<(std::ostream&, const test_program&);
+
+
+/// Builder for a test_program object.
+class test_program_builder : utils::noncopyable {
+ struct impl;
+
+ /// Pointer to the shared internal implementation.
+ std::auto_ptr< impl > _pimpl;
+
+public:
+ test_program_builder(const std::string&, const utils::fs::path&,
+ const utils::fs::path&, const std::string&);
+ ~test_program_builder(void);
+
+ test_program_builder& add_test_case(const std::string&);
+ test_program_builder& add_test_case(const std::string&,
+ const model::metadata&);
+
+ test_program_builder& set_metadata(const model::metadata&);
+
+ test_program build(void) const;
+ test_program_ptr build_ptr(void) const;
+};
+
+
+} // namespace model
+
+#endif // !defined(MODEL_TEST_PROGRAM_HPP)
diff --git a/model/test_program_fwd.hpp b/model/test_program_fwd.hpp
new file mode 100644
index 000000000000..100f017c30a6
--- /dev/null
+++ b/model/test_program_fwd.hpp
@@ -0,0 +1,55 @@
+// Copyright 2014 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.
+
+/// \file model/test_program_fwd.hpp
+/// Forward declarations for model/test_program.hpp
+
+#if !defined(MODEL_TEST_PROGRAM_FWD_HPP)
+#define MODEL_TEST_PROGRAM_FWD_HPP
+
+#include <memory>
+#include <vector>
+
+
+namespace model {
+
+
+class test_program;
+
+
+/// Pointer to a test program.
+typedef std::shared_ptr< test_program > test_program_ptr;
+
+
+/// Collection of test programs.
+typedef std::vector< test_program_ptr > test_programs_vector;
+
+
+} // namespace model
+
+#endif // !defined(MODEL_TEST_PROGRAM_FWD_HPP)
diff --git a/model/test_program_test.cpp b/model/test_program_test.cpp
new file mode 100644
index 000000000000..f9a8f7e59da3
--- /dev/null
+++ b/model/test_program_test.cpp
@@ -0,0 +1,711 @@
+// 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"
+
+extern "C" {
+#include <sys/stat.h>
+
+#include <signal.h>
+}
+
+#include <set>
+#include <sstream>
+
+#include <atf-c++.hpp>
+
+#include "model/exceptions.hpp"
+#include "model/metadata.hpp"
+#include "model/test_case.hpp"
+#include "model/test_result.hpp"
+#include "utils/env.hpp"
+#include "utils/format/containers.ipp"
+#include "utils/format/macros.hpp"
+#include "utils/fs/operations.hpp"
+#include "utils/fs/path.hpp"
+#include "utils/optional.ipp"
+
+namespace fs = utils::fs;
+
+
+namespace {
+
+
+/// Test program that sets its test cases lazily.
+///
+/// This test class exists to test the behavior of a test_program object when
+/// the class is extended to offer lazy loading of test cases. We simulate such
+/// lazy loading here by storing the list of test cases aside at construction
+/// time and later setting it lazily the first time test_cases() is called.
+class lazy_test_program : public model::test_program {
+ /// Whether set_test_cases() has yet been called or not.
+ mutable bool _set_test_cases_called;
+
+ /// The list of test cases for this test program.
+ ///
+ /// Only use this in the call to set_test_cases(). All other reads of the
+ /// test cases list should happen via the parent class' test_cases() method.
+ model::test_cases_map _lazy_test_cases;
+
+public:
+ /// 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.
+ /// \param metadata_ Metadata of the test program.
+ /// \param test_cases_ The collection of test cases in the test program.
+ lazy_test_program(const std::string& interface_name_,
+ const utils::fs::path& binary_,
+ const utils::fs::path& root_,
+ const std::string& test_suite_name_,
+ const model::metadata& metadata_,
+ const model::test_cases_map& test_cases_) :
+ test_program(interface_name_, binary_, root_, test_suite_name_,
+ metadata_, model::test_cases_map()),
+ _set_test_cases_called(false),
+ _lazy_test_cases(test_cases_)
+ {
+ }
+
+ /// Lazily sets the test cases on the parent and returns them.
+ ///
+ /// \return The list of test cases.
+ const model::test_cases_map&
+ test_cases(void) const
+ {
+ if (!_set_test_cases_called) {
+ const_cast< lazy_test_program* >(this)->set_test_cases(
+ _lazy_test_cases);
+ _set_test_cases_called = true;
+ }
+ return test_program::test_cases();
+ }
+};
+
+
+} // anonymous namespace
+
+
+/// Runs a ctor_and_getters test.
+///
+/// \tparam TestProgram Either model::test_program or lazy_test_program.
+template< class TestProgram >
+static void
+check_ctor_and_getters(void)
+{
+ const model::metadata tp_md = model::metadata_builder()
+ .add_custom("first", "foo")
+ .add_custom("second", "bar")
+ .build();
+ const model::metadata tc_md = model::metadata_builder()
+ .add_custom("first", "baz")
+ .build();
+
+ const TestProgram test_program(
+ "mock", fs::path("binary"), fs::path("root"), "suite-name", tp_md,
+ model::test_cases_map_builder().add("foo", tc_md).build());
+
+
+ ATF_REQUIRE_EQ("mock", test_program.interface_name());
+ ATF_REQUIRE_EQ(fs::path("binary"), test_program.relative_path());
+ ATF_REQUIRE_EQ(fs::current_path() / "root/binary",
+ test_program.absolute_path());
+ ATF_REQUIRE_EQ(fs::path("root"), test_program.root());
+ ATF_REQUIRE_EQ("suite-name", test_program.test_suite_name());
+ ATF_REQUIRE_EQ(tp_md, test_program.get_metadata());
+
+ const model::metadata exp_tc_md = model::metadata_builder()
+ .add_custom("first", "baz")
+ .add_custom("second", "bar")
+ .build();
+ const model::test_cases_map exp_tcs = model::test_cases_map_builder()
+ .add("foo", exp_tc_md)
+ .build();
+ ATF_REQUIRE_EQ(exp_tcs, test_program.test_cases());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(ctor_and_getters);
+ATF_TEST_CASE_BODY(ctor_and_getters)
+{
+ check_ctor_and_getters< model::test_program >();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(derived__ctor_and_getters);
+ATF_TEST_CASE_BODY(derived__ctor_and_getters)
+{
+ check_ctor_and_getters< lazy_test_program >();
+}
+
+
+/// Runs a find_ok test.
+///
+/// \tparam TestProgram Either model::test_program or lazy_test_program.
+template< class TestProgram >
+static void
+check_find_ok(void)
+{
+ const model::test_case test_case("main", model::metadata_builder().build());
+
+ const TestProgram test_program(
+ "mock", fs::path("non-existent"), fs::path("."), "suite-name",
+ model::metadata_builder().build(),
+ model::test_cases_map_builder().add(test_case).build());
+
+ const model::test_case& found_test_case = test_program.find("main");
+ ATF_REQUIRE_EQ(test_case, found_test_case);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(find__ok);
+ATF_TEST_CASE_BODY(find__ok)
+{
+ check_find_ok< model::test_program >();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(derived__find__ok);
+ATF_TEST_CASE_BODY(derived__find__ok)
+{
+ check_find_ok< lazy_test_program >();
+}
+
+
+/// Runs a find_missing test.
+///
+/// \tparam TestProgram Either model::test_program or lazy_test_program.
+template< class TestProgram >
+static void
+check_find_missing(void)
+{
+ const TestProgram test_program(
+ "mock", fs::path("non-existent"), fs::path("."), "suite-name",
+ model::metadata_builder().build(),
+ model::test_cases_map_builder().add("main").build());
+
+ ATF_REQUIRE_THROW_RE(model::not_found_error,
+ "case.*abc.*program.*non-existent",
+ test_program.find("abc"));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(find__missing);
+ATF_TEST_CASE_BODY(find__missing)
+{
+ check_find_missing< model::test_program >();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(derived__find__missing);
+ATF_TEST_CASE_BODY(derived__find__missing)
+{
+ check_find_missing< lazy_test_program >();
+}
+
+
+/// Runs a metadata_inheritance test.
+///
+/// \tparam TestProgram Either model::test_program or lazy_test_program.
+template< class TestProgram >
+static void
+check_metadata_inheritance(void)
+{
+ const model::test_cases_map test_cases = model::test_cases_map_builder()
+ .add("inherit-all")
+ .add("inherit-some",
+ model::metadata_builder()
+ .set_description("Overriden description")
+ .build())
+ .add("inherit-none",
+ model::metadata_builder()
+ .add_allowed_architecture("overriden-arch")
+ .add_allowed_platform("overriden-platform")
+ .set_description("Overriden description")
+ .build())
+ .build();
+
+ const model::metadata metadata = model::metadata_builder()
+ .add_allowed_architecture("base-arch")
+ .set_description("Base description")
+ .build();
+ const TestProgram test_program(
+ "plain", fs::path("non-existent"), fs::path("."), "suite-name",
+ metadata, test_cases);
+
+ {
+ const model::metadata exp_metadata = model::metadata_builder()
+ .add_allowed_architecture("base-arch")
+ .set_description("Base description")
+ .build();
+ ATF_REQUIRE_EQ(exp_metadata,
+ test_program.find("inherit-all").get_metadata());
+ }
+
+ {
+ const model::metadata exp_metadata = model::metadata_builder()
+ .add_allowed_architecture("base-arch")
+ .set_description("Overriden description")
+ .build();
+ ATF_REQUIRE_EQ(exp_metadata,
+ test_program.find("inherit-some").get_metadata());
+ }
+
+ {
+ const model::metadata exp_metadata = model::metadata_builder()
+ .add_allowed_architecture("overriden-arch")
+ .add_allowed_platform("overriden-platform")
+ .set_description("Overriden description")
+ .build();
+ ATF_REQUIRE_EQ(exp_metadata,
+ test_program.find("inherit-none").get_metadata());
+ }
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(metadata_inheritance);
+ATF_TEST_CASE_BODY(metadata_inheritance)
+{
+ check_metadata_inheritance< model::test_program >();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(derived__metadata_inheritance);
+ATF_TEST_CASE_BODY(derived__metadata_inheritance)
+{
+ check_metadata_inheritance< lazy_test_program >();
+}
+
+
+/// Runs a operators_eq_and_ne__copy test.
+///
+/// \tparam TestProgram Either model::test_program or lazy_test_program.
+template< class TestProgram >
+static void
+check_operators_eq_and_ne__copy(void)
+{
+ const TestProgram tp1(
+ "plain", fs::path("non-existent"), fs::path("."), "suite-name",
+ model::metadata_builder().build(),
+ model::test_cases_map());
+ const TestProgram tp2 = tp1;
+ ATF_REQUIRE( tp1 == tp2);
+ ATF_REQUIRE(!(tp1 != tp2));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__copy);
+ATF_TEST_CASE_BODY(operators_eq_and_ne__copy)
+{
+ check_operators_eq_and_ne__copy< model::test_program >();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(derived__operators_eq_and_ne__copy);
+ATF_TEST_CASE_BODY(derived__operators_eq_and_ne__copy)
+{
+ check_operators_eq_and_ne__copy< lazy_test_program >();
+}
+
+
+/// Runs a operators_eq_and_ne__not_copy test.
+///
+/// \tparam TestProgram Either model::test_program or lazy_test_program.
+template< class TestProgram >
+static void
+check_operators_eq_and_ne__not_copy(void)
+{
+ const std::string base_interface("plain");
+ const fs::path base_relative_path("the/test/program");
+ const fs::path base_root("/the/root");
+ const std::string base_test_suite("suite-name");
+ const model::metadata base_metadata = model::metadata_builder()
+ .add_custom("foo", "bar")
+ .build();
+
+ const model::test_cases_map base_tcs = model::test_cases_map_builder()
+ .add("main", model::metadata_builder()
+ .add_custom("second", "baz")
+ .build())
+ .build();
+
+ const TestProgram base_tp(
+ base_interface, base_relative_path, base_root, base_test_suite,
+ base_metadata, base_tcs);
+
+ // Construct with all same values.
+ {
+ const model::test_cases_map other_tcs = model::test_cases_map_builder()
+ .add("main", model::metadata_builder()
+ .add_custom("second", "baz")
+ .build())
+ .build();
+
+ const TestProgram other_tp(
+ base_interface, base_relative_path, base_root, base_test_suite,
+ base_metadata, other_tcs);
+
+ ATF_REQUIRE( base_tp == other_tp);
+ ATF_REQUIRE(!(base_tp != other_tp));
+ }
+
+ // Construct with same final metadata values but using a different
+ // intermediate representation. The original test program has one property
+ // in the base test program definition and another in the test case; here,
+ // we put both definitions explicitly in the test case.
+ {
+ const model::test_cases_map other_tcs = model::test_cases_map_builder()
+ .add("main", model::metadata_builder()
+ .add_custom("foo", "bar")
+ .add_custom("second", "baz")
+ .build())
+ .build();
+
+ const TestProgram other_tp(
+ base_interface, base_relative_path, base_root, base_test_suite,
+ base_metadata, other_tcs);
+
+ ATF_REQUIRE( base_tp == other_tp);
+ ATF_REQUIRE(!(base_tp != other_tp));
+ }
+
+ // Different interface.
+ {
+ const TestProgram other_tp(
+ "atf", base_relative_path, base_root, base_test_suite,
+ base_metadata, base_tcs);
+
+ ATF_REQUIRE(!(base_tp == other_tp));
+ ATF_REQUIRE( base_tp != other_tp);
+ }
+
+ // Different relative path.
+ {
+ const TestProgram other_tp(
+ base_interface, fs::path("a/b/c"), base_root, base_test_suite,
+ base_metadata, base_tcs);
+
+ ATF_REQUIRE(!(base_tp == other_tp));
+ ATF_REQUIRE( base_tp != other_tp);
+ }
+
+ // Different root.
+ {
+ const TestProgram other_tp(
+ base_interface, base_relative_path, fs::path("."), base_test_suite,
+ base_metadata, base_tcs);
+
+ ATF_REQUIRE(!(base_tp == other_tp));
+ ATF_REQUIRE( base_tp != other_tp);
+ }
+
+ // Different test suite.
+ {
+ const TestProgram other_tp(
+ base_interface, base_relative_path, base_root, "different-suite",
+ base_metadata, base_tcs);
+
+ ATF_REQUIRE(!(base_tp == other_tp));
+ ATF_REQUIRE( base_tp != other_tp);
+ }
+
+ // Different metadata.
+ {
+ const TestProgram other_tp(
+ base_interface, base_relative_path, base_root, base_test_suite,
+ model::metadata_builder().build(), base_tcs);
+
+ ATF_REQUIRE(!(base_tp == other_tp));
+ ATF_REQUIRE( base_tp != other_tp);
+ }
+
+ // Different test cases.
+ {
+ const model::test_cases_map other_tcs = model::test_cases_map_builder()
+ .add("foo").build();
+
+ const TestProgram other_tp(
+ base_interface, base_relative_path, base_root, base_test_suite,
+ base_metadata, other_tcs);
+
+ ATF_REQUIRE(!(base_tp == other_tp));
+ ATF_REQUIRE( base_tp != other_tp);
+ }
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(operators_eq_and_ne__not_copy);
+ATF_TEST_CASE_BODY(operators_eq_and_ne__not_copy)
+{
+ check_operators_eq_and_ne__not_copy< model::test_program >();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(derived__operators_eq_and_ne__not_copy);
+ATF_TEST_CASE_BODY(derived__operators_eq_and_ne__not_copy)
+{
+ check_operators_eq_and_ne__not_copy< lazy_test_program >();
+}
+
+
+/// Runs a operator_lt test.
+///
+/// \tparam TestProgram Either model::test_program or lazy_test_program.
+template< class TestProgram >
+static void
+check_operator_lt(void)
+{
+ const TestProgram tp1(
+ "plain", fs::path("a/b/c"), fs::path("/foo/bar"), "suite-name",
+ model::metadata_builder().build(),
+ model::test_cases_map());
+ const TestProgram tp2(
+ "atf", fs::path("c"), fs::path("/foo/bar"), "suite-name",
+ model::metadata_builder().build(),
+ model::test_cases_map());
+ const TestProgram tp3(
+ "plain", fs::path("a/b/c"), fs::path("/abc"), "suite-name",
+ model::metadata_builder().build(),
+ model::test_cases_map());
+
+ ATF_REQUIRE(!(tp1 < tp1));
+
+ ATF_REQUIRE( tp1 < tp2);
+ ATF_REQUIRE(!(tp2 < tp1));
+
+ ATF_REQUIRE(!(tp1 < tp3));
+ ATF_REQUIRE( tp3 < tp1);
+
+ // And now, test the actual reason why we want to have an < overload by
+ // attempting to put the various programs in a set.
+ std::set< TestProgram > programs;
+ programs.insert(tp1);
+ programs.insert(tp2);
+ programs.insert(tp3);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(operator_lt);
+ATF_TEST_CASE_BODY(operator_lt)
+{
+ check_operator_lt< model::test_program >();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(derived__operator_lt);
+ATF_TEST_CASE_BODY(derived__operator_lt)
+{
+ check_operator_lt< lazy_test_program >();
+}
+
+
+/// Runs a output__no_test_cases test.
+///
+/// \tparam TestProgram Either model::test_program or lazy_test_program.
+template< class TestProgram >
+static void
+check_output__no_test_cases(void)
+{
+ TestProgram tp(
+ "plain", fs::path("binary/path"), fs::path("/the/root"), "suite-name",
+ model::metadata_builder().add_allowed_architecture("a").build(),
+ model::test_cases_map());
+
+ std::ostringstream str;
+ str << tp;
+ ATF_REQUIRE_EQ(
+ "test_program{interface='plain', binary='binary/path', "
+ "root='/the/root', test_suite='suite-name', "
+ "metadata=metadata{allowed_architectures='a', allowed_platforms='', "
+ "description='', has_cleanup='false', is_exclusive='false', "
+ "required_configs='', required_disk_space='0', required_files='', "
+ "required_memory='0', "
+ "required_programs='', required_user='', timeout='300'}, "
+ "test_cases=map()}",
+ str.str());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(output__no_test_cases);
+ATF_TEST_CASE_BODY(output__no_test_cases)
+{
+ check_output__no_test_cases< model::test_program >();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(derived__output__no_test_cases);
+ATF_TEST_CASE_BODY(derived__output__no_test_cases)
+{
+ check_output__no_test_cases< lazy_test_program >();
+}
+
+
+/// Runs a output__some_test_cases test.
+///
+/// \tparam TestProgram Either model::test_program or lazy_test_program.
+template< class TestProgram >
+static void
+check_output__some_test_cases(void)
+{
+ const model::test_cases_map test_cases = model::test_cases_map_builder()
+ .add("the-name", model::metadata_builder()
+ .add_allowed_platform("foo")
+ .add_custom("bar", "baz")
+ .build())
+ .add("another-name")
+ .build();
+
+ const TestProgram tp = TestProgram(
+ "plain", fs::path("binary/path"), fs::path("/the/root"), "suite-name",
+ model::metadata_builder().add_allowed_architecture("a").build(),
+ test_cases);
+
+ std::ostringstream str;
+ str << tp;
+ ATF_REQUIRE_EQ(
+ "test_program{interface='plain', binary='binary/path', "
+ "root='/the/root', test_suite='suite-name', "
+ "metadata=metadata{allowed_architectures='a', allowed_platforms='', "
+ "description='', has_cleanup='false', is_exclusive='false', "
+ "required_configs='', required_disk_space='0', required_files='', "
+ "required_memory='0', "
+ "required_programs='', required_user='', timeout='300'}, "
+ "test_cases=map("
+ "another-name=test_case{name='another-name', "
+ "metadata=metadata{allowed_architectures='a', allowed_platforms='', "
+ "description='', has_cleanup='false', is_exclusive='false', "
+ "required_configs='', required_disk_space='0', required_files='', "
+ "required_memory='0', "
+ "required_programs='', required_user='', timeout='300'}}, "
+ "the-name=test_case{name='the-name', "
+ "metadata=metadata{allowed_architectures='a', allowed_platforms='foo', "
+ "custom.bar='baz', description='', has_cleanup='false', "
+ "is_exclusive='false', "
+ "required_configs='', required_disk_space='0', required_files='', "
+ "required_memory='0', "
+ "required_programs='', required_user='', timeout='300'}})}",
+ str.str());
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(output__some_test_cases);
+ATF_TEST_CASE_BODY(output__some_test_cases)
+{
+ check_output__some_test_cases< model::test_program >();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(derived__output__some_test_cases);
+ATF_TEST_CASE_BODY(derived__output__some_test_cases)
+{
+ check_output__some_test_cases< lazy_test_program >();
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(builder__defaults);
+ATF_TEST_CASE_BODY(builder__defaults)
+{
+ const model::test_program expected(
+ "mock", fs::path("non-existent"), fs::path("."), "suite-name",
+ model::metadata_builder().build(), model::test_cases_map());
+
+ const model::test_program built = model::test_program_builder(
+ "mock", fs::path("non-existent"), fs::path("."), "suite-name")
+ .build();
+
+ ATF_REQUIRE_EQ(built, expected);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(builder__overrides);
+ATF_TEST_CASE_BODY(builder__overrides)
+{
+ const model::metadata md = model::metadata_builder()
+ .add_custom("foo", "bar")
+ .build();
+ const model::test_cases_map tcs = model::test_cases_map_builder()
+ .add("first")
+ .add("second", md)
+ .build();
+ const model::test_program expected(
+ "mock", fs::path("binary"), fs::path("root"), "suite-name", md, tcs);
+
+ const model::test_program built = model::test_program_builder(
+ "mock", fs::path("binary"), fs::path("root"), "suite-name")
+ .add_test_case("first")
+ .add_test_case("second", md)
+ .set_metadata(md)
+ .build();
+
+ ATF_REQUIRE_EQ(built, expected);
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(builder__ptr);
+ATF_TEST_CASE_BODY(builder__ptr)
+{
+ const model::test_program expected(
+ "mock", fs::path("non-existent"), fs::path("."), "suite-name",
+ model::metadata_builder().build(), model::test_cases_map());
+
+ const model::test_program_ptr built = model::test_program_builder(
+ "mock", fs::path("non-existent"), fs::path("."), "suite-name")
+ .build_ptr();
+
+ ATF_REQUIRE_EQ(*built, expected);
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, ctor_and_getters);
+ ATF_ADD_TEST_CASE(tcs, find__ok);
+ ATF_ADD_TEST_CASE(tcs, find__missing);
+ ATF_ADD_TEST_CASE(tcs, metadata_inheritance);
+ ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__copy);
+ ATF_ADD_TEST_CASE(tcs, operators_eq_and_ne__not_copy);
+ ATF_ADD_TEST_CASE(tcs, operator_lt);
+ ATF_ADD_TEST_CASE(tcs, output__no_test_cases);
+ ATF_ADD_TEST_CASE(tcs, output__some_test_cases);
+
+ ATF_ADD_TEST_CASE(tcs, derived__ctor_and_getters);
+ ATF_ADD_TEST_CASE(tcs, derived__find__ok);
+ ATF_ADD_TEST_CASE(tcs, derived__find__missing);
+ ATF_ADD_TEST_CASE(tcs, derived__metadata_inheritance);
+ ATF_ADD_TEST_CASE(tcs, derived__operators_eq_and_ne__copy);
+ ATF_ADD_TEST_CASE(tcs, derived__operators_eq_and_ne__not_copy);
+ ATF_ADD_TEST_CASE(tcs, derived__operator_lt);
+ ATF_ADD_TEST_CASE(tcs, derived__output__no_test_cases);
+ ATF_ADD_TEST_CASE(tcs, derived__output__some_test_cases);
+
+ ATF_ADD_TEST_CASE(tcs, builder__defaults);
+ ATF_ADD_TEST_CASE(tcs, builder__overrides);
+ ATF_ADD_TEST_CASE(tcs, builder__ptr);
+}
diff --git a/model/test_result.cpp b/model/test_result.cpp
new file mode 100644
index 000000000000..7392e77f5561
--- /dev/null
+++ b/model/test_result.cpp
@@ -0,0 +1,142 @@
+// Copyright 2014 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_result.hpp"
+
+#include "utils/format/macros.hpp"
+#include "utils/sanity.hpp"
+#include "utils/text/operations.ipp"
+
+namespace text = utils::text;
+
+
+/// Constructs a base result.
+///
+/// \param type_ The type of the result.
+/// \param reason_ The reason explaining the result, if any. It is OK for this
+/// to be empty, which is actually the default.
+model::test_result::test_result(const test_result_type type_,
+ const std::string& reason_) :
+ _type(type_),
+ _reason(reason_)
+{
+}
+
+
+/// Returns the type of the result.
+///
+/// \return A result type.
+model::test_result_type
+model::test_result::type(void) const
+{
+ return _type;
+}
+
+
+/// Returns the reason explaining the result.
+///
+/// \return A textual reason, possibly empty.
+const std::string&
+model::test_result::reason(void) const
+{
+ return _reason;
+}
+
+
+/// True if the test case result has a positive connotation.
+///
+/// \return Whether the test case is good or not.
+bool
+model::test_result::good(void) const
+{
+ switch (_type) {
+ case test_result_expected_failure:
+ case test_result_passed:
+ case test_result_skipped:
+ return true;
+
+ case test_result_broken:
+ case test_result_failed:
+ return false;
+ }
+ UNREACHABLE;
+}
+
+
+/// Equality comparator.
+///
+/// \param other The test result to compare to.
+///
+/// \return True if the other object is equal to this one, false otherwise.
+bool
+model::test_result::operator==(const test_result& other) const
+{
+ return _type == other._type && _reason == other._reason;
+}
+
+
+/// Inequality comparator.
+///
+/// \param other The test result to compare to.
+///
+/// \return True if the other object is different from this one, false
+/// otherwise.
+bool
+model::test_result::operator!=(const test_result& other) const
+{
+ return !(*this == other);
+}
+
+
+/// 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_result& object)
+{
+ std::string result_name;
+ switch (object.type()) {
+ case test_result_broken: result_name = "broken"; break;
+ case test_result_expected_failure: result_name = "expected_failure"; break;
+ case test_result_failed: result_name = "failed"; break;
+ case test_result_passed: result_name = "passed"; break;
+ case test_result_skipped: result_name = "skipped"; break;
+ }
+ const std::string& reason = object.reason();
+ if (reason.empty()) {
+ output << F("model::test_result{type=%s}")
+ % text::quote(result_name, '\'');
+ } else {
+ output << F("model::test_result{type=%s, reason=%s}")
+ % text::quote(result_name, '\'') % text::quote(reason, '\'');
+ }
+ return output;
+}
diff --git a/model/test_result.hpp b/model/test_result.hpp
new file mode 100644
index 000000000000..b9c439ce789a
--- /dev/null
+++ b/model/test_result.hpp
@@ -0,0 +1,79 @@
+// Copyright 2014 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.
+
+/// \file model/test_result.hpp
+/// Definition of the "test result" concept.
+
+#if !defined(MODEL_TEST_RESULT_HPP)
+#define MODEL_TEST_RESULT_HPP
+
+#include "model/test_result_fwd.hpp"
+
+#include <ostream>
+#include <string>
+
+namespace model {
+
+
+/// Representation of a single test result.
+///
+/// A test result is a simple pair of (type, reason). The type indicates the
+/// semantics of the results, and the optional reason provides an extra
+/// description of the result type.
+///
+/// In general, a 'passed' result will not have a reason attached, because a
+/// successful test case does not deserve any kind of explanation. We used to
+/// special-case this with a very complex class hierarchy, but it proved to
+/// result in an extremely-complex to maintain code base that provided no
+/// benefits. As a result, we allow any test type to carry a reason.
+class test_result {
+ /// The type of the result.
+ test_result_type _type;
+
+ /// A description of the result; may be empty.
+ std::string _reason;
+
+public:
+ test_result(const test_result_type, const std::string& = "");
+
+ test_result_type type(void) const;
+ const std::string& reason(void) const;
+
+ bool good(void) const;
+
+ bool operator==(const test_result&) const;
+ bool operator!=(const test_result&) const;
+};
+
+
+std::ostream& operator<<(std::ostream&, const test_result&);
+
+
+} // namespace model
+
+#endif // !defined(MODEL_TEST_RESULT_HPP)
diff --git a/model/test_result_fwd.hpp b/model/test_result_fwd.hpp
new file mode 100644
index 000000000000..d7871e81d23e
--- /dev/null
+++ b/model/test_result_fwd.hpp
@@ -0,0 +1,53 @@
+// Copyright 2014 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.
+
+/// \file model/test_result_fwd.hpp
+/// Forward declarations for model/test_result.hpp
+
+#if !defined(MODEL_TEST_RESULT_FWD_HPP)
+#define MODEL_TEST_RESULT_FWD_HPP
+
+namespace model {
+
+
+/// Definitions for all possible test case results.
+enum test_result_type {
+ test_result_broken,
+ test_result_expected_failure,
+ test_result_failed,
+ test_result_passed,
+ test_result_skipped,
+};
+
+
+class test_result;
+
+
+} // namespace model
+
+#endif // !defined(MODEL_TEST_RESULT_FWD_HPP)
diff --git a/model/test_result_test.cpp b/model/test_result_test.cpp
new file mode 100644
index 000000000000..355587d37aee
--- /dev/null
+++ b/model/test_result_test.cpp
@@ -0,0 +1,185 @@
+// Copyright 2014 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_result.hpp"
+
+#include <sstream>
+
+#include <atf-c++.hpp>
+
+
+/// Creates a test case to validate the getters.
+///
+/// \param name The name of the test case; "__getters" will be appended.
+/// \param expected_type The expected type of the result.
+/// \param expected_reason The expected reason for the result.
+/// \param result The result to query.
+#define GETTERS_TEST(name, expected_type, expected_reason, result) \
+ ATF_TEST_CASE_WITHOUT_HEAD(name ## __getters); \
+ ATF_TEST_CASE_BODY(name ## __getters) \
+ { \
+ ATF_REQUIRE(expected_type == result.type()); \
+ ATF_REQUIRE_EQ(expected_reason, result.reason()); \
+ }
+
+
+/// Creates a test case to validate the good() method.
+///
+/// \param name The name of the test case; "__good" will be appended.
+/// \param expected The expected result of good().
+/// \param result_type The result type to check.
+#define GOOD_TEST(name, expected, result_type) \
+ ATF_TEST_CASE_WITHOUT_HEAD(name ## __good); \
+ ATF_TEST_CASE_BODY(name ## __good) \
+ { \
+ ATF_REQUIRE_EQ(expected, model::test_result(result_type).good()); \
+ }
+
+
+/// Creates a test case to validate the operator<< method.
+///
+/// \param name The name of the test case; "__output" will be appended.
+/// \param expected The expected string in the output.
+/// \param result The result to format.
+#define OUTPUT_TEST(name, expected, result) \
+ ATF_TEST_CASE_WITHOUT_HEAD(name ## __output); \
+ ATF_TEST_CASE_BODY(name ## __output) \
+ { \
+ std::ostringstream output; \
+ output << "prefix" << result << "suffix"; \
+ ATF_REQUIRE_EQ("prefix" + std::string(expected) + "suffix", \
+ output.str()); \
+ }
+
+
+GETTERS_TEST(
+ broken,
+ model::test_result_broken,
+ "The reason",
+ model::test_result(model::test_result_broken, "The reason"));
+GETTERS_TEST(
+ expected_failure,
+ model::test_result_expected_failure,
+ "The reason",
+ model::test_result(model::test_result_expected_failure, "The reason"));
+GETTERS_TEST(
+ failed,
+ model::test_result_failed,
+ "The reason",
+ model::test_result(model::test_result_failed, "The reason"));
+GETTERS_TEST(
+ passed,
+ model::test_result_passed,
+ "",
+ model::test_result(model::test_result_passed));
+GETTERS_TEST(
+ skipped,
+ model::test_result_skipped,
+ "The reason",
+ model::test_result(model::test_result_skipped, "The reason"));
+
+
+GOOD_TEST(broken, false, model::test_result_broken);
+GOOD_TEST(expected_failure, true, model::test_result_expected_failure);
+GOOD_TEST(failed, false, model::test_result_failed);
+GOOD_TEST(passed, true, model::test_result_passed);
+GOOD_TEST(skipped, true, model::test_result_skipped);
+
+
+OUTPUT_TEST(
+ broken,
+ "model::test_result{type='broken', reason='foo'}",
+ model::test_result(model::test_result_broken, "foo"));
+OUTPUT_TEST(
+ expected_failure,
+ "model::test_result{type='expected_failure', reason='abc def'}",
+ model::test_result(model::test_result_expected_failure, "abc def"));
+OUTPUT_TEST(
+ failed,
+ "model::test_result{type='failed', reason='some \\'string'}",
+ model::test_result(model::test_result_failed, "some 'string"));
+OUTPUT_TEST(
+ passed,
+ "model::test_result{type='passed'}",
+ model::test_result(model::test_result_passed, ""));
+OUTPUT_TEST(
+ skipped,
+ "model::test_result{type='skipped', reason='last message'}",
+ model::test_result(model::test_result_skipped, "last message"));
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(operator_eq);
+ATF_TEST_CASE_BODY(operator_eq)
+{
+ const model::test_result result1(model::test_result_broken, "Foo");
+ const model::test_result result2(model::test_result_broken, "Foo");
+ const model::test_result result3(model::test_result_broken, "Bar");
+ const model::test_result result4(model::test_result_failed, "Foo");
+
+ ATF_REQUIRE( result1 == result1);
+ ATF_REQUIRE( result1 == result2);
+ ATF_REQUIRE(!(result1 == result3));
+ ATF_REQUIRE(!(result1 == result4));
+}
+
+
+ATF_TEST_CASE_WITHOUT_HEAD(operator_ne);
+ATF_TEST_CASE_BODY(operator_ne)
+{
+ const model::test_result result1(model::test_result_broken, "Foo");
+ const model::test_result result2(model::test_result_broken, "Foo");
+ const model::test_result result3(model::test_result_broken, "Bar");
+ const model::test_result result4(model::test_result_failed, "Foo");
+
+ ATF_REQUIRE(!(result1 != result1));
+ ATF_REQUIRE(!(result1 != result2));
+ ATF_REQUIRE( result1 != result3);
+ ATF_REQUIRE( result1 != result4);
+}
+
+
+ATF_INIT_TEST_CASES(tcs)
+{
+ ATF_ADD_TEST_CASE(tcs, broken__getters);
+ ATF_ADD_TEST_CASE(tcs, broken__good);
+ ATF_ADD_TEST_CASE(tcs, broken__output);
+ ATF_ADD_TEST_CASE(tcs, expected_failure__getters);
+ ATF_ADD_TEST_CASE(tcs, expected_failure__good);
+ ATF_ADD_TEST_CASE(tcs, expected_failure__output);
+ ATF_ADD_TEST_CASE(tcs, failed__getters);
+ ATF_ADD_TEST_CASE(tcs, failed__good);
+ ATF_ADD_TEST_CASE(tcs, failed__output);
+ ATF_ADD_TEST_CASE(tcs, passed__getters);
+ ATF_ADD_TEST_CASE(tcs, passed__good);
+ ATF_ADD_TEST_CASE(tcs, passed__output);
+ ATF_ADD_TEST_CASE(tcs, skipped__getters);
+ ATF_ADD_TEST_CASE(tcs, skipped__good);
+ ATF_ADD_TEST_CASE(tcs, skipped__output);
+ ATF_ADD_TEST_CASE(tcs, operator_eq);
+ ATF_ADD_TEST_CASE(tcs, operator_ne);
+}
diff --git a/model/types.hpp b/model/types.hpp
new file mode 100644
index 000000000000..e877b6f58d46
--- /dev/null
+++ b/model/types.hpp
@@ -0,0 +1,61 @@
+// Copyright 2014 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.
+
+/// \file model/types.hpp
+/// Definition of miscellaneous base types required by our classes.
+///
+/// We consider objects coming from the STL and from the utils module to be
+/// base types.
+
+#if !defined(MODEL_TYPES_HPP)
+#define MODEL_TYPES_HPP
+
+#include <map>
+#include <set>
+#include <string>
+
+#include "utils/fs/path_fwd.hpp"
+
+namespace model {
+
+
+/// Collection of paths.
+typedef std::set< utils::fs::path > paths_set;
+
+
+/// Collection of strings.
+typedef std::set< std::string > strings_set;
+
+
+/// Collection of test properties in their textual form.
+typedef std::map< std::string, std::string > properties_map;
+
+
+} // namespace model
+
+#endif // !defined(MODEL_TYPES_HPP)