diff options
Diffstat (limited to 'engine/kyuafile.cpp')
-rw-r--r-- | engine/kyuafile.cpp | 694 |
1 files changed, 694 insertions, 0 deletions
diff --git a/engine/kyuafile.cpp b/engine/kyuafile.cpp new file mode 100644 index 000000000000..4dca3193832b --- /dev/null +++ b/engine/kyuafile.cpp @@ -0,0 +1,694 @@ +// 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 "engine/kyuafile.hpp" + +#include <algorithm> +#include <iterator> +#include <stdexcept> + +#include <lutok/exceptions.hpp> +#include <lutok/operations.hpp> +#include <lutok/stack_cleaner.hpp> +#include <lutok/state.ipp> + +#include "engine/exceptions.hpp" +#include "engine/scheduler.hpp" +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "utils/config/exceptions.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/lua_module.hpp" +#include "utils/fs/operations.hpp" +#include "utils/logging/macros.hpp" +#include "utils/noncopyable.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace scheduler = engine::scheduler; + +using utils::none; +using utils::optional; + + +// History of Kyuafile file versions: +// +// 3 - DOES NOT YET EXIST. Pending changes for when this is introduced: +// +// * Revisit what to do about the test_suite definition. Support for +// per-test program overrides is deprecated and should be removed. +// But, maybe, the whole test_suite definition idea is wrong and we +// should instead be explicitly telling which configuration variables +// to "inject" into each test program. +// +// 2 - Changed the syntax() call to take only a version number, instead of the +// word 'config' as the first argument and the version as the second one. +// Files now start with syntax(2) instead of syntax('kyuafile', 1). +// +// 1 - Initial version. + + +namespace { + + +static int lua_current_kyuafile(lutok::state&); +static int lua_generic_test_program(lutok::state&); +static int lua_include(lutok::state&); +static int lua_syntax(lutok::state&); +static int lua_test_suite(lutok::state&); + + +/// Concatenates two paths while avoiding paths to start with './'. +/// +/// \param root Path to the directory containing the file. +/// \param file Path to concatenate to root. Cannot be absolute. +/// +/// \return The concatenated path. +static fs::path +relativize(const fs::path& root, const fs::path& file) +{ + PRE(!file.is_absolute()); + + if (root == fs::path(".")) + return file; + else + return root / file; +} + + +/// Implementation of a parser for Kyuafiles. +/// +/// The main purpose of having this as a class is to keep track of global state +/// within the Lua files and allowing the Lua callbacks to easily access such +/// data. +class parser : utils::noncopyable { + /// Lua state to parse a single Kyuafile file. + lutok::state _state; + + /// Root directory of the test suite represented by the Kyuafile. + const fs::path _source_root; + + /// Root directory of the test programs. + const fs::path _build_root; + + /// Name of the Kyuafile to load relative to _source_root. + const fs::path _relative_filename; + + /// Version of the Kyuafile file format requested by the parsed file. + /// + /// This is set once the Kyuafile invokes the syntax() call. + optional< int > _version; + + /// Name of the test suite defined by the Kyuafile. + /// + /// This is set once the Kyuafile invokes the test_suite() call. + optional< std::string > _test_suite; + + /// Collection of test programs defined by the Kyuafile. + /// + /// This acts as an accumulator for all the *_test_program() calls within + /// the Kyuafile. + model::test_programs_vector _test_programs; + + /// Safely gets _test_suite and respects any test program overrides. + /// + /// \param program_override The test program-specific test suite name. May + /// be empty to indicate no override. + /// + /// \return The name of the test suite. + /// + /// \throw std::runtime_error If program_override is empty and the Kyuafile + /// did not yet define the global name of the test suite. + std::string + get_test_suite(const std::string& program_override) + { + std::string test_suite; + + if (program_override.empty()) { + if (!_test_suite) { + throw std::runtime_error("No test suite defined in the " + "Kyuafile and no override provided in " + "the test_program definition"); + } + test_suite = _test_suite.get(); + } else { + test_suite = program_override; + } + + return test_suite; + } + +public: + /// Initializes the parser and the Lua state. + /// + /// \param source_root_ The root directory of the test suite represented by + /// the Kyuafile. + /// \param build_root_ The root directory of the test programs. + /// \param relative_filename_ Name of the Kyuafile to load relative to + /// source_root_. + /// \param user_config User configuration holding any test suite properties + /// to be passed to the list operation. + /// \param scheduler_handle The scheduler context to use for loading the + /// test case lists. + parser(const fs::path& source_root_, const fs::path& build_root_, + const fs::path& relative_filename_, + const config::tree& user_config, + scheduler::scheduler_handle& scheduler_handle) : + _source_root(source_root_), _build_root(build_root_), + _relative_filename(relative_filename_) + { + lutok::stack_cleaner cleaner(_state); + + _state.push_cxx_function(lua_syntax); + _state.set_global("syntax"); + + *_state.new_userdata< parser* >() = this; + _state.set_global("_parser"); + + _state.push_cxx_function(lua_current_kyuafile); + _state.set_global("current_kyuafile"); + + *_state.new_userdata< const config::tree* >() = &user_config; + *_state.new_userdata< scheduler::scheduler_handle* >() = + &scheduler_handle; + _state.push_cxx_closure(lua_include, 2); + _state.set_global("include"); + + _state.push_cxx_function(lua_test_suite); + _state.set_global("test_suite"); + + const std::set< std::string > interfaces = + scheduler::registered_interface_names(); + for (std::set< std::string >::const_iterator iter = interfaces.begin(); + iter != interfaces.end(); ++iter) { + const std::string& interface = *iter; + + _state.push_string(interface); + *_state.new_userdata< const config::tree* >() = &user_config; + *_state.new_userdata< scheduler::scheduler_handle* >() = + &scheduler_handle; + _state.push_cxx_closure(lua_generic_test_program, 3); + _state.set_global(interface + "_test_program"); + } + + _state.open_base(); + _state.open_string(); + _state.open_table(); + fs::open_fs(_state, callback_current_kyuafile().branch_path()); + } + + /// Destructor. + ~parser(void) + { + } + + /// Gets the parser object associated to a Lua state. + /// + /// \param state The Lua state from which to obtain the parser object. + /// + /// \return A pointer to the parser. + static parser* + get_from_state(lutok::state& state) + { + lutok::stack_cleaner cleaner(state); + state.get_global("_parser"); + return *state.to_userdata< parser* >(-1); + } + + /// Callback for the Kyuafile current_kyuafile() function. + /// + /// \return Returns the absolute path to the current Kyuafile. + fs::path + callback_current_kyuafile(void) const + { + const fs::path file = relativize(_source_root, _relative_filename); + if (file.is_absolute()) + return file; + else + return file.to_absolute(); + } + + /// Callback for the Kyuafile include() function. + /// + /// \post _test_programs is extended with the the test programs defined by + /// the included file. + /// + /// \param raw_file Path to the file to include. + /// \param user_config User configuration holding any test suite properties + /// to be passed to the list operation. + /// \param scheduler_handle Scheduler context to run test programs in. + void + callback_include(const fs::path& raw_file, + const config::tree& user_config, + scheduler::scheduler_handle& scheduler_handle) + { + const fs::path file = relativize(_relative_filename.branch_path(), + raw_file); + const model::test_programs_vector subtps = + parser(_source_root, _build_root, file, user_config, + scheduler_handle).parse(); + + std::copy(subtps.begin(), subtps.end(), + std::back_inserter(_test_programs)); + } + + /// Callback for the Kyuafile syntax() function. + /// + /// \post _version is set to the requested version. + /// + /// \param version Version of the Kyuafile syntax requested by the file. + /// + /// \throw std::runtime_error If the format or the version are invalid, or + /// if syntax() has already been called. + void + callback_syntax(const int version) + { + if (_version) + throw std::runtime_error("Can only call syntax() once"); + + if (version < 1 || version > 2) + throw std::runtime_error(F("Unsupported file version %s") % + version); + + _version = utils::make_optional(version); + } + + /// Callback for the various Kyuafile *_test_program() functions. + /// + /// \post _test_programs is extended to include the newly defined test + /// program. + /// + /// \param interface Name of the test program interface. + /// \param raw_path Path to the test program, relative to the Kyuafile. + /// This has to be adjusted according to the relative location of this + /// Kyuafile to _source_root. + /// \param test_suite_override Name of the test suite this test program + /// belongs to, if explicitly defined at the test program level. + /// \param metadata Metadata variables passed to the test program. + /// \param user_config User configuration holding any test suite properties + /// to be passed to the list operation. + /// \param scheduler_handle Scheduler context to run test programs in. + /// + /// \throw std::runtime_error If the test program definition is invalid or + /// if the test program does not exist. + void + callback_test_program(const std::string& interface, + const fs::path& raw_path, + const std::string& test_suite_override, + const model::metadata& metadata, + const config::tree& user_config, + scheduler::scheduler_handle& scheduler_handle) + { + if (raw_path.is_absolute()) + throw std::runtime_error(F("Got unexpected absolute path for test " + "program '%s'") % raw_path); + else if (raw_path.str() != raw_path.leaf_name()) + throw std::runtime_error(F("Test program '%s' cannot contain path " + "components") % raw_path); + + const fs::path path = relativize(_relative_filename.branch_path(), + raw_path); + + if (!fs::exists(_build_root / path)) + throw std::runtime_error(F("Non-existent test program '%s'") % + path); + + const std::string test_suite = get_test_suite(test_suite_override); + + _test_programs.push_back(model::test_program_ptr( + new scheduler::lazy_test_program(interface, path, _build_root, + test_suite, metadata, user_config, + scheduler_handle))); + } + + /// Callback for the Kyuafile test_suite() function. + /// + /// \post _version is set to the requested version. + /// + /// \param name Name of the test suite. + /// + /// \throw std::runtime_error If test_suite() has already been called. + void + callback_test_suite(const std::string& name) + { + if (_test_suite) + throw std::runtime_error("Can only call test_suite() once"); + _test_suite = utils::make_optional(name); + } + + /// Parses the Kyuafile. + /// + /// \pre Can only be invoked once. + /// + /// \return The collection of test programs defined by the Kyuafile. + /// + /// \throw load_error If there is any problem parsing the file. + const model::test_programs_vector& + parse(void) + { + PRE(_test_programs.empty()); + + const fs::path load_path = relativize(_source_root, _relative_filename); + try { + lutok::do_file(_state, load_path.str(), 0, 0, 0); + } catch (const std::runtime_error& e) { + // It is tempting to think that all of our various auxiliary + // functions above could raise load_error by themselves thus making + // this exception rewriting here unnecessary. Howver, that would + // not work because the helper functions above are executed within a + // Lua context, and we lose their type when they are propagated out + // of it. + throw engine::load_error(load_path, e.what()); + } + + if (!_version) + throw engine::load_error(load_path, "syntax() never called"); + + return _test_programs; + } +}; + + +/// Glue to invoke parser::callback_test_program() from Lua. +/// +/// This is a helper function for the various *_test_program() calls, as they +/// only differ in the interface of the defined test program. +/// +/// \pre state(-1) A table with the arguments that define the test program. The +/// special argument 'test_suite' provides an override to the global test suite +/// name. The rest of the arguments are part of the test program metadata. +/// \pre state(upvalue 1) String with the name of the interface. +/// \pre state(upvalue 2) User configuration with the per-test suite settings. +/// \pre state(upvalue 3) Scheduler context to run test programs in. +/// +/// \param state The Lua state that executed the function. +/// +/// \return Number of return values left on the Lua stack. +/// +/// \throw std::runtime_error If the arguments to the function are invalid. +static int +lua_generic_test_program(lutok::state& state) +{ + if (!state.is_string(state.upvalue_index(1))) + throw std::runtime_error("Found corrupt state for test_program " + "function"); + const std::string interface = state.to_string(state.upvalue_index(1)); + + if (!state.is_userdata(state.upvalue_index(2))) + throw std::runtime_error("Found corrupt state for test_program " + "function"); + const config::tree* user_config = *state.to_userdata< const config::tree* >( + state.upvalue_index(2)); + + if (!state.is_userdata(state.upvalue_index(3))) + throw std::runtime_error("Found corrupt state for test_program " + "function"); + scheduler::scheduler_handle* scheduler_handle = + *state.to_userdata< scheduler::scheduler_handle* >( + state.upvalue_index(3)); + + if (!state.is_table(-1)) + throw std::runtime_error( + F("%s_test_program expects a table of properties as its single " + "argument") % interface); + + scheduler::ensure_valid_interface(interface); + + lutok::stack_cleaner cleaner(state); + + state.push_string("name"); + state.get_table(-2); + if (!state.is_string(-1)) + throw std::runtime_error("Test program name not defined or not a " + "string"); + const fs::path path(state.to_string(-1)); + state.pop(1); + + state.push_string("test_suite"); + state.get_table(-2); + std::string test_suite; + if (state.is_nil(-1)) { + // Leave empty to use the global test-suite value. + } else if (state.is_string(-1)) { + test_suite = state.to_string(-1); + } else { + throw std::runtime_error(F("Found non-string value in the test_suite " + "property of test program '%s'") % path); + } + state.pop(1); + + model::metadata_builder mdbuilder; + state.push_nil(); + while (state.next(-2)) { + if (!state.is_string(-2)) + throw std::runtime_error(F("Found non-string metadata property " + "name in test program '%s'") % + path); + const std::string property = state.to_string(-2); + + if (property != "name" && property != "test_suite") { + std::string value; + if (state.is_boolean(-1)) { + value = F("%s") % state.to_boolean(-1); + } else if (state.is_number(-1)) { + value = F("%s") % state.to_integer(-1); + } else if (state.is_string(-1)) { + value = state.to_string(-1); + } else { + throw std::runtime_error( + F("Metadata property '%s' in test program '%s' cannot be " + "converted to a string") % property % path); + } + + mdbuilder.set_string(property, value); + } + + state.pop(1); + } + + parser::get_from_state(state)->callback_test_program( + interface, path, test_suite, mdbuilder.build(), *user_config, + *scheduler_handle); + return 0; +} + + +/// Glue to invoke parser::callback_current_kyuafile() from Lua. +/// +/// \param state The Lua state that executed the function. +/// +/// \return Number of return values left on the Lua stack. +static int +lua_current_kyuafile(lutok::state& state) +{ + state.push_string(parser::get_from_state(state)-> + callback_current_kyuafile().str()); + return 1; +} + + +/// Glue to invoke parser::callback_include() from Lua. +/// +/// \param state The Lua state that executed the function. +/// +/// \pre state(upvalue 1) User configuration with the per-test suite settings. +/// \pre state(upvalue 2) Scheduler context to run test programs in. +/// +/// \return Number of return values left on the Lua stack. +static int +lua_include(lutok::state& state) +{ + if (!state.is_userdata(state.upvalue_index(1))) + throw std::runtime_error("Found corrupt state for test_program " + "function"); + const config::tree* user_config = *state.to_userdata< const config::tree* >( + state.upvalue_index(1)); + + if (!state.is_userdata(state.upvalue_index(2))) + throw std::runtime_error("Found corrupt state for test_program " + "function"); + scheduler::scheduler_handle* scheduler_handle = + *state.to_userdata< scheduler::scheduler_handle* >( + state.upvalue_index(2)); + + parser::get_from_state(state)->callback_include( + fs::path(state.to_string(-1)), *user_config, *scheduler_handle); + return 0; +} + + +/// Glue to invoke parser::callback_syntax() from Lua. +/// +/// \pre state(-2) The syntax format name, if a v1 file. +/// \pre state(-1) The syntax format version. +/// +/// \param state The Lua state that executed the function. +/// +/// \return Number of return values left on the Lua stack. +static int +lua_syntax(lutok::state& state) +{ + if (!state.is_number(-1)) + throw std::runtime_error("Last argument to syntax must be a number"); + const int syntax_version = state.to_integer(-1); + + if (syntax_version == 1) { + if (state.get_top() != 2) + throw std::runtime_error("Version 1 files need two arguments to " + "syntax()"); + if (!state.is_string(-2) || state.to_string(-2) != "kyuafile") + throw std::runtime_error("First argument to syntax must be " + "'kyuafile' for version 1 files"); + } else { + if (state.get_top() != 1) + throw std::runtime_error("syntax() only takes one argument"); + } + + parser::get_from_state(state)->callback_syntax(syntax_version); + return 0; +} + + +/// Glue to invoke parser::callback_test_suite() from Lua. +/// +/// \param state The Lua state that executed the function. +/// +/// \return Number of return values left on the Lua stack. +static int +lua_test_suite(lutok::state& state) +{ + parser::get_from_state(state)->callback_test_suite(state.to_string(-1)); + return 0; +} + + +} // anonymous namespace + + +/// Constructs a kyuafile form initialized data. +/// +/// Use load() to parse a test suite configuration file and construct a +/// kyuafile object. +/// +/// \param source_root_ The root directory for the test suite represented by the +/// Kyuafile. In other words, the directory containing the first Kyuafile +/// processed. +/// \param build_root_ The root directory for the test programs themselves. In +/// general, this will be the same as source_root_. If different, the +/// specified directory must follow the exact same layout of source_root_. +/// \param tps_ Collection of test programs that belong to this test suite. +engine::kyuafile::kyuafile(const fs::path& source_root_, + const fs::path& build_root_, + const model::test_programs_vector& tps_) : + _source_root(source_root_), + _build_root(build_root_), + _test_programs(tps_) +{ +} + + +/// Destructor. +engine::kyuafile::~kyuafile(void) +{ +} + + +/// Parses a test suite configuration file. +/// +/// \param file The file to parse. +/// \param user_build_root If not none, specifies a path to a directory +/// containing the test programs themselves. The layout of the build root +/// must match the layout of the source root (which is just the directory +/// from which the Kyuafile is being read). +/// \param user_config User configuration holding any test suite properties +/// to be passed to the list operation. +/// \param scheduler_handle The scheduler context to use for loading the test +/// case lists. +/// +/// \return High-level representation of the configuration file. +/// +/// \throw load_error If there is any problem loading the file. This includes +/// file access errors and syntax errors. +engine::kyuafile +engine::kyuafile::load(const fs::path& file, + const optional< fs::path > user_build_root, + const config::tree& user_config, + scheduler::scheduler_handle& scheduler_handle) +{ + const fs::path source_root_ = file.branch_path(); + const fs::path build_root_ = user_build_root ? + user_build_root.get() : source_root_; + + // test_program.absolute_path() uses the current work directory and that + // fails to resolve the correct path once we have used chdir to enter the + // test work directory. To prevent this causing issues down the road, + // force the build root to be absolute so that absolute_path() does not + // need to rely on the current work directory. + const fs::path abs_build_root = build_root_.is_absolute() ? + build_root_ : build_root_.to_absolute(); + + return kyuafile(source_root_, build_root_, + parser(source_root_, abs_build_root, + fs::path(file.leaf_name()), user_config, + scheduler_handle).parse()); +} + + +/// Gets the root directory of the test suite. +/// +/// \return A path. +const fs::path& +engine::kyuafile::source_root(void) const +{ + return _source_root; +} + + +/// Gets the root directory of the test programs. +/// +/// \return A path. +const fs::path& +engine::kyuafile::build_root(void) const +{ + return _build_root; +} + + +/// Gets the collection of test programs that belong to this test suite. +/// +/// \return Collection of test program executable names. +const model::test_programs_vector& +engine::kyuafile::test_programs(void) const +{ + return _test_programs; +} |