diff options
Diffstat (limited to 'cli')
41 files changed, 6787 insertions, 0 deletions
diff --git a/cli/Kyuafile b/cli/Kyuafile new file mode 100644 index 000000000000..f5b797d760c3 --- /dev/null +++ b/cli/Kyuafile @@ -0,0 +1,14 @@ +syntax(2) + +test_suite("kyua") + +atf_test_program{name="cmd_about_test"} +atf_test_program{name="cmd_config_test"} +atf_test_program{name="cmd_db_exec_test"} +atf_test_program{name="cmd_debug_test"} +atf_test_program{name="cmd_help_test"} +atf_test_program{name="cmd_list_test"} +atf_test_program{name="cmd_test_test"} +atf_test_program{name="common_test"} +atf_test_program{name="config_test"} +atf_test_program{name="main_test"} diff --git a/cli/Makefile.am.inc b/cli/Makefile.am.inc new file mode 100644 index 000000000000..27872088a1b7 --- /dev/null +++ b/cli/Makefile.am.inc @@ -0,0 +1,123 @@ +# 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. + +CLI_CFLAGS = $(DRIVERS_CFLAGS) +CLI_LIBS = libcli.a $(DRIVERS_LIBS) + +noinst_LIBRARIES += libcli.a +libcli_a_SOURCES = cli/cmd_about.cpp +libcli_a_SOURCES += cli/cmd_about.hpp +libcli_a_SOURCES += cli/cmd_config.cpp +libcli_a_SOURCES += cli/cmd_config.hpp +libcli_a_SOURCES += cli/cmd_db_exec.cpp +libcli_a_SOURCES += cli/cmd_db_exec.hpp +libcli_a_SOURCES += cli/cmd_db_migrate.cpp +libcli_a_SOURCES += cli/cmd_db_migrate.hpp +libcli_a_SOURCES += cli/cmd_debug.cpp +libcli_a_SOURCES += cli/cmd_debug.hpp +libcli_a_SOURCES += cli/cmd_help.cpp +libcli_a_SOURCES += cli/cmd_help.hpp +libcli_a_SOURCES += cli/cmd_list.cpp +libcli_a_SOURCES += cli/cmd_list.hpp +libcli_a_SOURCES += cli/cmd_report.cpp +libcli_a_SOURCES += cli/cmd_report.hpp +libcli_a_SOURCES += cli/cmd_report_html.cpp +libcli_a_SOURCES += cli/cmd_report_html.hpp +libcli_a_SOURCES += cli/cmd_report_junit.cpp +libcli_a_SOURCES += cli/cmd_report_junit.hpp +libcli_a_SOURCES += cli/cmd_test.cpp +libcli_a_SOURCES += cli/cmd_test.hpp +libcli_a_SOURCES += cli/common.cpp +libcli_a_SOURCES += cli/common.hpp +libcli_a_SOURCES += cli/common.ipp +libcli_a_SOURCES += cli/config.cpp +libcli_a_SOURCES += cli/config.hpp +libcli_a_SOURCES += cli/main.cpp +libcli_a_SOURCES += cli/main.hpp +libcli_a_CPPFLAGS = -DKYUA_CONFDIR="\"$(kyua_confdir)\"" +libcli_a_CPPFLAGS += -DKYUA_DOCDIR="\"$(docdir)\"" +libcli_a_CPPFLAGS += -DKYUA_MISCDIR="\"$(miscdir)\"" +libcli_a_CPPFLAGS += $(DRIVERS_CFLAGS) +libcli_a_LIBADD = libutils.a + +if WITH_ATF +tests_clidir = $(pkgtestsdir)/cli + +tests_cli_DATA = cli/Kyuafile +EXTRA_DIST += $(tests_cli_DATA) + +tests_cli_PROGRAMS = cli/cmd_about_test +cli_cmd_about_test_SOURCES = cli/cmd_about_test.cpp +cli_cmd_about_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_about_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_config_test +cli_cmd_config_test_SOURCES = cli/cmd_config_test.cpp +cli_cmd_config_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_config_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_db_exec_test +cli_cmd_db_exec_test_SOURCES = cli/cmd_db_exec_test.cpp +cli_cmd_db_exec_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_db_exec_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_debug_test +cli_cmd_debug_test_SOURCES = cli/cmd_debug_test.cpp +cli_cmd_debug_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_debug_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_help_test +cli_cmd_help_test_SOURCES = cli/cmd_help_test.cpp +cli_cmd_help_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_help_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_list_test +cli_cmd_list_test_SOURCES = cli/cmd_list_test.cpp +cli_cmd_list_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_list_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/cmd_test_test +cli_cmd_test_test_SOURCES = cli/cmd_test_test.cpp +cli_cmd_test_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_cmd_test_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/common_test +cli_common_test_SOURCES = cli/common_test.cpp +cli_common_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_common_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/config_test +cli_config_test_SOURCES = cli/config_test.cpp +cli_config_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_config_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) + +tests_cli_PROGRAMS += cli/main_test +cli_main_test_SOURCES = cli/main_test.cpp +cli_main_test_CXXFLAGS = $(CLI_CFLAGS) $(ATF_CXX_CFLAGS) +cli_main_test_LDADD = $(CLI_LIBS) $(ATF_CXX_LIBS) +endif diff --git a/cli/cmd_about.cpp b/cli/cmd_about.cpp new file mode 100644 index 000000000000..f2b3f99e0ada --- /dev/null +++ b/cli/cmd_about.cpp @@ -0,0 +1,160 @@ +// 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 "cli/cmd_about.hpp" + +#include <cstdlib> +#include <fstream> +#include <utility> +#include <vector> + +#include "cli/common.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/sanity.hpp" +#include "utils/text/regex.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; +namespace text = utils::text; + +using cli::cmd_about; + + +namespace { + + +/// Print the contents of a document. +/// +/// If the file cannot be opened for whatever reason, an error message is +/// printed to the output of the program instead of the contents of the file. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param file The file to print. +/// \param filter_re Regular expression to match the lines to print. If empty, +/// no filtering is applied. +/// +/// \return True if the file was printed, false otherwise. +static bool +cat_file(cmdline::ui* ui, const fs::path& file, + const std::string& filter_re = "") +{ + std::ifstream input(file.c_str()); + if (!input) { + ui->err(F("Failed to open %s") % file); + return false; + } + + std::string line; + if (filter_re.empty()) { + while (std::getline(input, line).good()) { + ui->out(line); + } + } else { + const text::regex filter = text::regex::compile(filter_re, 0); + while (std::getline(input, line).good()) { + if (filter.match(line)) { + ui->out(line); + } + } + } + input.close(); + return true; +} + + +} // anonymous namespace + + +/// Default constructor for cmd_about. +cmd_about::cmd_about(void) : cli_command( + "about", "[authors|license|version]", 0, 1, + "Shows detailed authors and contributors; license; and version information") +{ +} + + +/// Entry point for the "about" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if any of the necessary documents cannot be +/// opened. +int +cmd_about::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + const fs::path docdir(utils::getenv_with_default( + "KYUA_DOCDIR", KYUA_DOCDIR)); + + bool success = true; + + static const char* list_re = "^\\* "; + + if (cmdline.arguments().empty()) { + ui->out(PACKAGE " (" PACKAGE_NAME ") " PACKAGE_VERSION); + ui->out(""); + ui->out("License terms:"); + ui->out(""); + success &= cat_file(ui, docdir / "LICENSE"); + ui->out(""); + ui->out("Brought to you by:"); + ui->out(""); + success &= cat_file(ui, docdir / "AUTHORS", list_re); + ui->out(""); + success &= cat_file(ui, docdir / "CONTRIBUTORS", list_re); + ui->out(""); + ui->out(F("Homepage: %s") % PACKAGE_URL); + } else { + const std::string& topic = cmdline.arguments()[0]; + + if (topic == "authors") { + success &= cat_file(ui, docdir / "AUTHORS", list_re); + success &= cat_file(ui, docdir / "CONTRIBUTORS", list_re); + } else if (topic == "license") { + success &= cat_file(ui, docdir / "LICENSE"); + } else if (topic == "version") { + write_version_header(ui); + } else { + throw cmdline::usage_error(F("Invalid about topic '%s'") % topic); + } + } + + return success ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/cli/cmd_about.hpp b/cli/cmd_about.hpp new file mode 100644 index 000000000000..2d1ed57a498b --- /dev/null +++ b/cli/cmd_about.hpp @@ -0,0 +1,57 @@ +// 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 cli/cmd_about.hpp +/// Provides the cmd_about class. + +#if !defined(CLI_CMD_ABOUT_HPP) +#define CLI_CMD_ABOUT_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "about" subcommand. +class cmd_about : public cli_command +{ + /// Path to the directory containing the distribution documents. + const std::string _docdir; + +public: + cmd_about(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_ABOUT_HPP) diff --git a/cli/cmd_about_test.cpp b/cli/cmd_about_test.cpp new file mode 100644 index 000000000000..da75db3b3871 --- /dev/null +++ b/cli/cmd_about_test.cpp @@ -0,0 +1,306 @@ +// 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 "cli/cmd_about.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +#include <cstdlib> + +#include <atf-c++.hpp> + +#include "cli/common.ipp" +#include "engine/config.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/config/tree.ipp" +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace fs = utils::fs; + +using cli::cmd_about; + + +ATF_TEST_CASE_WITHOUT_HEAD(all_topics__ok); +ATF_TEST_CASE_BODY(all_topics__ok) +{ + cmdline::args_vector args; + args.push_back("about"); + + fs::mkdir(fs::path("fake-docs"), 0755); + atf::utils::create_file("fake-docs/AUTHORS", + "Content of AUTHORS\n" + "* First author\n" + " * garbage\n" + "* Second author\n"); + atf::utils::create_file("fake-docs/CONTRIBUTORS", + "Content of CONTRIBUTORS\n" + "* First contributor\n" + " * garbage\n" + "* Second contributor\n"); + atf::utils::create_file("fake-docs/LICENSE", "Content of LICENSE\n"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_NAME, ui.out_log()[0])); + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_VERSION, ui.out_log()[0])); + + ATF_REQUIRE(!atf::utils::grep_collection("Content of AUTHORS", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* First author", ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("garbage", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* Second author", ui.out_log())); + + ATF_REQUIRE(!atf::utils::grep_collection("Content of CONTRIBUTORS", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* First contributor", + ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("garbage", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* Second contributor", + ui.out_log())); + + ATF_REQUIRE(atf::utils::grep_collection("Content of LICENSE", + ui.out_log())); + + ATF_REQUIRE(atf::utils::grep_collection("Homepage", ui.out_log())); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(all_topics__missing_docs); +ATF_TEST_CASE_BODY(all_topics__missing_docs) +{ + cmdline::args_vector args; + args.push_back("about"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_FAILURE, cmd.main(&ui, args, engine::default_config())); + + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_NAME, ui.out_log()[0])); + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_VERSION, ui.out_log()[0])); + + ATF_REQUIRE(atf::utils::grep_collection("Homepage", ui.out_log())); + + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*AUTHORS", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*CONTRIBUTORS", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*LICENSE", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(topic_authors__ok); +ATF_TEST_CASE_BODY(topic_authors__ok) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("authors"); + + fs::mkdir(fs::path("fake-docs"), 0755); + atf::utils::create_file("fake-docs/AUTHORS", + "Content of AUTHORS\n" + "* First author\n" + " * garbage\n" + "* Second author\n"); + atf::utils::create_file("fake-docs/CONTRIBUTORS", + "Content of CONTRIBUTORS\n" + "* First contributor\n" + " * garbage\n" + "* Second contributor\n"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(!atf::utils::grep_string(PACKAGE_NAME, ui.out_log()[0])); + + ATF_REQUIRE(!atf::utils::grep_collection("Content of AUTHORS", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* First author", ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("garbage", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* Second author", ui.out_log())); + + ATF_REQUIRE(!atf::utils::grep_collection("Content of CONTRIBUTORS", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* First contributor", + ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("garbage", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("\\* Second contributor", + ui.out_log())); + + ATF_REQUIRE(!atf::utils::grep_collection("LICENSE", ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("Homepage", ui.out_log())); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(topic_authors__missing_doc); +ATF_TEST_CASE_BODY(topic_authors__missing_doc) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("authors"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_FAILURE, cmd.main(&ui, args, engine::default_config())); + + ATF_REQUIRE_EQ(0, ui.out_log().size()); + + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*AUTHORS", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*CONTRIBUTORS", + ui.err_log())); + ATF_REQUIRE(!atf::utils::grep_collection("Failed to open.*LICENSE", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(topic_license__ok); +ATF_TEST_CASE_BODY(topic_license__ok) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("license"); + + fs::mkdir(fs::path("fake-docs"), 0755); + atf::utils::create_file("fake-docs/LICENSE", "Content of LICENSE\n"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(!atf::utils::grep_string(PACKAGE_NAME, ui.out_log()[0])); + ATF_REQUIRE(!atf::utils::grep_collection("AUTHORS", ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("CONTRIBUTORS", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("Content of LICENSE", + ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection("Homepage", ui.out_log())); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(topic_license__missing_doc); +ATF_TEST_CASE_BODY(topic_license__missing_doc) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("license"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_FAILURE, cmd.main(&ui, args, engine::default_config())); + + ATF_REQUIRE_EQ(0, ui.out_log().size()); + + ATF_REQUIRE(!atf::utils::grep_collection("Failed to open.*AUTHORS", + ui.err_log())); + ATF_REQUIRE(!atf::utils::grep_collection("Failed to open.*CONTRIBUTORS", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Failed to open.*LICENSE", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(topic_version__ok); +ATF_TEST_CASE_BODY(topic_version__ok) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("version"); + + utils::setenv("KYUA_DOCDIR", "fake-docs"); + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_NAME, ui.out_log()[0])); + ATF_REQUIRE(atf::utils::grep_string(PACKAGE_VERSION, ui.out_log()[0])); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_args); +ATF_TEST_CASE_BODY(invalid_args) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("first"); + args.push_back("second"); + + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Too many arguments", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_topic); +ATF_TEST_CASE_BODY(invalid_topic) +{ + cmdline::args_vector args; + args.push_back("about"); + args.push_back("foo"); + + cmd_about cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Invalid about topic 'foo'", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, all_topics__ok); + ATF_ADD_TEST_CASE(tcs, all_topics__missing_docs); + ATF_ADD_TEST_CASE(tcs, topic_authors__ok); + ATF_ADD_TEST_CASE(tcs, topic_authors__missing_doc); + ATF_ADD_TEST_CASE(tcs, topic_license__ok); + ATF_ADD_TEST_CASE(tcs, topic_license__missing_doc); + ATF_ADD_TEST_CASE(tcs, topic_version__ok); + ATF_ADD_TEST_CASE(tcs, invalid_args); + ATF_ADD_TEST_CASE(tcs, invalid_topic); +} diff --git a/cli/cmd_config.cpp b/cli/cmd_config.cpp new file mode 100644 index 000000000000..947449aacc2d --- /dev/null +++ b/cli/cmd_config.cpp @@ -0,0 +1,122 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_config.hpp" + +#include <cstdlib> + +#include "cli/common.ipp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/config/tree.ipp" +#include "utils/format/macros.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; + +using cli::cmd_config; + + +namespace { + + +/// Prints all configuration variables. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param properties The key/value map representing all the configuration +/// variables. +/// +/// \return 0 for success. +static int +print_all(cmdline::ui* ui, const config::properties_map& properties) +{ + for (config::properties_map::const_iterator iter = properties.begin(); + iter != properties.end(); iter++) + ui->out(F("%s = %s") % (*iter).first % (*iter).second); + return EXIT_SUCCESS; +} + + +/// Prints the configuration variables that the user requests. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param properties The key/value map representing all the configuration +/// variables. +/// \param filters The names of the configuration variables to print. +/// +/// \return 0 if all specified filters are valid; 1 otherwise. +static int +print_some(cmdline::ui* ui, const config::properties_map& properties, + const cmdline::args_vector& filters) +{ + bool ok = true; + + for (cmdline::args_vector::const_iterator iter = filters.begin(); + iter != filters.end(); iter++) { + const config::properties_map::const_iterator match = + properties.find(*iter); + if (match == properties.end()) { + cmdline::print_warning(ui, F("'%s' is not defined.") % *iter); + ok = false; + } else + ui->out(F("%s = %s") % (*match).first % (*match).second); + } + + return ok ? EXIT_SUCCESS : EXIT_FAILURE; +} + + +} // anonymous namespace + + +/// Default constructor for cmd_config. +cmd_config::cmd_config(void) : cli_command( + "config", "[variable1 .. variableN]", 0, -1, + "Inspects the values of configuration variables") +{ +} + + +/// Entry point for the "config" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// \param user_config The runtime configuration of the program. +/// +/// \return 0 if everything is OK, 1 if any of the necessary documents cannot be +/// opened. +int +cmd_config::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& user_config) +{ + const config::properties_map properties = user_config.all_properties(); + if (cmdline.arguments().empty()) + return print_all(ui, properties); + else + return print_some(ui, properties, cmdline.arguments()); +} diff --git a/cli/cmd_config.hpp b/cli/cmd_config.hpp new file mode 100644 index 000000000000..42f5abd90c28 --- /dev/null +++ b/cli/cmd_config.hpp @@ -0,0 +1,54 @@ +// 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 cli/cmd_config.hpp +/// Provides the cmd_config class. + +#if !defined(CLI_CMD_CONFIG_HPP) +#define CLI_CMD_CONFIG_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "config" subcommand. +class cmd_config : public cli_command +{ +public: + cmd_config(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_CONFIG_HPP) diff --git a/cli/cmd_config_test.cpp b/cli/cmd_config_test.cpp new file mode 100644 index 000000000000..f084f99bb90a --- /dev/null +++ b/cli/cmd_config_test.cpp @@ -0,0 +1,144 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_config.hpp" + +#include <cstdlib> + +#include <atf-c++.hpp> + +#include "cli/common.ipp" +#include "engine/config.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/config/tree.ipp" +#include "utils/optional.ipp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; + +using cli::cmd_config; +using utils::none; + + +namespace { + + +/// Instantiates a fake user configuration for testing purposes. +/// +/// The user configuration is populated with a collection of test-suite +/// properties and some hardcoded values for the generic configuration options. +/// +/// \return A new user configuration object. +static config::tree +fake_config(void) +{ + config::tree user_config = engine::default_config(); + user_config.set_string("architecture", "the-architecture"); + user_config.set_string("parallelism", "128"); + user_config.set_string("platform", "the-platform"); + //user_config.set_string("unprivileged_user", ""); + user_config.set_string("test_suites.foo.bar", "first"); + user_config.set_string("test_suites.foo.baz", "second"); + return user_config; +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(all); +ATF_TEST_CASE_BODY(all) +{ + cmdline::args_vector args; + args.push_back("config"); + + cmd_config cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, fake_config())); + + ATF_REQUIRE_EQ(5, ui.out_log().size()); + ATF_REQUIRE_EQ("architecture = the-architecture", ui.out_log()[0]); + ATF_REQUIRE_EQ("parallelism = 128", ui.out_log()[1]); + ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[2]); + ATF_REQUIRE_EQ("test_suites.foo.bar = first", ui.out_log()[3]); + ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[4]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some__ok); +ATF_TEST_CASE_BODY(some__ok) +{ + cmdline::args_vector args; + args.push_back("config"); + args.push_back("platform"); + args.push_back("test_suites.foo.baz"); + + cmd_config cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, fake_config())); + + ATF_REQUIRE_EQ(2, ui.out_log().size()); + ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[0]); + ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[1]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(some__fail); +ATF_TEST_CASE_BODY(some__fail) +{ + cmdline::args_vector args; + args.push_back("config"); + args.push_back("platform"); + args.push_back("unknown"); + args.push_back("test_suites.foo.baz"); + + cmdline::init("progname"); + + cmd_config cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_FAILURE, cmd.main(&ui, args, fake_config())); + + ATF_REQUIRE_EQ(2, ui.out_log().size()); + ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[0]); + ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[1]); + ATF_REQUIRE_EQ(1, ui.err_log().size()); + ATF_REQUIRE(atf::utils::grep_string("unknown.*not defined", + ui.err_log()[0])); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, all); + ATF_ADD_TEST_CASE(tcs, some__ok); + ATF_ADD_TEST_CASE(tcs, some__fail); +} diff --git a/cli/cmd_db_exec.cpp b/cli/cmd_db_exec.cpp new file mode 100644 index 000000000000..54304e6643de --- /dev/null +++ b/cli/cmd_db_exec.cpp @@ -0,0 +1,200 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_db_exec.hpp" + +#include <algorithm> +#include <cstdlib> +#include <iterator> +#include <sstream> +#include <string> + +#include "cli/common.ipp" +#include "store/exceptions.hpp" +#include "store/layout.hpp" +#include "store/read_backend.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/sanity.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/exceptions.hpp" +#include "utils/sqlite/statement.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; +namespace layout = store::layout; +namespace sqlite = utils::sqlite; + +using cli::cmd_db_exec; + + +namespace { + + +/// Concatenates a vector into a string using ' ' as a separator. +/// +/// \param args The objects to join. This cannot be empty. +/// +/// \return The concatenation of all the objects in the set. +static std::string +flatten_args(const cmdline::args_vector& args) +{ + std::ostringstream output; + std::copy(args.begin(), args.end(), + std::ostream_iterator< std::string >(output, " ")); + + std::string result = output.str(); + result.erase(result.end() - 1); + return result; +} + + +} // anonymous namespace + + +/// Formats a particular cell of a statement result. +/// +/// \param stmt The statement whose cell to format. +/// \param index The index of the cell to format. +/// +/// \return A textual representation of the cell. +std::string +cli::format_cell(sqlite::statement& stmt, const int index) +{ + switch (stmt.column_type(index)) { + case sqlite::type_blob: { + const sqlite::blob blob = stmt.column_blob(index); + return F("BLOB of %s bytes") % blob.size; + } + + case sqlite::type_float: + return F("%s") % stmt.column_double(index); + + case sqlite::type_integer: + return F("%s") % stmt.column_int64(index); + + case sqlite::type_null: + return "NULL"; + + case sqlite::type_text: + return stmt.column_text(index); + } + + UNREACHABLE; +} + + +/// Formats the column names of a statement for output as CSV. +/// +/// \param stmt The statement whose columns to format. +/// +/// \return A comma-separated list of column names. +std::string +cli::format_headers(sqlite::statement& stmt) +{ + std::string output; + int i = 0; + for (; i < stmt.column_count() - 1; ++i) + output += stmt.column_name(i) + ','; + output += stmt.column_name(i); + return output; +} + + +/// Formats a row of a statement for output as CSV. +/// +/// \param stmt The statement whose current row to format. +/// +/// \return A comma-separated list of values. +std::string +cli::format_row(sqlite::statement& stmt) +{ + std::string output; + int i = 0; + for (; i < stmt.column_count() - 1; ++i) + output += cli::format_cell(stmt, i) + ','; + output += cli::format_cell(stmt, i); + return output; +} + + +/// Default constructor for cmd_db_exec. +cmd_db_exec::cmd_db_exec(void) : cli_command( + "db-exec", "sql_statement", 1, -1, + "Executes an arbitrary SQL statement in a results file and prints " + "the resulting table") +{ + add_option(results_file_open_option); + add_option(cmdline::bool_option("no-headers", "Do not show headers in the " + "output table")); +} + + +/// Entry point for the "db-exec" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if the statement is invalid or if there is +/// any other problem. +int +cmd_db_exec::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + try { + const fs::path results_file = layout::find_results( + results_file_open(cmdline)); + + // TODO(jmmv): Shouldn't be using store::detail here... + sqlite::database db = store::detail::open_and_setup( + results_file, sqlite::open_readwrite); + sqlite::statement stmt = db.create_statement( + flatten_args(cmdline.arguments())); + + if (stmt.step()) { + if (!cmdline.has_option("no-headers")) + ui->out(cli::format_headers(stmt)); + do + ui->out(cli::format_row(stmt)); + while (stmt.step()); + } + + return EXIT_SUCCESS; + } catch (const sqlite::error& e) { + cmdline::print_error(ui, F("SQLite error: %s.") % e.what()); + return EXIT_FAILURE; + } catch (const store::error& e) { + cmdline::print_error(ui, F("%s.") % e.what()); + return EXIT_FAILURE; + } +} diff --git a/cli/cmd_db_exec.hpp b/cli/cmd_db_exec.hpp new file mode 100644 index 000000000000..18aa16108553 --- /dev/null +++ b/cli/cmd_db_exec.hpp @@ -0,0 +1,61 @@ +// 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 cli/cmd_db_exec.hpp +/// Provides the cmd_db_exec class. + +#if !defined(CLI_CMD_DB_EXEC_HPP) +#define CLI_CMD_DB_EXEC_HPP + +#include <string> + +#include "cli/common.hpp" +#include "utils/sqlite/statement_fwd.hpp" + +namespace cli { + + +std::string format_cell(utils::sqlite::statement&, const int); +std::string format_headers(utils::sqlite::statement&); +std::string format_row(utils::sqlite::statement&); + + +/// Implementation of the "db-exec" subcommand. +class cmd_db_exec : public cli_command +{ +public: + cmd_db_exec(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + +#endif // !defined(CLI_CMD_DB_EXEC_HPP) diff --git a/cli/cmd_db_exec_test.cpp b/cli/cmd_db_exec_test.cpp new file mode 100644 index 000000000000..1bf6b2e074a9 --- /dev/null +++ b/cli/cmd_db_exec_test.cpp @@ -0,0 +1,165 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_db_exec.hpp" + +#include <cstring> + +#include <atf-c++.hpp> + +#include "utils/format/macros.hpp" +#include "utils/sqlite/database.hpp" +#include "utils/sqlite/statement.ipp" + +namespace sqlite = utils::sqlite; + + +namespace { + + +/// Performs a test for the cli::format_cell() function. +/// +/// \tparam Cell The type of the value to insert into the test column. +/// \param column_type The SQL type of the test column. +/// \param value The value to insert into the test column. +/// \param exp_value The expected return value of cli::format_cell(). +template< class Cell > +static void +do_format_cell_test(const std::string column_type, + const Cell& value, const std::string& exp_value) +{ + sqlite::database db = sqlite::database::in_memory(); + + sqlite::statement create = db.create_statement( + F("CREATE TABLE test (column %s)") % column_type); + create.step_without_results(); + + sqlite::statement insert = db.create_statement( + "INSERT INTO test (column) VALUES (:column)"); + insert.bind(":column", value); + insert.step_without_results(); + + sqlite::statement query = db.create_statement("SELECT * FROM test"); + ATF_REQUIRE(query.step()); + ATF_REQUIRE_EQ(exp_value, cli::format_cell(query, 0)); + ATF_REQUIRE(!query.step()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(format_cell__blob); +ATF_TEST_CASE_BODY(format_cell__blob) +{ + const char* contents = "Some random contents"; + do_format_cell_test( + "BLOB", sqlite::blob(contents, std::strlen(contents)), + F("BLOB of %s bytes") % strlen(contents)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_cell__float); +ATF_TEST_CASE_BODY(format_cell__float) +{ + do_format_cell_test("FLOAT", 3.5, "3.5"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_cell__integer); +ATF_TEST_CASE_BODY(format_cell__integer) +{ + do_format_cell_test("INTEGER", 123456, "123456"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_cell__null); +ATF_TEST_CASE_BODY(format_cell__null) +{ + do_format_cell_test("TEXT", sqlite::null(), "NULL"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_cell__text); +ATF_TEST_CASE_BODY(format_cell__text) +{ + do_format_cell_test("TEXT", "Hello, world", "Hello, world"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_headers); +ATF_TEST_CASE_BODY(format_headers) +{ + sqlite::database db = sqlite::database::in_memory(); + + sqlite::statement create = db.create_statement( + "CREATE TABLE test (c1 TEXT, c2 TEXT, c3 TEXT)"); + create.step_without_results(); + + sqlite::statement query = db.create_statement( + "SELECT c1, c2, c3 AS c3bis FROM test"); + ATF_REQUIRE_EQ("c1,c2,c3bis", cli::format_headers(query)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_row); +ATF_TEST_CASE_BODY(format_row) +{ + sqlite::database db = sqlite::database::in_memory(); + + sqlite::statement create = db.create_statement( + "CREATE TABLE test (c1 TEXT, c2 BLOB)"); + create.step_without_results(); + + const char* memory = "BLOB contents"; + sqlite::statement insert = db.create_statement( + "INSERT INTO test VALUES (:v1, :v2)"); + insert.bind(":v1", "A string"); + insert.bind(":v2", sqlite::blob(memory, std::strlen(memory))); + insert.step_without_results(); + + sqlite::statement query = db.create_statement("SELECT * FROM test"); + query.step(); + ATF_REQUIRE_EQ( + (F("A string,BLOB of %s bytes") % std::strlen(memory)).str(), + cli::format_row(query)); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, format_cell__blob); + ATF_ADD_TEST_CASE(tcs, format_cell__float); + ATF_ADD_TEST_CASE(tcs, format_cell__integer); + ATF_ADD_TEST_CASE(tcs, format_cell__null); + ATF_ADD_TEST_CASE(tcs, format_cell__text); + + ATF_ADD_TEST_CASE(tcs, format_headers); + + ATF_ADD_TEST_CASE(tcs, format_row); +} diff --git a/cli/cmd_db_migrate.cpp b/cli/cmd_db_migrate.cpp new file mode 100644 index 000000000000..c6076c6afa4d --- /dev/null +++ b/cli/cmd_db_migrate.cpp @@ -0,0 +1,82 @@ +// Copyright 2013 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_db_migrate.hpp" + +#include <cstdlib> + +#include "cli/common.ipp" +#include "store/exceptions.hpp" +#include "store/layout.hpp" +#include "store/migrate.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/ui.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; +namespace layout = store::layout; + +using cli::cmd_db_migrate; + + +/// Default constructor for cmd_db_migrate. +cmd_db_migrate::cmd_db_migrate(void) : cli_command( + "db-migrate", "", 0, 0, + "Upgrades the schema of an existing results file to the currently " + "implemented version. A backup of the results file is created, but " + "this operation is not reversible") +{ + add_option(results_file_open_option); +} + + +/// Entry point for the "db-migrate" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if the statement is invalid or if there is +/// any other problem. +int +cmd_db_migrate::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + try { + const fs::path results_file = layout::find_results( + results_file_open(cmdline)); + store::migrate_schema(results_file); + return EXIT_SUCCESS; + } catch (const store::error& e) { + cmdline::print_error(ui, F("Migration failed: %s.") % e.what()); + return EXIT_FAILURE; + } +} diff --git a/cli/cmd_db_migrate.hpp b/cli/cmd_db_migrate.hpp new file mode 100644 index 000000000000..ebbe2b8a4ba4 --- /dev/null +++ b/cli/cmd_db_migrate.hpp @@ -0,0 +1,54 @@ +// Copyright 2013 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 cli/cmd_db_migrate.hpp +/// Provides the cmd_db_migrate class. + +#if !defined(CLI_CMD_DB_MIGRATE_HPP) +#define CLI_CMD_DB_MIGRATE_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "db-migrate" subcommand. +class cmd_db_migrate : public cli_command +{ +public: + cmd_db_migrate(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_DB_MIGRATE_HPP) diff --git a/cli/cmd_debug.cpp b/cli/cmd_debug.cpp new file mode 100644 index 000000000000..b7a29b7ab804 --- /dev/null +++ b/cli/cmd_debug.cpp @@ -0,0 +1,94 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_debug.hpp" + +#include <cstdlib> + +#include "cli/common.ipp" +#include "drivers/debug_test.hpp" +#include "engine/filters.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/format/macros.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; + +using cli::cmd_debug; + + +/// Default constructor for cmd_debug. +cmd_debug::cmd_debug(void) : cli_command( + "debug", "test_case", 1, 1, + "Executes a single test case providing facilities for debugging") +{ + add_option(build_root_option); + add_option(kyuafile_option); + + add_option(cmdline::path_option( + "stdout", "Where to direct the standard output of the test case", + "path", "/dev/stdout")); + + add_option(cmdline::path_option( + "stderr", "Where to direct the standard error of the test case", + "path", "/dev/stderr")); +} + + +/// Entry point for the "debug" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// \param user_config The runtime debuguration of the program. +/// +/// \return 0 if everything is OK, 1 if any of the necessary documents cannot be +/// opened. +int +cmd_debug::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& user_config) +{ + const std::string& test_case_name = cmdline.arguments()[0]; + if (test_case_name.find(':') == std::string::npos) + throw cmdline::usage_error(F("'%s' is not a test case identifier " + "(missing ':'?)") % test_case_name); + const engine::test_filter filter = engine::test_filter::parse( + test_case_name); + + const drivers::debug_test::result result = drivers::debug_test::drive( + kyuafile_path(cmdline), build_root_path(cmdline), filter, user_config, + cmdline.get_option< cmdline::path_option >("stdout"), + cmdline.get_option< cmdline::path_option >("stderr")); + + ui->out(F("%s -> %s") % cli::format_test_case_id(result.test_case) % + cli::format_result(result.test_result)); + + return result.test_result.good() ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/cli/cmd_debug.hpp b/cli/cmd_debug.hpp new file mode 100644 index 000000000000..2d9e8dee1797 --- /dev/null +++ b/cli/cmd_debug.hpp @@ -0,0 +1,54 @@ +// 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 cli/cmd_debug.hpp +/// Provides the cmd_debug class. + +#if !defined(CLI_CMD_DEBUG_HPP) +#define CLI_CMD_DEBUG_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "debug" subcommand. +class cmd_debug : public cli_command +{ +public: + cmd_debug(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_DEBUG_HPP) diff --git a/cli/cmd_debug_test.cpp b/cli/cmd_debug_test.cpp new file mode 100644 index 000000000000..28137e028962 --- /dev/null +++ b/cli/cmd_debug_test.cpp @@ -0,0 +1,82 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_debug.hpp" + +#include <stdexcept> + +#include <atf-c++.hpp> + +#include "cli/common.ipp" +#include "engine/config.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/config/tree.ipp" + +namespace cmdline = utils::cmdline; + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_filter); +ATF_TEST_CASE_BODY(invalid_filter) +{ + cmdline::args_vector args; + args.push_back("debug"); + args.push_back("incorrect:"); + + cli::cmd_debug cmd; + cmdline::ui_mock ui; + // TODO(jmmv): This error should really be cmdline::usage_error. + ATF_REQUIRE_THROW_RE(std::runtime_error, "Test case.*'incorrect:'.*empty", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(filter_without_test_case); +ATF_TEST_CASE_BODY(filter_without_test_case) +{ + cmdline::args_vector args; + args.push_back("debug"); + args.push_back("program"); + + cli::cmd_debug cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::error, "'program'.*not a test case", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, invalid_filter); + ATF_ADD_TEST_CASE(tcs, filter_without_test_case); +} diff --git a/cli/cmd_help.cpp b/cli/cmd_help.cpp new file mode 100644 index 000000000000..9ebe6f50c852 --- /dev/null +++ b/cli/cmd_help.cpp @@ -0,0 +1,250 @@ +// 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 "cli/cmd_help.hpp" + +#include <algorithm> +#include <cstdlib> + +#include "cli/common.ipp" +#include "utils/cmdline/commands_map.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/sanity.hpp" +#include "utils/text/table.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace text = utils::text; + +using cli::cmd_help; + + +namespace { + + +/// Creates a table with the help of a set of options. +/// +/// \param options The set of options to describe. May be empty. +/// +/// \return A 2-column wide table with the description of the options. +static text::table +options_help(const cmdline::options_vector& options) +{ + text::table table(2); + + for (cmdline::options_vector::const_iterator iter = options.begin(); + iter != options.end(); iter++) { + const cmdline::base_option* option = *iter; + + std::string description = option->description(); + if (option->needs_arg() && option->has_default_value()) + description += F(" (default: %s)") % option->default_value(); + + text::table_row row; + + if (option->has_short_name()) + row.push_back(F("%s, %s") % option->format_short_name() % + option->format_long_name()); + else + row.push_back(F("%s") % option->format_long_name()); + row.push_back(F("%s.") % description); + + table.add_row(row); + } + + return table; +} + + +/// Prints the summary of commands and generic options. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param options The set of program-wide options for which to print help. +/// \param commands The set of commands for which to print help. +static void +general_help(cmdline::ui* ui, const cmdline::options_vector* options, + const cmdline::commands_map< cli::cli_command >* commands) +{ + PRE(!commands->empty()); + + cli::write_version_header(ui); + ui->out(""); + ui->out_tag_wrap( + "Usage: ", + F("%s [general_options] command [command_options] [args]") % + cmdline::progname(), false); + + const text::table options_table = options_help(*options); + text::widths_vector::value_type first_width = + options_table.column_width(0); + + std::map< std::string, text::table > command_tables; + + for (cmdline::commands_map< cli::cli_command >::const_iterator + iter = commands->begin(); iter != commands->end(); iter++) { + const std::string& category = (*iter).first; + const std::set< std::string >& command_names = (*iter).second; + + command_tables.insert(std::map< std::string, text::table >::value_type( + category, text::table(2))); + text::table& table = command_tables.find(category)->second; + + for (std::set< std::string >::const_iterator i2 = command_names.begin(); + i2 != command_names.end(); i2++) { + const cli::cli_command* command = commands->find(*i2); + text::table_row row; + row.push_back(command->name()); + row.push_back(F("%s.") % command->short_description()); + table.add_row(row); + } + + if (table.column_width(0) > first_width) + first_width = table.column_width(0); + } + + text::table_formatter formatter; + formatter.set_column_width(0, first_width); + formatter.set_column_width(1, text::table_formatter::width_refill); + formatter.set_separator(" "); + + if (!options_table.empty()) { + ui->out_wrap(""); + ui->out_wrap("Available general options:"); + ui->out_table(options_table, formatter, " "); + } + + // Iterate using the same loop as above to preserve ordering. + for (cmdline::commands_map< cli::cli_command >::const_iterator + iter = commands->begin(); iter != commands->end(); iter++) { + const std::string& category = (*iter).first; + ui->out_wrap(""); + ui->out_wrap(F("%s commands:") % + (category.empty() ? "Generic" : category)); + ui->out_table(command_tables.find(category)->second, formatter, " "); + } + + ui->out_wrap(""); + ui->out_wrap("See kyua(1) for more details."); +} + + +/// Prints help for a particular subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param general_options The options that apply to all commands. +/// \param command Pointer to the command to describe. +static void +subcommand_help(cmdline::ui* ui, + const utils::cmdline::options_vector* general_options, + const cli::cli_command* command) +{ + cli::write_version_header(ui); + ui->out(""); + ui->out_tag_wrap( + "Usage: ", F("%s [general_options] %s%s%s") % + cmdline::progname() % command->name() % + (command->options().empty() ? "" : " [command_options]") % + (command->arg_list().empty() ? "" : (" " + command->arg_list())), + false); + ui->out_wrap(""); + ui->out_wrap(F("%s.") % command->short_description()); + + const text::table general_table = options_help(*general_options); + const text::table command_table = options_help(command->options()); + + const text::widths_vector::value_type first_width = + std::max(general_table.column_width(0), command_table.column_width(0)); + text::table_formatter formatter; + formatter.set_column_width(0, first_width); + formatter.set_column_width(1, text::table_formatter::width_refill); + formatter.set_separator(" "); + + if (!general_table.empty()) { + ui->out_wrap(""); + ui->out_wrap("Available general options:"); + ui->out_table(general_table, formatter, " "); + } + + if (!command_table.empty()) { + ui->out_wrap(""); + ui->out_wrap("Available command options:"); + ui->out_table(command_table, formatter, " "); + } + + ui->out_wrap(""); + ui->out_wrap(F("See kyua-%s(1) for more details.") % command->name()); +} + + +} // anonymous namespace + + +/// Default constructor for cmd_help. +/// +/// \param options_ The set of program-wide options for which to provide help. +/// \param commands_ The set of commands for which to provide help. +cmd_help::cmd_help(const cmdline::options_vector* options_, + const cmdline::commands_map< cli_command >* commands_) : + cli_command("help", "[subcommand]", 0, 1, "Shows usage information"), + _options(options_), + _commands(commands_) +{ +} + + +/// Entry point for the "help" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 to indicate success. +int +cmd_help::run(utils::cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + if (cmdline.arguments().empty()) { + general_help(ui, _options, _commands); + } else { + INV(cmdline.arguments().size() == 1); + const std::string& cmdname = cmdline.arguments()[0]; + const cli::cli_command* command = _commands->find(cmdname); + if (command == NULL) + throw cmdline::usage_error(F("The command %s does not exist") % + cmdname); + else + subcommand_help(ui, _options, command); + } + + return EXIT_SUCCESS; +} diff --git a/cli/cmd_help.hpp b/cli/cmd_help.hpp new file mode 100644 index 000000000000..5f3b19db901d --- /dev/null +++ b/cli/cmd_help.hpp @@ -0,0 +1,62 @@ +// 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 cli/cmd_help.hpp +/// Provides the cmd_help class. + +#if !defined(CLI_CMD_HELP_HPP) +#define CLI_CMD_HELP_HPP + +#include "cli/common.hpp" +#include "utils/cmdline/commands_map_fwd.hpp" + +namespace cli { + + +/// Implementation of the "help" subcommand. +class cmd_help : public cli_command +{ + /// The set of program-wide options for which to provide help. + const utils::cmdline::options_vector* _options; + + /// The set of commands for which to provide help. + const utils::cmdline::commands_map< cli_command >* _commands; + +public: + cmd_help(const utils::cmdline::options_vector*, + const utils::cmdline::commands_map< cli_command >*); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_HELP_HPP) diff --git a/cli/cmd_help_test.cpp b/cli/cmd_help_test.cpp new file mode 100644 index 000000000000..d292090be451 --- /dev/null +++ b/cli/cmd_help_test.cpp @@ -0,0 +1,347 @@ +// 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 "cli/cmd_help.hpp" + +#include <algorithm> +#include <cstdlib> +#include <iterator> + +#include <atf-c++.hpp> + +#include "cli/common.ipp" +#include "engine/config.hpp" +#include "utils/cmdline/commands_map.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/config/tree.ipp" +#include "utils/defs.hpp" +#include "utils/sanity.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +namespace cmdline = utils::cmdline; +namespace config = utils::config; + +using cli::cmd_help; + + +namespace { + + +/// Mock command with a simple definition (no options, no arguments). +/// +/// Attempting to run this command will result in a crash. It is only provided +/// to validate the generation of interactive help. +class cmd_mock_simple : public cli::cli_command { +public: + /// Constructs a new mock command. + /// + /// \param name_ The name of the command to create. + cmd_mock_simple(const char* name_) : cli::cli_command( + name_, "", 0, 0, "Simple command") + { + } + + /// Runs the mock command. + /// + /// \return Nothing because this function is never called. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& /* cmdline */, + const config::tree& /* user_config */) + { + UNREACHABLE; + } +}; + + +/// Mock command with a complex definition (some options, some arguments). +/// +/// Attempting to run this command will result in a crash. It is only provided +/// to validate the generation of interactive help. +class cmd_mock_complex : public cli::cli_command { +public: + /// Constructs a new mock command. + /// + /// \param name_ The name of the command to create. + cmd_mock_complex(const char* name_) : cli::cli_command( + name_, "[arg1 .. argN]", 0, 2, "Complex command") + { + add_option(cmdline::bool_option("flag_a", "Flag A")); + add_option(cmdline::bool_option('b', "flag_b", "Flag B")); + add_option(cmdline::string_option('c', "flag_c", "Flag C", "c_arg")); + add_option(cmdline::string_option("flag_d", "Flag D", "d_arg", "foo")); + } + + /// Runs the mock command. + /// + /// \return Nothing because this function is never called. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& /* cmdline */, + const config::tree& /* user_config */) + { + UNREACHABLE; + } +}; + + +/// Initializes the cmdline library and generates the set of test commands. +/// +/// \param [out] commands A mapping that is updated to contain the commands to +/// use for testing. +static void +setup(cmdline::commands_map< cli::cli_command >& commands) +{ + cmdline::init("progname"); + + commands.insert(new cmd_mock_simple("mock_simple")); + commands.insert(new cmd_mock_complex("mock_complex")); + + commands.insert(new cmd_mock_simple("mock_simple_2"), "First"); + commands.insert(new cmd_mock_complex("mock_complex_2"), "First"); + + commands.insert(new cmd_mock_simple("mock_simple_3"), "Second"); +} + + +/// Performs a test on the global help (not that of a subcommand). +/// +/// \param general_options The genral options supported by the tool, if any. +/// \param expected_options Expected lines of help output documenting the +/// options in general_options. +/// \param ui The cmdline::mock_ui object to which to write the output. +static void +global_test(const cmdline::options_vector& general_options, + const std::vector< std::string >& expected_options, + cmdline::ui_mock& ui) +{ + cmdline::commands_map< cli::cli_command > mock_commands; + setup(mock_commands); + + cmdline::args_vector args; + args.push_back("help"); + + cmd_help cmd(&general_options, &mock_commands); + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + + std::vector< std::string > expected; + + expected.push_back(PACKAGE " (" PACKAGE_NAME ") " PACKAGE_VERSION); + expected.push_back(""); + expected.push_back("Usage: progname [general_options] command " + "[command_options] [args]"); + if (!general_options.empty()) { + expected.push_back(""); + expected.push_back("Available general options:"); + std::copy(expected_options.begin(), expected_options.end(), + std::back_inserter(expected)); + } + expected.push_back(""); + expected.push_back("Generic commands:"); + expected.push_back(" mock_complex Complex command."); + expected.push_back(" mock_simple Simple command."); + expected.push_back(""); + expected.push_back("First commands:"); + expected.push_back(" mock_complex_2 Complex command."); + expected.push_back(" mock_simple_2 Simple command."); + expected.push_back(""); + expected.push_back("Second commands:"); + expected.push_back(" mock_simple_3 Simple command."); + expected.push_back(""); + expected.push_back("See kyua(1) for more details."); + + ATF_REQUIRE(expected == ui.out_log()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(global__no_options); +ATF_TEST_CASE_BODY(global__no_options) +{ + cmdline::ui_mock ui; + + cmdline::options_vector general_options; + + global_test(general_options, std::vector< std::string >(), ui); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(global__some_options); +ATF_TEST_CASE_BODY(global__some_options) +{ + cmdline::ui_mock ui; + + cmdline::options_vector general_options; + const cmdline::bool_option flag_a("flag_a", "Flag A"); + general_options.push_back(&flag_a); + const cmdline::string_option flag_c('c', "lc", "Flag C", "X"); + general_options.push_back(&flag_c); + + std::vector< std::string > expected; + expected.push_back(" --flag_a Flag A."); + expected.push_back(" -c X, --lc=X Flag C."); + + global_test(general_options, expected, ui); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subcommand__simple); +ATF_TEST_CASE_BODY(subcommand__simple) +{ + cmdline::options_vector general_options; + + cmdline::commands_map< cli::cli_command > mock_commands; + setup(mock_commands); + + cmdline::args_vector args; + args.push_back("help"); + args.push_back("mock_simple"); + + cmd_help cmd(&general_options, &mock_commands); + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(atf::utils::grep_collection( + "^kyua.*" PACKAGE_VERSION, ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "^Usage: progname \\[general_options\\] mock_simple$", ui.out_log())); + ATF_REQUIRE(!atf::utils::grep_collection( + "Available.*options", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "^See kyua-mock_simple\\(1\\) for more details.", ui.out_log())); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subcommand__complex); +ATF_TEST_CASE_BODY(subcommand__complex) +{ + cmdline::options_vector general_options; + const cmdline::bool_option global_a("global_a", "Global A"); + general_options.push_back(&global_a); + const cmdline::string_option global_c('c', "global_c", "Global C", + "c_global"); + general_options.push_back(&global_c); + + cmdline::commands_map< cli::cli_command > mock_commands; + setup(mock_commands); + + cmdline::args_vector args; + args.push_back("help"); + args.push_back("mock_complex"); + + cmd_help cmd(&general_options, &mock_commands); + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(atf::utils::grep_collection( + "^kyua.*" PACKAGE_VERSION, ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "^Usage: progname \\[general_options\\] mock_complex " + "\\[command_options\\] \\[arg1 .. argN\\]$", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("Available general options", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("--global_a", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("--global_c=c_global", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("Available command options", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("--flag_a *Flag A", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection("-b.*--flag_b *Flag B", + ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "-c c_arg.*--flag_c=c_arg *Flag C", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "--flag_d=d_arg *Flag D.*default.*foo", ui.out_log())); + ATF_REQUIRE(atf::utils::grep_collection( + "^See kyua-mock_complex\\(1\\) for more details.", ui.out_log())); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(subcommand__unknown); +ATF_TEST_CASE_BODY(subcommand__unknown) +{ + cmdline::options_vector general_options; + + cmdline::commands_map< cli::cli_command > mock_commands; + setup(mock_commands); + + cmdline::args_vector args; + args.push_back("help"); + args.push_back("foobar"); + + cmd_help cmd(&general_options, &mock_commands); + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "command foobar.*not exist", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_args); +ATF_TEST_CASE_BODY(invalid_args) +{ + cmdline::options_vector general_options; + + cmdline::commands_map< cli::cli_command > mock_commands; + setup(mock_commands); + + cmdline::args_vector args; + args.push_back("help"); + args.push_back("mock_simple"); + args.push_back("mock_complex"); + + cmd_help cmd(&general_options, &mock_commands); + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::usage_error, "Too many arguments", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, global__no_options); + ATF_ADD_TEST_CASE(tcs, global__some_options); + ATF_ADD_TEST_CASE(tcs, subcommand__simple); + ATF_ADD_TEST_CASE(tcs, subcommand__complex); + ATF_ADD_TEST_CASE(tcs, subcommand__unknown); + ATF_ADD_TEST_CASE(tcs, invalid_args); +} diff --git a/cli/cmd_list.cpp b/cli/cmd_list.cpp new file mode 100644 index 000000000000..ed0e4980fc47 --- /dev/null +++ b/cli/cmd_list.cpp @@ -0,0 +1,161 @@ +// 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 "cli/cmd_list.hpp" + +#include <cstdlib> +#include <utility> +#include <vector> + +#include "cli/common.ipp" +#include "drivers/list_tests.hpp" +#include "engine/filters.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/types.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; + + +namespace { + + +/// Hooks for list_tests to print test cases as they come. +class progress_hooks : public drivers::list_tests::base_hooks { + /// The ui object to which to print the test cases. + cmdline::ui* _ui; + + /// Whether to print test case details or just their names. + bool _verbose; + +public: + /// Initializes the hooks. + /// + /// \param ui_ The ui object to which to print the test cases. + /// \param verbose_ Whether to print test case details or just their names. + progress_hooks(cmdline::ui* ui_, const bool verbose_) : + _ui(ui_), + _verbose(verbose_) + { + } + + /// Reports a test case as soon as it is found. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the located test case. + void + got_test_case(const model::test_program& test_program, + const std::string& test_case_name) + { + cli::detail::list_test_case(_ui, _verbose, test_program, + test_case_name); + } +}; + + +} // anonymous namespace + + +/// Lists a single test case. +/// +/// \param [out] ui Object to interact with the I/O of the program. +/// \param verbose Whether to be verbose or not. +/// \param test_program The test program containing the test case to print. +/// \param test_case_name The name of the test case to print. +void +cli::detail::list_test_case(cmdline::ui* ui, const bool verbose, + const model::test_program& test_program, + const std::string& test_case_name) +{ + const model::test_case& test_case = test_program.find(test_case_name); + + const std::string id = format_test_case_id(test_program, test_case_name); + if (!verbose) { + ui->out(id); + } else { + ui->out(F("%s (%s)") % id % test_program.test_suite_name()); + + // TODO(jmmv): Running these for every test case is probably not the + // fastest thing to do. + const model::metadata default_md = model::metadata_builder().build(); + const model::properties_map default_props = default_md.to_properties(); + + const model::metadata& test_md = test_case.get_metadata(); + const model::properties_map test_props = test_md.to_properties(); + + for (model::properties_map::const_iterator iter = test_props.begin(); + iter != test_props.end(); iter++) { + const model::properties_map::const_iterator default_iter = + default_props.find((*iter).first); + if (default_iter == default_props.end() || + (*iter).second != (*default_iter).second) + ui->out(F(" %s = %s") % (*iter).first % (*iter).second); + } + } +} + + +/// Default constructor for cmd_list. +cli::cmd_list::cmd_list(void) : + cli_command("list", "[test-program ...]", 0, -1, + "Lists test cases and their meta-data") +{ + add_option(build_root_option); + add_option(kyuafile_option); + add_option(cmdline::bool_option('v', "verbose", "Show properties")); +} + + +/// Entry point for the "list" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// \param user_config The runtime configuration of the program. +/// +/// \return 0 to indicate success. +int +cli::cmd_list::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& user_config) +{ + progress_hooks hooks(ui, cmdline.has_option("verbose")); + const drivers::list_tests::result result = drivers::list_tests::drive( + kyuafile_path(cmdline), build_root_path(cmdline), + parse_filters(cmdline.arguments()), user_config, hooks); + + return report_unused_filters(result.unused_filters, ui) ? + EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/cli/cmd_list.hpp b/cli/cmd_list.hpp new file mode 100644 index 000000000000..cbdc084a6e16 --- /dev/null +++ b/cli/cmd_list.hpp @@ -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. + +/// \file cli/cmd_list.hpp +/// Provides the cmd_list class. + +#if !defined(CLI_CMD_LIST_HPP) +#define CLI_CMD_LIST_HPP + +#include <string> + +#include "cli/common.hpp" +#include "model/test_program_fwd.hpp" +#include "utils/fs/path_fwd.hpp" + +namespace cli { + + +namespace detail { + +void list_test_case(utils::cmdline::ui*, const bool, const model::test_program&, + const std::string&); + +} // namespace detail + + +/// Implementation of the "list" subcommand. +class cmd_list : public cli_command +{ +public: + cmd_list(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + +#endif // !defined(CLI_CMD_LIST_HPP) diff --git a/cli/cmd_list_test.cpp b/cli/cmd_list_test.cpp new file mode 100644 index 000000000000..19078abd7d48 --- /dev/null +++ b/cli/cmd_list_test.cpp @@ -0,0 +1,112 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_list.hpp" + +#include <atf-c++.hpp> + +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace fs = utils::fs; + + +ATF_TEST_CASE_WITHOUT_HEAD(list_test_case__no_verbose); +ATF_TEST_CASE_BODY(list_test_case__no_verbose) +{ + const model::metadata md = model::metadata_builder() + .set_description("This should not be shown") + .build(); + const model::test_program test_program = model::test_program_builder( + "mock", fs::path("the/test-program"), fs::path("root"), "suite") + .add_test_case("abc", md) + .set_metadata(md) + .build(); + + cmdline::ui_mock ui; + cli::detail::list_test_case(&ui, false, test_program, "abc"); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("the/test-program:abc", ui.out_log()[0]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list_test_case__verbose__no_properties); +ATF_TEST_CASE_BODY(list_test_case__verbose__no_properties) +{ + const model::test_program test_program = model::test_program_builder( + "mock", fs::path("hello/world"), fs::path("root"), "the-suite") + .add_test_case("my_name") + .build(); + + cmdline::ui_mock ui; + cli::detail::list_test_case(&ui, true, test_program, "my_name"); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("hello/world:my_name (the-suite)", ui.out_log()[0]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(list_test_case__verbose__some_properties); +ATF_TEST_CASE_BODY(list_test_case__verbose__some_properties) +{ + const model::metadata md = model::metadata_builder() + .add_custom("my-property", "value") + .set_description("Some description") + .set_has_cleanup(true) + .build(); + const model::test_program test_program = model::test_program_builder( + "mock", fs::path("hello/world"), fs::path("root"), "the-suite") + .add_test_case("my_name", md) + .set_metadata(md) + .build(); + + cmdline::ui_mock ui; + cli::detail::list_test_case(&ui, true, test_program, "my_name"); + ATF_REQUIRE_EQ(4, ui.out_log().size()); + ATF_REQUIRE_EQ("hello/world:my_name (the-suite)", ui.out_log()[0]); + ATF_REQUIRE_EQ(" custom.my-property = value", ui.out_log()[1]); + ATF_REQUIRE_EQ(" description = Some description", ui.out_log()[2]); + ATF_REQUIRE_EQ(" has_cleanup = true", ui.out_log()[3]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, list_test_case__no_verbose); + ATF_ADD_TEST_CASE(tcs, list_test_case__verbose__no_properties); + ATF_ADD_TEST_CASE(tcs, list_test_case__verbose__some_properties); + + // Tests for cmd_list::run are located in integration/cmd_list_test. +} diff --git a/cli/cmd_report.cpp b/cli/cmd_report.cpp new file mode 100644 index 000000000000..27827e893de7 --- /dev/null +++ b/cli/cmd_report.cpp @@ -0,0 +1,421 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_report.hpp" + +#include <algorithm> +#include <cstddef> +#include <cstdlib> +#include <map> +#include <ostream> +#include <string> +#include <vector> + +#include "cli/common.ipp" +#include "drivers/scan_results.hpp" +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "model/types.hpp" +#include "store/layout.hpp" +#include "store/read_transaction.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/stream.hpp" +#include "utils/text/operations.ipp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; +namespace text = utils::text; + +using cli::cmd_report; +using utils::optional; + + +namespace { + + +/// Generates a plain-text report intended to be printed to the console. +class report_console_hooks : public drivers::scan_results::base_hooks { + /// Stream to which to write the report. + std::ostream& _output; + + /// Whether to include details in the report or not. + const bool _verbose; + + /// Collection of result types to include in the report. + const cli::result_types& _results_filters; + + /// Path to the results file being read. + const fs::path& _results_file; + + /// The start time of the first test. + optional< utils::datetime::timestamp > _start_time; + + /// The end time of the last test. + optional< utils::datetime::timestamp > _end_time; + + /// The total run time of the tests. Note that we cannot subtract _end_time + /// from _start_time to compute this due to parallel execution. + utils::datetime::delta _runtime; + + /// Representation of a single result. + struct result_data { + /// The relative path to the test program. + utils::fs::path binary_path; + + /// The name of the test case. + std::string test_case_name; + + /// The result of the test case. + model::test_result result; + + /// The duration of the test case execution. + utils::datetime::delta duration; + + /// Constructs a new results data. + /// + /// \param binary_path_ The relative path to the test program. + /// \param test_case_name_ The name of the test case. + /// \param result_ The result of the test case. + /// \param duration_ The duration of the test case execution. + result_data(const utils::fs::path& binary_path_, + const std::string& test_case_name_, + const model::test_result& result_, + const utils::datetime::delta& duration_) : + binary_path(binary_path_), test_case_name(test_case_name_), + result(result_), duration(duration_) + { + } + }; + + /// Results received, broken down by their type. + /// + /// Note that this may not include all results, as keeping the whole list in + /// memory may be too much. + std::map< model::test_result_type, std::vector< result_data > > _results; + + /// Pretty-prints the value of an environment variable. + /// + /// \param indent Prefix for the lines to print. Continuation lines + /// use this indentation twice. + /// \param name Name of the variable. + /// \param value Value of the variable. Can have newlines. + void + print_env_var(const char* indent, const std::string& name, + const std::string& value) + { + const std::vector< std::string > lines = text::split(value, '\n'); + if (lines.size() == 0) { + _output << F("%s%s=\n") % indent % name;; + } else { + _output << F("%s%s=%s\n") % indent % name % lines[0]; + for (std::vector< std::string >::size_type i = 1; + i < lines.size(); ++i) { + _output << F("%s%s%s\n") % indent % indent % lines[i]; + } + } + } + + /// Prints the execution context to the output. + /// + /// \param context The context to dump. + void + print_context(const model::context& context) + { + _output << "===> Execution context\n"; + + _output << F("Current directory: %s\n") % context.cwd(); + const std::map< std::string, std::string >& env = context.env(); + if (env.empty()) + _output << "No environment variables recorded\n"; + else { + _output << "Environment variables:\n"; + for (std::map< std::string, std::string >::const_iterator + iter = env.begin(); iter != env.end(); iter++) { + print_env_var(" ", (*iter).first, (*iter).second); + } + } + } + + /// Dumps a detailed view of the test case. + /// + /// \param result_iter Results iterator pointing at the test case to be + /// dumped. + void + print_test_case_and_result(const store::results_iterator& result_iter) + { + const model::test_case& test_case = + result_iter.test_program()->find(result_iter.test_case_name()); + const model::properties_map props = + test_case.get_metadata().to_properties(); + + _output << F("===> %s:%s\n") % + result_iter.test_program()->relative_path() % + result_iter.test_case_name(); + _output << F("Result: %s\n") % + cli::format_result(result_iter.result()); + _output << F("Start time: %s\n") % + result_iter.start_time().to_iso8601_in_utc(); + _output << F("End time: %s\n") % + result_iter.end_time().to_iso8601_in_utc(); + _output << F("Duration: %s\n") % + cli::format_delta(result_iter.end_time() - + result_iter.start_time()); + + _output << "\n"; + _output << "Metadata:\n"; + for (model::properties_map::const_iterator iter = props.begin(); + iter != props.end(); ++iter) { + if ((*iter).second.empty()) { + _output << F(" %s is empty\n") % (*iter).first; + } else { + _output << F(" %s = %s\n") % (*iter).first % (*iter).second; + } + } + + const std::string stdout_contents = result_iter.stdout_contents(); + if (!stdout_contents.empty()) { + _output << "\n" + << "Standard output:\n" + << stdout_contents; + } + + const std::string stderr_contents = result_iter.stderr_contents(); + if (!stderr_contents.empty()) { + _output << "\n" + << "Standard error:\n" + << stderr_contents; + } + } + + /// Counts how many results of a given type have been received. + /// + /// \param type Test result type to count results for. + /// + /// \return The number of test results with \p type. + std::size_t + count_results(const model::test_result_type type) + { + const std::map< model::test_result_type, + std::vector< result_data > >::const_iterator iter = + _results.find(type); + if (iter == _results.end()) + return 0; + else + return (*iter).second.size(); + } + + /// Prints a set of results. + /// + /// \param type Test result type to print results for. + /// \param title Title used when printing results. + void + print_results(const model::test_result_type type, + const char* title) + { + const std::map< model::test_result_type, + std::vector< result_data > >::const_iterator iter2 = + _results.find(type); + if (iter2 == _results.end()) + return; + const std::vector< result_data >& all = (*iter2).second; + + _output << F("===> %s\n") % title; + for (std::vector< result_data >::const_iterator iter = all.begin(); + iter != all.end(); iter++) { + _output << F("%s:%s -> %s [%s]\n") % (*iter).binary_path % + (*iter).test_case_name % + cli::format_result((*iter).result) % + cli::format_delta((*iter).duration); + } + } + +public: + /// Constructor for the hooks. + /// + /// \param [out] output_ Stream to which to write the report. + /// \param verbose_ Whether to include details in the output or not. + /// \param results_filters_ The result types to include in the report. + /// Cannot be empty. + /// \param results_file_ Path to the results file being read. + report_console_hooks(std::ostream& output_, const bool verbose_, + const cli::result_types& results_filters_, + const fs::path& results_file_) : + _output(output_), + _verbose(verbose_), + _results_filters(results_filters_), + _results_file(results_file_) + { + PRE(!results_filters_.empty()); + } + + /// Callback executed when the context is loaded. + /// + /// \param context The context loaded from the database. + void + got_context(const model::context& context) + { + if (_verbose) + print_context(context); + } + + /// Callback executed when a test results is found. + /// + /// \param iter Container for the test result's data. + void + got_result(store::results_iterator& iter) + { + if (!_start_time || _start_time.get() > iter.start_time()) + _start_time = iter.start_time(); + if (!_end_time || _end_time.get() < iter.end_time()) + _end_time = iter.end_time(); + + const datetime::delta duration = iter.end_time() - iter.start_time(); + + _runtime += duration; + const model::test_result result = iter.result(); + _results[result.type()].push_back( + result_data(iter.test_program()->relative_path(), + iter.test_case_name(), iter.result(), duration)); + + if (_verbose) { + // TODO(jmmv): _results_filters is a list and is small enough for + // std::find to not be an expensive operation here (probably). But + // we should be using a std::set instead. + if (std::find(_results_filters.begin(), _results_filters.end(), + iter.result().type()) != _results_filters.end()) { + print_test_case_and_result(iter); + } + } + } + + /// Prints the tests summary. + void + end(const drivers::scan_results::result& /* r */) + { + typedef std::map< model::test_result_type, const char* > types_map; + + types_map titles; + titles[model::test_result_broken] = "Broken tests"; + titles[model::test_result_expected_failure] = "Expected failures"; + titles[model::test_result_failed] = "Failed tests"; + titles[model::test_result_passed] = "Passed tests"; + titles[model::test_result_skipped] = "Skipped tests"; + + for (cli::result_types::const_iterator iter = _results_filters.begin(); + iter != _results_filters.end(); ++iter) { + const types_map::const_iterator match = titles.find(*iter); + INV_MSG(match != titles.end(), "Conditional does not match user " + "input validation in parse_types()"); + print_results((*match).first, (*match).second); + } + + const std::size_t broken = count_results(model::test_result_broken); + const std::size_t failed = count_results(model::test_result_failed); + const std::size_t passed = count_results(model::test_result_passed); + const std::size_t skipped = count_results(model::test_result_skipped); + const std::size_t xfail = count_results( + model::test_result_expected_failure); + const std::size_t total = broken + failed + passed + skipped + xfail; + + _output << "===> Summary\n"; + _output << F("Results read from %s\n") % _results_file; + _output << F("Test cases: %s total, %s skipped, %s expected failures, " + "%s broken, %s failed\n") % + total % skipped % xfail % broken % failed; + if (_verbose && _start_time) { + INV(_end_time); + _output << F("Start time: %s\n") % + _start_time.get().to_iso8601_in_utc(); + _output << F("End time: %s\n") % + _end_time.get().to_iso8601_in_utc(); + } + _output << F("Total time: %s\n") % cli::format_delta(_runtime); + } +}; + + +} // anonymous namespace + + +/// Default constructor for cmd_report. +cmd_report::cmd_report(void) : cli_command( + "report", "", 0, -1, + "Generates a report with the results of a test suite run") +{ + add_option(results_file_open_option); + add_option(cmdline::bool_option( + "verbose", "Include the execution context and the details of each test " + "case in the report")); + add_option(cmdline::path_option("output", "Path to the output file", "path", + "/dev/stdout")); + add_option(results_filter_option); +} + + +/// Entry point for the "report" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if the statement is invalid or if there is +/// any other problem. +int +cmd_report::run(cmdline::ui* ui, + const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + std::auto_ptr< std::ostream > output = utils::open_ostream( + cmdline.get_option< cmdline::path_option >("output")); + + const fs::path results_file = layout::find_results( + results_file_open(cmdline)); + + const result_types types = get_result_types(cmdline); + report_console_hooks hooks(*output.get(), cmdline.has_option("verbose"), + types, results_file); + const drivers::scan_results::result result = drivers::scan_results::drive( + results_file, parse_filters(cmdline.arguments()), hooks); + + return report_unused_filters(result.unused_filters, ui) ? + EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/cli/cmd_report.hpp b/cli/cmd_report.hpp new file mode 100644 index 000000000000..3d73c592ed9b --- /dev/null +++ b/cli/cmd_report.hpp @@ -0,0 +1,54 @@ +// 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 cli/cmd_report.hpp +/// Provides the cmd_report class. + +#if !defined(CLI_CMD_REPORT_HPP) +#define CLI_CMD_REPORT_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "report" subcommand. +class cmd_report : public cli_command +{ +public: + cmd_report(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_REPORT_HPP) diff --git a/cli/cmd_report_html.cpp b/cli/cmd_report_html.cpp new file mode 100644 index 000000000000..b2133a8de047 --- /dev/null +++ b/cli/cmd_report_html.cpp @@ -0,0 +1,474 @@ +// 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 "cli/cmd_report_html.hpp" + +#include <algorithm> +#include <cerrno> +#include <cstdlib> +#include <set> +#include <stdexcept> + +#include "cli/common.ipp" +#include "drivers/scan_results.hpp" +#include "engine/filters.hpp" +#include "model/context.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/layout.hpp" +#include "store/read_transaction.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/text/templates.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; +namespace text = utils::text; + +using utils::optional; + + +namespace { + + +/// Creates the report's top directory and fails if it exists. +/// +/// \param directory The directory to create. +/// \param force Whether to wipe an existing directory or not. +/// +/// \throw std::runtime_error If the directory already exists; this is a user +/// error that the user must correct. +/// \throw fs::error If the directory creation fails for any other reason. +static void +create_top_directory(const fs::path& directory, const bool force) +{ + if (force) { + if (fs::exists(directory)) + fs::rm_r(directory); + } + + try { + fs::mkdir(directory, 0755); + } catch (const fs::system_error& e) { + if (e.original_errno() == EEXIST) + throw std::runtime_error(F("Output directory '%s' already exists; " + "maybe use --force?") % + directory); + else + throw e; + } +} + + +/// Generates a flat unique filename for a given test case. +/// +/// \param test_program The test program for which to genereate the name. +/// \param test_case_name The test case name. +/// +/// \return A filename unique within a directory with a trailing HTML extension. +static std::string +test_case_filename(const model::test_program& test_program, + const std::string& test_case_name) +{ + static const char* special_characters = "/:"; + + std::string name = cli::format_test_case_id(test_program, test_case_name); + std::string::size_type pos = name.find_first_of(special_characters); + while (pos != std::string::npos) { + name.replace(pos, 1, "_"); + pos = name.find_first_of(special_characters, pos + 1); + } + return name + ".html"; +} + + +/// Adds a string to string map to the templates. +/// +/// \param [in,out] templates The templates to add the map to. +/// \param props The map to add to the templates. +/// \param key_vector Name of the template vector that holds the keys. +/// \param value_vector Name of the template vector that holds the values. +static void +add_map(text::templates_def& templates, const config::properties_map& props, + const std::string& key_vector, const std::string& value_vector) +{ + templates.add_vector(key_vector); + templates.add_vector(value_vector); + + for (config::properties_map::const_iterator iter = props.begin(); + iter != props.end(); ++iter) { + templates.add_to_vector(key_vector, (*iter).first); + templates.add_to_vector(value_vector, (*iter).second); + } +} + + +/// Generates an HTML report. +class html_hooks : public drivers::scan_results::base_hooks { + /// User interface object where to report progress. + cmdline::ui* _ui; + + /// The top directory in which to create the HTML files. + fs::path _directory; + + /// Collection of result types to include in the report. + const cli::result_types& _results_filters; + + /// The start time of the first test. + optional< utils::datetime::timestamp > _start_time; + + /// The end time of the last test. + optional< utils::datetime::timestamp > _end_time; + + /// The total run time of the tests. Note that we cannot subtract _end_time + /// from _start_time to compute this due to parallel execution. + utils::datetime::delta _runtime; + + /// Templates accumulator to generate the index.html file. + text::templates_def _summary_templates; + + /// Mapping of result types to the amount of tests with such result. + std::map< model::test_result_type, std::size_t > _types_count; + + /// Generates a common set of templates for all of our files. + /// + /// \return A new templates object with common parameters. + static text::templates_def + common_templates(void) + { + text::templates_def templates; + templates.add_variable("css", "report.css"); + return templates; + } + + /// Adds a test case result to the summary. + /// + /// \param test_program The test program with the test case to be added. + /// \param test_case_name Name of the test case. + /// \param result The result of the test case. + /// \param has_detail If true, the result of the test case has not been + /// filtered and therefore there exists a separate file for the test + /// with all of its information. + void + add_to_summary(const model::test_program& test_program, + const std::string& test_case_name, + const model::test_result& result, + const bool has_detail) + { + ++_types_count[result.type()]; + + if (!has_detail) + return; + + std::string test_cases_vector; + std::string test_cases_file_vector; + switch (result.type()) { + case model::test_result_broken: + test_cases_vector = "broken_test_cases"; + test_cases_file_vector = "broken_test_cases_file"; + break; + + case model::test_result_expected_failure: + test_cases_vector = "xfail_test_cases"; + test_cases_file_vector = "xfail_test_cases_file"; + break; + + case model::test_result_failed: + test_cases_vector = "failed_test_cases"; + test_cases_file_vector = "failed_test_cases_file"; + break; + + case model::test_result_passed: + test_cases_vector = "passed_test_cases"; + test_cases_file_vector = "passed_test_cases_file"; + break; + + case model::test_result_skipped: + test_cases_vector = "skipped_test_cases"; + test_cases_file_vector = "skipped_test_cases_file"; + break; + } + INV(!test_cases_vector.empty()); + INV(!test_cases_file_vector.empty()); + + _summary_templates.add_to_vector( + test_cases_vector, + cli::format_test_case_id(test_program, test_case_name)); + _summary_templates.add_to_vector( + test_cases_file_vector, + test_case_filename(test_program, test_case_name)); + } + + /// Instantiate a template to generate an HTML file in the output directory. + /// + /// \param templates The templates to use. + /// \param template_name The name of the template. This is automatically + /// searched for in the installed directory, so do not provide a path. + /// \param output_name The name of the output file. This is a basename to + /// be created within the output directory. + /// + /// \throw text::error If there is any problem applying the templates. + void + generate(const text::templates_def& templates, + const std::string& template_name, + const std::string& output_name) const + { + const fs::path miscdir(utils::getenv_with_default( + "KYUA_MISCDIR", KYUA_MISCDIR)); + const fs::path template_file = miscdir / template_name; + const fs::path output_path(_directory / output_name); + + _ui->out(F("Generating %s") % output_path); + text::instantiate(templates, template_file, output_path); + } + + /// Gets the number of tests with a given result type. + /// + /// \param type The type to be queried. + /// + /// \return The number of tests of the given type, or 0 if none have yet + /// been registered by add_to_summary(). + std::size_t + get_count(const model::test_result_type type) const + { + const std::map< model::test_result_type, std::size_t >::const_iterator + iter = _types_count.find(type); + if (iter == _types_count.end()) + return 0; + else + return (*iter).second; + } + +public: + /// Constructor for the hooks. + /// + /// \param ui_ User interface object where to report progress. + /// \param directory_ The directory in which to create the HTML files. + /// \param results_filters_ The result types to include in the report. + /// Cannot be empty. + html_hooks(cmdline::ui* ui_, const fs::path& directory_, + const cli::result_types& results_filters_) : + _ui(ui_), + _directory(directory_), + _results_filters(results_filters_), + _summary_templates(common_templates()) + { + PRE(!results_filters_.empty()); + + // Keep in sync with add_to_summary(). + _summary_templates.add_vector("broken_test_cases"); + _summary_templates.add_vector("broken_test_cases_file"); + _summary_templates.add_vector("xfail_test_cases"); + _summary_templates.add_vector("xfail_test_cases_file"); + _summary_templates.add_vector("failed_test_cases"); + _summary_templates.add_vector("failed_test_cases_file"); + _summary_templates.add_vector("passed_test_cases"); + _summary_templates.add_vector("passed_test_cases_file"); + _summary_templates.add_vector("skipped_test_cases"); + _summary_templates.add_vector("skipped_test_cases_file"); + } + + /// Callback executed when the context is loaded. + /// + /// \param context The context loaded from the database. + void + got_context(const model::context& context) + { + text::templates_def templates = common_templates(); + templates.add_variable("cwd", context.cwd().str()); + add_map(templates, context.env(), "env_var", "env_var_value"); + generate(templates, "context.html", "context.html"); + } + + /// Callback executed when a test results is found. + /// + /// \param iter Container for the test result's data. + void + got_result(store::results_iterator& iter) + { + const model::test_program_ptr test_program = iter.test_program(); + const std::string& test_case_name = iter.test_case_name(); + const model::test_result result = iter.result(); + + if (std::find(_results_filters.begin(), _results_filters.end(), + result.type()) == _results_filters.end()) { + add_to_summary(*test_program, test_case_name, result, false); + return; + } + + add_to_summary(*test_program, test_case_name, result, true); + + if (!_start_time || _start_time.get() > iter.start_time()) + _start_time = iter.start_time(); + if (!_end_time || _end_time.get() < iter.end_time()) + _end_time = iter.end_time(); + + const datetime::delta duration = iter.end_time() - iter.start_time(); + + _runtime += duration; + + text::templates_def templates = common_templates(); + templates.add_variable("test_case", + cli::format_test_case_id(*test_program, + test_case_name)); + templates.add_variable("test_program", + test_program->absolute_path().str()); + templates.add_variable("result", cli::format_result(result)); + templates.add_variable("start_time", + iter.start_time().to_iso8601_in_utc()); + templates.add_variable("end_time", + iter.end_time().to_iso8601_in_utc()); + templates.add_variable("duration", cli::format_delta(duration)); + + const model::test_case& test_case = test_program->find(test_case_name); + add_map(templates, test_case.get_metadata().to_properties(), + "metadata_var", "metadata_value"); + + { + const std::string stdout_text = iter.stdout_contents(); + if (!stdout_text.empty()) + templates.add_variable("stdout", stdout_text); + } + { + const std::string stderr_text = iter.stderr_contents(); + if (!stderr_text.empty()) + templates.add_variable("stderr", stderr_text); + } + + generate(templates, "test_result.html", + test_case_filename(*test_program, test_case_name)); + } + + /// Writes the index.html file in the output directory. + /// + /// This should only be called once all the processing has been done; + /// i.e. when the scan_results driver returns. + void + write_summary(void) + { + const std::size_t n_passed = get_count(model::test_result_passed); + const std::size_t n_failed = get_count(model::test_result_failed); + const std::size_t n_skipped = get_count(model::test_result_skipped); + const std::size_t n_xfail = get_count( + model::test_result_expected_failure); + const std::size_t n_broken = get_count(model::test_result_broken); + + const std::size_t n_bad = n_broken + n_failed; + + if (_start_time) { + INV(_end_time); + _summary_templates.add_variable( + "start_time", _start_time.get().to_iso8601_in_utc()); + _summary_templates.add_variable( + "end_time", _end_time.get().to_iso8601_in_utc()); + } else { + _summary_templates.add_variable("start_time", "No tests run"); + _summary_templates.add_variable("end_time", "No tests run"); + } + _summary_templates.add_variable("duration", + cli::format_delta(_runtime)); + _summary_templates.add_variable("passed_tests_count", + F("%s") % n_passed); + _summary_templates.add_variable("failed_tests_count", + F("%s") % n_failed); + _summary_templates.add_variable("skipped_tests_count", + F("%s") % n_skipped); + _summary_templates.add_variable("xfail_tests_count", + F("%s") % n_xfail); + _summary_templates.add_variable("broken_tests_count", + F("%s") % n_broken); + _summary_templates.add_variable("bad_tests_count", F("%s") % n_bad); + + generate(text::templates_def(), "report.css", "report.css"); + generate(_summary_templates, "index.html", "index.html"); + } +}; + + +} // anonymous namespace + + +/// Default constructor for cmd_report_html. +cli::cmd_report_html::cmd_report_html(void) : cli_command( + "report-html", "", 0, 0, + "Generates an HTML report with the result of a test suite run") +{ + add_option(results_file_open_option); + add_option(cmdline::bool_option( + "force", "Wipe the output directory before generating the new report; " + "use care")); + add_option(cmdline::path_option( + "output", "The directory in which to store the HTML files", + "path", "html")); + add_option(cmdline::list_option( + "results-filter", "Comma-separated list of result types to include in " + "the report", "types", "skipped,xfail,broken,failed")); +} + + +/// Entry point for the "report-html" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if the statement is invalid or if there is +/// any other problem. +int +cli::cmd_report_html::run(cmdline::ui* ui, + const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + const result_types types = get_result_types(cmdline); + + const fs::path results_file = layout::find_results( + results_file_open(cmdline)); + + const fs::path directory = + cmdline.get_option< cmdline::path_option >("output"); + create_top_directory(directory, cmdline.has_option("force")); + html_hooks hooks(ui, directory, types); + drivers::scan_results::drive(results_file, + std::set< engine::test_filter >(), + hooks); + hooks.write_summary(); + + return EXIT_SUCCESS; +} diff --git a/cli/cmd_report_html.hpp b/cli/cmd_report_html.hpp new file mode 100644 index 000000000000..fadc138293ad --- /dev/null +++ b/cli/cmd_report_html.hpp @@ -0,0 +1,55 @@ +// 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 cli/cmd_report_html.hpp +/// Provides the cmd_report_html class. + +#if !defined(CLI_CMD_REPORT_HTML_HPP) +#define CLI_CMD_REPORT_HTML_HPP + +#include "cli/common.hpp" +#include "utils/cmdline/ui_fwd.hpp" + +namespace cli { + + +/// Implementation of the "report-html" subcommand. +class cmd_report_html : public cli_command +{ +public: + cmd_report_html(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_REPORT_HTML_HPP) diff --git a/cli/cmd_report_junit.cpp b/cli/cmd_report_junit.cpp new file mode 100644 index 000000000000..c4846c8795f2 --- /dev/null +++ b/cli/cmd_report_junit.cpp @@ -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. + +#include "cli/cmd_report_junit.hpp" + +#include <cstddef> +#include <cstdlib> +#include <set> + +#include "cli/common.ipp" +#include "drivers/report_junit.hpp" +#include "drivers/scan_results.hpp" +#include "engine/filters.hpp" +#include "store/layout.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/defs.hpp" +#include "utils/optional.ipp" +#include "utils/stream.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; +namespace layout = store::layout; + +using cli::cmd_report_junit; +using utils::optional; + + +/// Default constructor for cmd_report. +cmd_report_junit::cmd_report_junit(void) : cli_command( + "report-junit", "", 0, 0, + "Generates a JUnit report with the result of a test suite run") +{ + add_option(results_file_open_option); + add_option(cmdline::path_option("output", "Path to the output file", "path", + "/dev/stdout")); +} + + +/// Entry point for the "report" subcommand. +/// +/// \param cmdline Representation of the command line to the subcommand. +/// +/// \return 0 if everything is OK, 1 if the statement is invalid or if there is +/// any other problem. +int +cmd_report_junit::run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& cmdline, + const config::tree& /* user_config */) +{ + const fs::path results_file = layout::find_results( + results_file_open(cmdline)); + + std::auto_ptr< std::ostream > output = utils::open_ostream( + cmdline.get_option< cmdline::path_option >("output")); + + drivers::report_junit_hooks hooks(*output.get()); + drivers::scan_results::drive(results_file, + std::set< engine::test_filter >(), + hooks); + + return EXIT_SUCCESS; +} diff --git a/cli/cmd_report_junit.hpp b/cli/cmd_report_junit.hpp new file mode 100644 index 000000000000..1dc0bb731645 --- /dev/null +++ b/cli/cmd_report_junit.hpp @@ -0,0 +1,54 @@ +// 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 cli/cmd_report_junit.hpp +/// Provides the cmd_report_junit class. + +#if !defined(CLI_CMD_REPORT_JUNIT_HPP) +#define CLI_CMD_REPORT_JUNIT_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "report-junit" subcommand. +class cmd_report_junit : public cli_command +{ +public: + cmd_report_junit(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_REPORT_JUNIT_HPP) diff --git a/cli/cmd_test.cpp b/cli/cmd_test.cpp new file mode 100644 index 000000000000..cfaeec9b74cc --- /dev/null +++ b/cli/cmd_test.cpp @@ -0,0 +1,186 @@ +// 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 "cli/cmd_test.hpp" + +#include <cstdlib> + +#include "cli/common.ipp" +#include "drivers/run_tests.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/layout.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/config/tree.ipp" +#include "utils/datetime.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; + +using cli::cmd_test; + + +namespace { + + +/// Hooks to print a progress report of the execution of the tests. +class print_hooks : public drivers::run_tests::base_hooks { + /// Object to interact with the I/O of the program. + cmdline::ui* _ui; + + /// Whether the tests are executed in parallel or not. + bool _parallel; + +public: + /// The amount of positive test results found so far. + unsigned long good_count; + + /// The amount of negative test results found so far. + unsigned long bad_count; + + /// Constructor for the hooks. + /// + /// \param ui_ Object to interact with the I/O of the program. + /// \param parallel_ True if we are executing more than one test at once. + print_hooks(cmdline::ui* ui_, const bool parallel_) : + _ui(ui_), + _parallel(parallel_), + good_count(0), + bad_count(0) + { + } + + /// Called when the processing of a test case begins. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the test case being executed. + virtual void + got_test_case(const model::test_program& test_program, + const std::string& test_case_name) + { + if (!_parallel) { + _ui->out(F("%s -> ") % + cli::format_test_case_id(test_program, test_case_name), + false); + } + } + + /// Called when a result of a test case becomes available. + /// + /// \param test_program The test program containing the test case. + /// \param test_case_name The name of the test case being executed. + /// \param result The result of the execution of the test case. + /// \param duration The time it took to run the test. + virtual void + got_result(const model::test_program& test_program, + const std::string& test_case_name, + const model::test_result& result, + const datetime::delta& duration) + { + if (_parallel) { + _ui->out(F("%s -> ") % + cli::format_test_case_id(test_program, test_case_name), + false); + } + _ui->out(F("%s [%s]") % cli::format_result(result) % + cli::format_delta(duration)); + if (result.good()) + good_count++; + else + bad_count++; + } +}; + + +} // anonymous namespace + + +/// Default constructor for cmd_test. +cmd_test::cmd_test(void) : cli_command( + "test", "[test-program ...]", 0, -1, "Run tests") +{ + add_option(build_root_option); + add_option(kyuafile_option); + add_option(results_file_create_option); +} + + +/// Entry point for the "test" subcommand. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param cmdline Representation of the command line to the subcommand. +/// \param user_config The runtime configuration of the program. +/// +/// \return 0 if all tests passed, 1 otherwise. +int +cmd_test::run(cmdline::ui* ui, const cmdline::parsed_cmdline& cmdline, + const config::tree& user_config) +{ + const layout::results_id_file_pair results = layout::new_db( + results_file_create(cmdline), kyuafile_path(cmdline).branch_path()); + + const bool parallel = (user_config.lookup< config::positive_int_node >( + "parallelism") > 1); + + print_hooks hooks(ui, parallel); + const drivers::run_tests::result result = drivers::run_tests::drive( + kyuafile_path(cmdline), build_root_path(cmdline), results.second, + parse_filters(cmdline.arguments()), user_config, hooks); + + int exit_code; + if (hooks.good_count > 0 || hooks.bad_count > 0) { + ui->out(""); + if (!results.first.empty()) { + ui->out(F("Results file id is %s") % results.first); + } + ui->out(F("Results saved to %s") % results.second); + ui->out(""); + + ui->out(F("%s/%s passed (%s failed)") % hooks.good_count % + (hooks.good_count + hooks.bad_count) % hooks.bad_count); + + exit_code = (hooks.bad_count == 0 ? EXIT_SUCCESS : EXIT_FAILURE); + } else { + // TODO(jmmv): Delete created empty file; it's useless! + if (!results.first.empty()) { + ui->out(F("Results file id is %s") % results.first); + } + ui->out(F("Results saved to %s") % results.second); + exit_code = EXIT_SUCCESS; + } + + return report_unused_filters(result.unused_filters, ui) ? + EXIT_FAILURE : exit_code; +} diff --git a/cli/cmd_test.hpp b/cli/cmd_test.hpp new file mode 100644 index 000000000000..22d8422cb293 --- /dev/null +++ b/cli/cmd_test.hpp @@ -0,0 +1,54 @@ +// 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 cli/cmd_test.hpp +/// Provides the cmd_test class. + +#if !defined(CLI_CMD_TEST_HPP) +#define CLI_CMD_TEST_HPP + +#include "cli/common.hpp" + +namespace cli { + + +/// Implementation of the "test" subcommand. +class cmd_test : public cli_command +{ +public: + cmd_test(void); + + int run(utils::cmdline::ui*, const utils::cmdline::parsed_cmdline&, + const utils::config::tree&); +}; + + +} // namespace cli + + +#endif // !defined(CLI_CMD_TEST_HPP) diff --git a/cli/cmd_test_test.cpp b/cli/cmd_test_test.cpp new file mode 100644 index 000000000000..fb623323dd86 --- /dev/null +++ b/cli/cmd_test_test.cpp @@ -0,0 +1,63 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/cmd_test.hpp" + +#include <atf-c++.hpp> + +#include "cli/common.ipp" +#include "engine/config.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/config/tree.ipp" + +namespace cmdline = utils::cmdline; + + +ATF_TEST_CASE_WITHOUT_HEAD(invalid_filter); +ATF_TEST_CASE_BODY(invalid_filter) +{ + cmdline::args_vector args; + args.push_back("test"); + args.push_back("correct"); + args.push_back("incorrect:"); + + cli::cmd_test cmd; + cmdline::ui_mock ui; + ATF_REQUIRE_THROW_RE(cmdline::error, "Test case.*'incorrect:'.*empty", + cmd.main(&ui, args, engine::default_config())); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, invalid_filter); +} diff --git a/cli/common.cpp b/cli/common.cpp new file mode 100644 index 000000000000..dbb7f12f18e0 --- /dev/null +++ b/cli/common.cpp @@ -0,0 +1,411 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/common.hpp" + +#include <algorithm> +#include <fstream> +#include <iostream> +#include <stdexcept> + +#include "engine/filters.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/layout.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/logging/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +namespace cmdline = utils::cmdline; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; + +using utils::none; +using utils::optional; + + +/// Standard definition of the option to specify the build root. +const cmdline::path_option cli::build_root_option( + "build-root", + "Path to the built test programs, if different from the location of the " + "Kyuafile scripts", + "path"); + + +/// Standard definition of the option to specify a Kyuafile. +const cmdline::path_option cli::kyuafile_option( + 'k', "kyuafile", + "Path to the test suite definition", + "file", "Kyuafile"); + + +/// Standard definition of the option to specify filters on test results. +const cmdline::list_option cli::results_filter_option( + "results-filter", "Comma-separated list of result types to include in " + "the report", "types", "skipped,xfail,broken,failed"); + + +/// Standard definition of the option to specify the results file. +/// +/// TODO(jmmv): Should support a git-like syntax to go back in time, like +/// --results-file=LATEST^N where N indicates how many runs to go back to. +const cmdline::string_option cli::results_file_create_option( + 'r', "results-file", + "Path to the results file to create; if left to the default value, the " + "name of the file is automatically computed for the current test suite", + "file", layout::results_auto_create_name); + + +/// Standard definition of the option to specify the results file. +/// +/// TODO(jmmv): Should support a git-like syntax to go back in time, like +/// --results-file=LATEST^N where N indicates how many runs to go back to. +const cmdline::string_option cli::results_file_open_option( + 'r', "results-file", + "Path to the results file to open or the identifier of the current test " + "suite or a previous results file for automatic lookup; if left to the " + "default value, uses the current directory as the test suite name", + "file", layout::results_auto_open_name); + + +namespace { + + +/// Gets the path to the historical database if it exists. +/// +/// TODO(jmmv): This function should go away. It only exists as a temporary +/// transitional path to force the use of the stale ~/.kyua/store.db if it +/// exists. +/// +/// \return A path if the file is found; none otherwise. +static optional< fs::path > +get_historical_db(void) +{ + optional< fs::path > home = utils::get_home(); + if (home) { + const fs::path old_db = home.get() / ".kyua/store.db"; + if (fs::exists(old_db)) { + if (old_db.is_absolute()) + return utils::make_optional(old_db); + else + return utils::make_optional(old_db.to_absolute()); + } else { + return none; + } + } else { + return none; + } +} + + +/// Converts a set of result type names to identifiers. +/// +/// \param names The collection of names to process; may be empty. +/// +/// \return The result type identifiers corresponding to the input names. +/// +/// \throw std::runtime_error If any name in the input names is invalid. +static cli::result_types +parse_types(const std::vector< std::string >& names) +{ + typedef std::map< std::string, model::test_result_type > types_map; + types_map valid_types; + valid_types["broken"] = model::test_result_broken; + valid_types["failed"] = model::test_result_failed; + valid_types["passed"] = model::test_result_passed; + valid_types["skipped"] = model::test_result_skipped; + valid_types["xfail"] = model::test_result_expected_failure; + + cli::result_types types; + for (std::vector< std::string >::const_iterator iter = names.begin(); + iter != names.end(); ++iter) { + const types_map::const_iterator match = valid_types.find(*iter); + if (match == valid_types.end()) + throw std::runtime_error(F("Unknown result type '%s'") % *iter); + else + types.push_back((*match).second); + } + return types; +} + + +} // anonymous namespace + + +/// Gets the path to the build root, if any. +/// +/// This is just syntactic sugar to simplify quierying the 'build_root_option'. +/// +/// \param cmdline The parsed command line. +/// +/// \return The path to the build root, if specified; none otherwise. +optional< fs::path > +cli::build_root_path(const cmdline::parsed_cmdline& cmdline) +{ + optional< fs::path > build_root; + if (cmdline.has_option(build_root_option.long_name())) + build_root = cmdline.get_option< cmdline::path_option >( + build_root_option.long_name()); + return build_root; +} + + +/// Gets the path to the Kyuafile to be loaded. +/// +/// This is just syntactic sugar to simplify quierying the 'kyuafile_option'. +/// +/// \param cmdline The parsed command line. +/// +/// \return The path to the Kyuafile to be loaded. +fs::path +cli::kyuafile_path(const cmdline::parsed_cmdline& cmdline) +{ + return cmdline.get_option< cmdline::path_option >( + kyuafile_option.long_name()); +} + + +/// Gets the value of the results-file flag for the creation of a new file. +/// +/// \param cmdline The parsed command line from which to extract any possible +/// override for the location of the database via the --results-file flag. +/// +/// \return The path to the database to be used. +/// +/// \throw cmdline::error If the value passed to the flag is invalid. +std::string +cli::results_file_create(const cmdline::parsed_cmdline& cmdline) +{ + std::string results_file = cmdline.get_option< cmdline::string_option >( + results_file_create_option.long_name()); + if (results_file == results_file_create_option.default_value()) { + const optional< fs::path > historical_db = get_historical_db(); + if (historical_db) + results_file = historical_db.get().str(); + } else { + try { + (void)fs::path(results_file); + } catch (const fs::error& e) { + throw cmdline::usage_error(F("Invalid value passed to --%s") % + results_file_create_option.long_name()); + } + } + return results_file; +} + + +/// Gets the value of the results-file flag for the lookup of the file. +/// +/// \param cmdline The parsed command line from which to extract any possible +/// override for the location of the database via the --results-file flag. +/// +/// \return The path to the database to be used. +/// +/// \throw cmdline::error If the value passed to the flag is invalid. +std::string +cli::results_file_open(const cmdline::parsed_cmdline& cmdline) +{ + std::string results_file = cmdline.get_option< cmdline::string_option >( + results_file_open_option.long_name()); + if (results_file == results_file_open_option.default_value()) { + const optional< fs::path > historical_db = get_historical_db(); + if (historical_db) + results_file = historical_db.get().str(); + } else { + try { + (void)fs::path(results_file); + } catch (const fs::error& e) { + throw cmdline::usage_error(F("Invalid value passed to --%s") % + results_file_open_option.long_name()); + } + } + return results_file; +} + + +/// Gets the filters for the result types. +/// +/// \param cmdline The parsed command line. +/// +/// \return A collection of result types to be used for filtering. +/// +/// \throw std::runtime_error If any of the user-provided filters is invalid. +cli::result_types +cli::get_result_types(const utils::cmdline::parsed_cmdline& cmdline) +{ + result_types types = parse_types( + cmdline.get_option< cmdline::list_option >("results-filter")); + if (types.empty()) { + types.push_back(model::test_result_passed); + types.push_back(model::test_result_skipped); + types.push_back(model::test_result_expected_failure); + types.push_back(model::test_result_broken); + types.push_back(model::test_result_failed); + } + return types; +} + + +/// Parses a set of command-line arguments to construct test filters. +/// +/// \param args The command-line arguments representing test filters. +/// +/// \return A set of test filters. +/// +/// \throw cmdline:error If any of the arguments is invalid, or if they +/// represent a non-disjoint collection of filters. +std::set< engine::test_filter > +cli::parse_filters(const cmdline::args_vector& args) +{ + std::set< engine::test_filter > filters; + + try { + for (cmdline::args_vector::const_iterator iter = args.begin(); + iter != args.end(); iter++) { + const engine::test_filter filter(engine::test_filter::parse(*iter)); + if (filters.find(filter) != filters.end()) + throw cmdline::error(F("Duplicate filter '%s'") % filter.str()); + filters.insert(filter); + } + check_disjoint_filters(filters); + } catch (const std::runtime_error& e) { + throw cmdline::error(e.what()); + } + + return filters; +} + + +/// Reports the filters that have not matched any tests as errors. +/// +/// \param unused The collection of unused filters to report. +/// \param ui The user interface object through which errors are to be reported. +/// +/// \return True if there are any unused filters. The caller should report this +/// as an error to the user by means of a non-successful exit code. +bool +cli::report_unused_filters(const std::set< engine::test_filter >& unused, + cmdline::ui* ui) +{ + for (std::set< engine::test_filter >::const_iterator iter = unused.begin(); + iter != unused.end(); iter++) { + cmdline::print_warning(ui, F("No test cases matched by the filter " + "'%s'.") % (*iter).str()); + } + + return !unused.empty(); +} + + +/// Formats a time delta for user presentation. +/// +/// \param delta The time delta to format. +/// +/// \return A user-friendly representation of the time delta. +std::string +cli::format_delta(const datetime::delta& delta) +{ + return F("%.3ss") % (delta.seconds + (delta.useconds / 1000000.0)); +} + + +/// Formats a test case result for user presentation. +/// +/// \param result The result to format. +/// +/// \return A user-friendly representation of the result. +std::string +cli::format_result(const model::test_result& result) +{ + std::string text; + + switch (result.type()) { + case model::test_result_broken: text = "broken"; break; + case model::test_result_expected_failure: text = "expected_failure"; break; + case model::test_result_failed: text = "failed"; break; + case model::test_result_passed: text = "passed"; break; + case model::test_result_skipped: text = "skipped"; break; + } + INV(!text.empty()); + + if (!result.reason().empty()) + text += ": " + result.reason(); + + return text; +} + + +/// Formats the identifier of a test case for user presentation. +/// +/// \param test_program The test program containing the test case. +/// \param test_case_name The name of the test case. +/// +/// \return A string representing the test case uniquely within a test suite. +std::string +cli::format_test_case_id(const model::test_program& test_program, + const std::string& test_case_name) +{ + return F("%s:%s") % test_program.relative_path() % test_case_name; +} + + +/// Formats a filter using the same syntax of a test case. +/// +/// \param test_filter The filter to format. +/// +/// \return A string representing the test filter. +std::string +cli::format_test_case_id(const engine::test_filter& test_filter) +{ + return F("%s:%s") % test_filter.test_program % test_filter.test_case; +} + + +/// Prints the version header information to the interface output. +/// +/// \param ui Interface to which to write the version details. +void +cli::write_version_header(utils::cmdline::ui* ui) +{ + ui->out(PACKAGE " (" PACKAGE_NAME ") " PACKAGE_VERSION); +} diff --git a/cli/common.hpp b/cli/common.hpp new file mode 100644 index 000000000000..15a7e9fa3344 --- /dev/null +++ b/cli/common.hpp @@ -0,0 +1,104 @@ +// 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 cli/common.hpp +/// Utility functions to implement CLI subcommands. + +#if !defined(CLI_COMMON_HPP) +#define CLI_COMMON_HPP + +#include <memory> +#include <set> +#include <vector> + +#include "engine/filters_fwd.hpp" +#include "model/test_program_fwd.hpp" +#include "model/test_result.hpp" +#include "utils/cmdline/base_command.hpp" +#include "utils/cmdline/options_fwd.hpp" +#include "utils/cmdline/parser_fwd.hpp" +#include "utils/cmdline/ui_fwd.hpp" +#include "utils/config/tree_fwd.hpp" +#include "utils/datetime_fwd.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/optional_fwd.hpp" + +namespace cli { + + +extern const utils::cmdline::path_option build_root_option; +extern const utils::cmdline::path_option kyuafile_option; +extern const utils::cmdline::string_option results_file_create_option; +extern const utils::cmdline::string_option results_file_open_option; +extern const utils::cmdline::list_option results_filter_option; +extern const utils::cmdline::property_option variable_option; + + +/// Base type for commands defined in the cli module. +/// +/// All commands in Kyua receive a configuration object as their runtime +/// data parameter because the configuration file applies to all the +/// commands. +typedef utils::cmdline::base_command< utils::config::tree > cli_command; + + +/// Scoped, strictly owned pointer to a cli_command. +typedef std::auto_ptr< cli_command > cli_command_ptr; + + +/// Collection of result types. +/// +/// This is a vector rather than a set because we want to respect the order in +/// which the user provided the types. +typedef std::vector< model::test_result_type > result_types; + + +utils::optional< utils::fs::path > build_root_path( + const utils::cmdline::parsed_cmdline&); +utils::fs::path kyuafile_path(const utils::cmdline::parsed_cmdline&); +std::string results_file_create(const utils::cmdline::parsed_cmdline&); +std::string results_file_open(const utils::cmdline::parsed_cmdline&); +result_types get_result_types(const utils::cmdline::parsed_cmdline&); + +std::set< engine::test_filter > parse_filters( + const utils::cmdline::args_vector&); +bool report_unused_filters(const std::set< engine::test_filter >&, + utils::cmdline::ui*); + +std::string format_delta(const utils::datetime::delta&); +std::string format_result(const model::test_result&); +std::string format_test_case_id(const model::test_program&, const std::string&); +std::string format_test_case_id(const engine::test_filter&); + + +void write_version_header(utils::cmdline::ui*); + + +} // namespace cli + +#endif // !defined(CLI_COMMON_HPP) diff --git a/cli/common.ipp b/cli/common.ipp new file mode 100644 index 000000000000..c0de4e44ccc1 --- /dev/null +++ b/cli/common.ipp @@ -0,0 +1,30 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/common.hpp" +#include "utils/cmdline/base_command.ipp" diff --git a/cli/common_test.cpp b/cli/common_test.cpp new file mode 100644 index 000000000000..05bb187ace22 --- /dev/null +++ b/cli/common_test.cpp @@ -0,0 +1,488 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/common.hpp" + +#include <fstream> + +#include <atf-c++.hpp> + +#include "engine/exceptions.hpp" +#include "engine/filters.hpp" +#include "model/metadata.hpp" +#include "model/test_program.hpp" +#include "model/test_result.hpp" +#include "store/layout.hpp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/datetime.hpp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace layout = store::layout; + +using utils::optional; + + +namespace { + + +/// Syntactic sugar to instantiate engine::test_filter objects. +/// +/// \param test_program Test program. +/// \param test_case Test case. +/// +/// \return A \code test_filter \endcode object, based on \p test_program and +/// \p test_case. +inline engine::test_filter +mkfilter(const char* test_program, const char* test_case) +{ + return engine::test_filter(fs::path(test_program), test_case); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(build_root_path__default); +ATF_TEST_CASE_BODY(build_root_path__default) +{ + std::map< std::string, std::vector< std::string > > options; + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE(!cli::build_root_path(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(build_root_path__explicit); +ATF_TEST_CASE_BODY(build_root_path__explicit) +{ + std::map< std::string, std::vector< std::string > > options; + options["build-root"].push_back("/my//path"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE(cli::build_root_path(mock_cmdline)); + ATF_REQUIRE_EQ("/my/path", cli::build_root_path(mock_cmdline).get().str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile_path__default); +ATF_TEST_CASE_BODY(kyuafile_path__default) +{ + std::map< std::string, std::vector< std::string > > options; + options["kyuafile"].push_back(cli::kyuafile_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_EQ(cli::kyuafile_option.default_value(), + cli::kyuafile_path(mock_cmdline).str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(kyuafile_path__explicit); +ATF_TEST_CASE_BODY(kyuafile_path__explicit) +{ + std::map< std::string, std::vector< std::string > > options; + options["kyuafile"].push_back("/my//path"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_EQ("/my/path", cli::kyuafile_path(mock_cmdline).str()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(result_types__default); +ATF_TEST_CASE_BODY(result_types__default) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-filter"].push_back( + cli::results_filter_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + cli::result_types exp_types; + exp_types.push_back(model::test_result_skipped); + exp_types.push_back(model::test_result_expected_failure); + exp_types.push_back(model::test_result_broken); + exp_types.push_back(model::test_result_failed); + ATF_REQUIRE(exp_types == cli::get_result_types(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(result_types__empty); +ATF_TEST_CASE_BODY(result_types__empty) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-filter"].push_back(""); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + cli::result_types exp_types; + exp_types.push_back(model::test_result_passed); + exp_types.push_back(model::test_result_skipped); + exp_types.push_back(model::test_result_expected_failure); + exp_types.push_back(model::test_result_broken); + exp_types.push_back(model::test_result_failed); + ATF_REQUIRE(exp_types == cli::get_result_types(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(result_types__explicit__all); +ATF_TEST_CASE_BODY(result_types__explicit__all) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-filter"].push_back("passed,skipped,xfail,broken,failed"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + cli::result_types exp_types; + exp_types.push_back(model::test_result_passed); + exp_types.push_back(model::test_result_skipped); + exp_types.push_back(model::test_result_expected_failure); + exp_types.push_back(model::test_result_broken); + exp_types.push_back(model::test_result_failed); + ATF_REQUIRE(exp_types == cli::get_result_types(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(result_types__explicit__some); +ATF_TEST_CASE_BODY(result_types__explicit__some) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-filter"].push_back("skipped,broken"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + cli::result_types exp_types; + exp_types.push_back(model::test_result_skipped); + exp_types.push_back(model::test_result_broken); + ATF_REQUIRE(exp_types == cli::get_result_types(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(result_types__explicit__invalid); +ATF_TEST_CASE_BODY(result_types__explicit__invalid) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-filter"].push_back("skipped,foo,broken"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_THROW_RE(std::runtime_error, "Unknown result type 'foo'", + cli::get_result_types(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_create__default__new); +ATF_TEST_CASE_BODY(results_file_create__default__new) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back( + cli::results_file_create_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const fs::path home("homedir"); + utils::setenv("HOME", home.str()); + + ATF_REQUIRE_EQ(cli::results_file_create_option.default_value(), + cli::results_file_create(mock_cmdline)); + ATF_REQUIRE(!fs::exists(home / ".kyua")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_create__default__historical); +ATF_TEST_CASE_BODY(results_file_create__default__historical) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back( + cli::results_file_create_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const fs::path home("homedir"); + utils::setenv("HOME", home.str()); + fs::mkdir_p(fs::path("homedir/.kyua"), 0755); + atf::utils::create_file("homedir/.kyua/store.db", "fake store"); + + ATF_REQUIRE_EQ(fs::path("homedir/.kyua/store.db").to_absolute(), + fs::path(cli::results_file_create(mock_cmdline))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_create__explicit); +ATF_TEST_CASE_BODY(results_file_create__explicit) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back("/my//path/f.db"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_EQ("/my//path/f.db", + cli::results_file_create(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_open__default__latest); +ATF_TEST_CASE_BODY(results_file_open__default__latest) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back( + cli::results_file_open_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const fs::path home("homedir"); + utils::setenv("HOME", home.str()); + + ATF_REQUIRE_EQ(cli::results_file_open_option.default_value(), + cli::results_file_open(mock_cmdline)); + ATF_REQUIRE(!fs::exists(home / ".kyua")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_open__default__historical); +ATF_TEST_CASE_BODY(results_file_open__default__historical) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back( + cli::results_file_open_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const fs::path home("homedir"); + utils::setenv("HOME", home.str()); + fs::mkdir_p(fs::path("homedir/.kyua"), 0755); + atf::utils::create_file("homedir/.kyua/store.db", "fake store"); + + ATF_REQUIRE_EQ(fs::path("homedir/.kyua/store.db").to_absolute(), + fs::path(cli::results_file_open(mock_cmdline))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(results_file_open__explicit); +ATF_TEST_CASE_BODY(results_file_open__explicit) +{ + std::map< std::string, std::vector< std::string > > options; + options["results-file"].push_back("/my//path/f.db"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_EQ("/my//path/f.db", cli::results_file_open(mock_cmdline)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_filters__none); +ATF_TEST_CASE_BODY(parse_filters__none) +{ + const cmdline::args_vector args; + const std::set< engine::test_filter > filters = cli::parse_filters(args); + ATF_REQUIRE(filters.empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_filters__ok); +ATF_TEST_CASE_BODY(parse_filters__ok) +{ + cmdline::args_vector args; + args.push_back("foo"); + args.push_back("bar/baz"); + args.push_back("other:abc"); + args.push_back("other:bcd"); + const std::set< engine::test_filter > filters = cli::parse_filters(args); + + std::set< engine::test_filter > exp_filters; + exp_filters.insert(mkfilter("foo", "")); + exp_filters.insert(mkfilter("bar/baz", "")); + exp_filters.insert(mkfilter("other", "abc")); + exp_filters.insert(mkfilter("other", "bcd")); + + ATF_REQUIRE(exp_filters == filters); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_filters__duplicate); +ATF_TEST_CASE_BODY(parse_filters__duplicate) +{ + cmdline::args_vector args; + args.push_back("foo/bar//baz"); + args.push_back("hello/world:yes"); + args.push_back("foo//bar/baz"); + ATF_REQUIRE_THROW_RE(cmdline::error, "Duplicate.*'foo/bar/baz'", + cli::parse_filters(args)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(parse_filters__nondisjoint); +ATF_TEST_CASE_BODY(parse_filters__nondisjoint) +{ + cmdline::args_vector args; + args.push_back("foo/bar"); + args.push_back("hello/world:yes"); + args.push_back("foo/bar:baz"); + ATF_REQUIRE_THROW_RE(cmdline::error, "'foo/bar'.*'foo/bar:baz'.*disjoint", + cli::parse_filters(args)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(report_unused_filters__none); +ATF_TEST_CASE_BODY(report_unused_filters__none) +{ + std::set< engine::test_filter > unused; + + cmdline::ui_mock ui; + ATF_REQUIRE(!cli::report_unused_filters(unused, &ui)); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(report_unused_filters__some); +ATF_TEST_CASE_BODY(report_unused_filters__some) +{ + std::set< engine::test_filter > unused; + unused.insert(mkfilter("a/b", "")); + unused.insert(mkfilter("hey/d", "yes")); + + cmdline::ui_mock ui; + cmdline::init("progname"); + ATF_REQUIRE(cli::report_unused_filters(unused, &ui)); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE_EQ(2, ui.err_log().size()); + ATF_REQUIRE( atf::utils::grep_collection("No.*matched.*'a/b'", + ui.err_log())); + ATF_REQUIRE( atf::utils::grep_collection("No.*matched.*'hey/d:yes'", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_delta); +ATF_TEST_CASE_BODY(format_delta) +{ + ATF_REQUIRE_EQ("0.000s", cli::format_delta(datetime::delta())); + ATF_REQUIRE_EQ("0.012s", cli::format_delta(datetime::delta(0, 12300))); + ATF_REQUIRE_EQ("0.999s", cli::format_delta(datetime::delta(0, 999000))); + ATF_REQUIRE_EQ("51.321s", cli::format_delta(datetime::delta(51, 321000))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_result__no_reason); +ATF_TEST_CASE_BODY(format_result__no_reason) +{ + ATF_REQUIRE_EQ("passed", cli::format_result( + model::test_result(model::test_result_passed))); + ATF_REQUIRE_EQ("failed", cli::format_result( + model::test_result(model::test_result_failed))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_result__with_reason); +ATF_TEST_CASE_BODY(format_result__with_reason) +{ + ATF_REQUIRE_EQ("broken: Something", cli::format_result( + model::test_result(model::test_result_broken, "Something"))); + ATF_REQUIRE_EQ("expected_failure: A B C", cli::format_result( + model::test_result(model::test_result_expected_failure, "A B C"))); + ATF_REQUIRE_EQ("failed: More text", cli::format_result( + model::test_result(model::test_result_failed, "More text"))); + ATF_REQUIRE_EQ("skipped: Bye", cli::format_result( + model::test_result(model::test_result_skipped, "Bye"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_test_case_id__test_case); +ATF_TEST_CASE_BODY(format_test_case_id__test_case) +{ + const model::test_program test_program = model::test_program_builder( + "mock", fs::path("foo/bar/baz"), fs::path("unused-root"), + "unused-suite-name") + .add_test_case("abc") + .build(); + ATF_REQUIRE_EQ("foo/bar/baz:abc", + cli::format_test_case_id(test_program, "abc")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(format_test_case_id__test_filter); +ATF_TEST_CASE_BODY(format_test_case_id__test_filter) +{ + const engine::test_filter filter(fs::path("foo/bar"), "baz"); + ATF_REQUIRE_EQ("foo/bar:baz", cli::format_test_case_id(filter)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(write_version_header); +ATF_TEST_CASE_BODY(write_version_header) +{ + cmdline::ui_mock ui; + cli::write_version_header(&ui); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_MATCH("^kyua .*[0-9]+\\.[0-9]+$", ui.out_log()[0]); + ATF_REQUIRE(ui.err_log().empty()); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, build_root_path__default); + ATF_ADD_TEST_CASE(tcs, build_root_path__explicit); + + ATF_ADD_TEST_CASE(tcs, kyuafile_path__default); + ATF_ADD_TEST_CASE(tcs, kyuafile_path__explicit); + + ATF_ADD_TEST_CASE(tcs, result_types__default); + ATF_ADD_TEST_CASE(tcs, result_types__empty); + ATF_ADD_TEST_CASE(tcs, result_types__explicit__all); + ATF_ADD_TEST_CASE(tcs, result_types__explicit__some); + ATF_ADD_TEST_CASE(tcs, result_types__explicit__invalid); + + ATF_ADD_TEST_CASE(tcs, results_file_create__default__new); + ATF_ADD_TEST_CASE(tcs, results_file_create__default__historical); + ATF_ADD_TEST_CASE(tcs, results_file_create__explicit); + + ATF_ADD_TEST_CASE(tcs, results_file_open__default__latest); + ATF_ADD_TEST_CASE(tcs, results_file_open__default__historical); + ATF_ADD_TEST_CASE(tcs, results_file_open__explicit); + + ATF_ADD_TEST_CASE(tcs, parse_filters__none); + ATF_ADD_TEST_CASE(tcs, parse_filters__ok); + ATF_ADD_TEST_CASE(tcs, parse_filters__duplicate); + ATF_ADD_TEST_CASE(tcs, parse_filters__nondisjoint); + + ATF_ADD_TEST_CASE(tcs, report_unused_filters__none); + ATF_ADD_TEST_CASE(tcs, report_unused_filters__some); + + ATF_ADD_TEST_CASE(tcs, format_delta); + + ATF_ADD_TEST_CASE(tcs, format_result__no_reason); + ATF_ADD_TEST_CASE(tcs, format_result__with_reason); + + ATF_ADD_TEST_CASE(tcs, format_test_case_id__test_case); + ATF_ADD_TEST_CASE(tcs, format_test_case_id__test_filter); + + ATF_ADD_TEST_CASE(tcs, write_version_header); +} diff --git a/cli/config.cpp b/cli/config.cpp new file mode 100644 index 000000000000..0049103706bf --- /dev/null +++ b/cli/config.cpp @@ -0,0 +1,223 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/config.hpp" + +#include "cli/common.hpp" +#include "engine/config.hpp" +#include "engine/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/config/tree.ipp" +#include "utils/format/macros.hpp" +#include "utils/fs/exceptions.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/env.hpp" +#include "utils/logging/macros.hpp" +#include "utils/optional.ipp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; + +using utils::optional; + + +namespace { + + +/// Basename of the configuration file. +static const char* config_basename = "kyua.conf"; + + +/// Magic string to disable loading of configuration files. +static const char* none_config = "none"; + + +/// Textual description of the default configuration files. +/// +/// This is just an auxiliary string required to define the option below, which +/// requires a pointer to a static C string. +/// +/// \todo If the user overrides the KYUA_CONFDIR environment variable, we don't +/// reflect this fact here. We don't want to query the variable during program +/// initialization due to the side-effects it may have. Therefore, fixing this +/// is tricky as it may require a whole rethink of this module. +static const std::string config_lookup_names = + (fs::path("~/.kyua") / config_basename).str() + " or " + + (fs::path(KYUA_CONFDIR) / config_basename).str(); + + +/// Loads the configuration file for this session, if any. +/// +/// This is a helper function that does not apply user-specified overrides. See +/// the documentation for cli::load_config() for more details. +/// +/// \param cmdline The parsed command line. +/// +/// \return The loaded configuration file, or the configuration defaults if the +/// loading is disabled. +/// +/// \throw engine::error If the parsing of the configuration file fails. +/// TODO(jmmv): I'm not sure if this is the raised exception. And even if +/// it is, we should make it more accurate. +config::tree +load_config_file(const cmdline::parsed_cmdline& cmdline) +{ + // TODO(jmmv): We should really be able to use cmdline.has_option here to + // detect whether the option was provided or not instead of checking against + // the default value. + const fs::path filename = cmdline.get_option< cmdline::path_option >( + cli::config_option.long_name()); + if (filename.str() == none_config) { + LD("Configuration loading disabled; using defaults"); + return engine::default_config(); + } else if (filename.str() != cli::config_option.default_value()) + return engine::load_config(filename); + + const optional< fs::path > home = utils::get_home(); + if (home) { + const fs::path path = home.get() / ".kyua" / config_basename; + try { + if (fs::exists(path)) + return engine::load_config(path); + } catch (const fs::error& e) { + // Fall through. If we fail to load the user-specific configuration + // file because it cannot be openend, we try to load the system-wide + // one. + LW(F("Failed to load user-specific configuration file '%s': %s") % + path % e.what()); + } + } + + const fs::path confdir(utils::getenv_with_default( + "KYUA_CONFDIR", KYUA_CONFDIR)); + + const fs::path path = confdir / config_basename; + if (fs::exists(path)) { + return engine::load_config(path); + } else { + return engine::default_config(); + } +} + + +/// Loads the configuration file for this session, if any. +/// +/// This is a helper function for cli::load_config() that attempts to load the +/// configuration unconditionally. +/// +/// \param cmdline The parsed command line. +/// +/// \return The loaded configuration file data. +/// +/// \throw engine::error If the parsing of the configuration file fails. +static config::tree +load_required_config(const cmdline::parsed_cmdline& cmdline) +{ + config::tree user_config = load_config_file(cmdline); + + if (cmdline.has_option(cli::variable_option.long_name())) { + typedef std::pair< std::string, std::string > override_pair; + + const std::vector< override_pair >& overrides = + cmdline.get_multi_option< cmdline::property_option >( + cli::variable_option.long_name()); + + for (std::vector< override_pair >::const_iterator + iter = overrides.begin(); iter != overrides.end(); iter++) { + try { + user_config.set_string((*iter).first, (*iter).second); + } catch (const config::error& e) { + // TODO(jmmv): Raising this type from here is obviously the + // wrong thing to do. + throw engine::error(e.what()); + } + } + } + + return user_config; +} + + +} // anonymous namespace + + +/// Standard definition of the option to specify a configuration file. +/// +/// You must use load_config() to load a configuration file while honoring the +/// value of this flag. +const cmdline::path_option cli::config_option( + 'c', "config", + (std::string("Path to the configuration file; '") + none_config + + "' to disable loading").c_str(), + "file", config_lookup_names.c_str()); + + +/// Standard definition of the option to specify a configuration variable. +const cmdline::property_option cli::variable_option( + 'v', "variable", + "Overrides a particular configuration variable", + "K=V"); + + +/// Loads the configuration file for this session, if any. +/// +/// The algorithm implemented here is as follows: +/// 1) If ~/.kyua/kyua.conf exists, load it. +/// 2) Otherwise, if sysconfdir/kyua.conf exists, load it. +/// 3) Otherwise, use the built-in settings. +/// 4) Lastly, apply any user-provided overrides. +/// +/// \param cmdline The parsed command line. +/// \param required Whether the loading of the configuration file must succeed. +/// Some commands should run regardless, and therefore we need to set this +/// to false for those commands. +/// +/// \return The loaded configuration file data. If required was set to false, +/// this might be the default configuration data if the requested file could not +/// be properly loaded. +/// +/// \throw engine::error If the parsing of the configuration file fails. +config::tree +cli::load_config(const cmdline::parsed_cmdline& cmdline, + const bool required) +{ + try { + return load_required_config(cmdline); + } catch (const engine::error& e) { + if (required) { + throw; + } else { + LW(F("Ignoring failure to load configuration because the requested " + "command should not fail: %s") % e.what()); + return engine::default_config(); + } + } +} diff --git a/cli/config.hpp b/cli/config.hpp new file mode 100644 index 000000000000..d948208ee5d0 --- /dev/null +++ b/cli/config.hpp @@ -0,0 +1,55 @@ +// 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 cli/config.hpp +/// Utility functions to load configuration files. +/// +/// \todo All this should probably just be merged into the main module +/// as nothing else should have access to this. + +#if !defined(CLI_CONFIG_HPP) +#define CLI_CONFIG_HPP + +#include "utils/cmdline/options_fwd.hpp" +#include "utils/cmdline/parser_fwd.hpp" +#include "utils/config/tree_fwd.hpp" + +namespace cli { + + +extern const utils::cmdline::path_option config_option; +extern const utils::cmdline::property_option variable_option; + + +utils::config::tree load_config(const utils::cmdline::parsed_cmdline&, + const bool); + + +} // namespace cli + +#endif // !defined(CLI_CONFIG_HPP) diff --git a/cli/config_test.cpp b/cli/config_test.cpp new file mode 100644 index 000000000000..7a20c2941d8c --- /dev/null +++ b/cli/config_test.cpp @@ -0,0 +1,351 @@ +// Copyright 2011 The Kyua Authors. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "cli/config.hpp" + +#include <atf-c++.hpp> + +#include "engine/config.hpp" +#include "engine/exceptions.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/config/tree.ipp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; + + +namespace { + + +/// Creates a configuration file for testing purposes. +/// +/// To ensure that the loaded file is the one created by this function, use +/// validate_mock_config(). +/// +/// \param name The name of the configuration file to create. +/// \param cookie The magic value to set in the configuration file, or NULL if a +/// broken configuration file is desired. +static void +create_mock_config(const char* name, const char* cookie) +{ + if (cookie != NULL) { + atf::utils::create_file( + name, + F("syntax(2)\n" + "test_suites.suite.magic_value = '%s'\n") % cookie); + } else { + atf::utils::create_file(name, "syntax(200)\n"); + } +} + + +/// Creates an invalid system configuration. +/// +/// \param cookie The magic value to set in the configuration file, or NULL if a +/// broken configuration file is desired. +static void +mock_system_config(const char* cookie) +{ + fs::mkdir(fs::path("system-dir"), 0755); + utils::setenv("KYUA_CONFDIR", (fs::current_path() / "system-dir").str()); + create_mock_config("system-dir/kyua.conf", cookie); +} + + +/// Creates an invalid user configuration. +/// +/// \param cookie The magic value to set in the configuration file, or NULL if a +/// broken configuration file is desired. +static void +mock_user_config(const char* cookie) +{ + fs::mkdir(fs::path("user-dir"), 0755); + fs::mkdir(fs::path("user-dir/.kyua"), 0755); + utils::setenv("HOME", (fs::current_path() / "user-dir").str()); + create_mock_config("user-dir/.kyua/kyua.conf", cookie); +} + + +/// Ensures that a loaded configuration was created with create_mock_config(). +/// +/// \param user_config The configuration to validate. +/// \param cookie The magic value to expect in the configuration file. +static void +validate_mock_config(const config::tree& user_config, const char* cookie) +{ + const config::properties_map& properties = user_config.all_properties( + "test_suites.suite", true); + const config::properties_map::const_iterator iter = + properties.find("magic_value"); + ATF_REQUIRE(iter != properties.end()); + ATF_REQUIRE_EQ(cookie, (*iter).second); +} + + +/// Ensures that two configuration trees are equal. +/// +/// \param exp_tree The expected configuration tree. +/// \param actual_tree The configuration tree being validated against exp_tree. +static void +require_eq(const config::tree& exp_tree, const config::tree& actual_tree) +{ + ATF_REQUIRE(exp_tree.all_properties() == actual_tree.all_properties()); +} + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__none); +ATF_TEST_CASE_BODY(load_config__none) +{ + utils::setenv("KYUA_CONFDIR", "/the/system/does/not/exist"); + utils::setenv("HOME", "/the/user/does/not/exist"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + require_eq(engine::default_config(), + cli::load_config(mock_cmdline, true)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__explicit__ok); +ATF_TEST_CASE_BODY(load_config__explicit__ok) +{ + mock_system_config(NULL); + mock_user_config(NULL); + + create_mock_config("test-file", "hello"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back("test-file"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + validate_mock_config(user_config, "hello"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__explicit__disable); +ATF_TEST_CASE_BODY(load_config__explicit__disable) +{ + mock_system_config(NULL); + mock_user_config(NULL); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back("none"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + require_eq(engine::default_config(), + cli::load_config(mock_cmdline, true)); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__explicit__fail); +ATF_TEST_CASE_BODY(load_config__explicit__fail) +{ + mock_system_config("ok1"); + mock_user_config("ok2"); + + create_mock_config("test-file", NULL); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back("test-file"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_THROW_RE(engine::error, "200", + cli::load_config(mock_cmdline, true)); + + const config::tree config = cli::load_config(mock_cmdline, false); + require_eq(engine::default_config(), config); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__user__ok); +ATF_TEST_CASE_BODY(load_config__user__ok) +{ + mock_system_config(NULL); + mock_user_config("I am the user config"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + validate_mock_config(user_config, "I am the user config"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__user__fail); +ATF_TEST_CASE_BODY(load_config__user__fail) +{ + mock_system_config("valid"); + mock_user_config(NULL); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_THROW_RE(engine::error, "200", + cli::load_config(mock_cmdline, true)); + + const config::tree config = cli::load_config(mock_cmdline, false); + require_eq(engine::default_config(), config); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__user__bad_home); +ATF_TEST_CASE_BODY(load_config__user__bad_home) +{ + mock_system_config("Fallback system config"); + utils::setenv("HOME", ""); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + validate_mock_config(user_config, "Fallback system config"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__system__ok); +ATF_TEST_CASE_BODY(load_config__system__ok) +{ + mock_system_config("I am the system config"); + utils::setenv("HOME", "/the/user/does/not/exist"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + validate_mock_config(user_config, "I am the system config"); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__system__fail); +ATF_TEST_CASE_BODY(load_config__system__fail) +{ + mock_system_config(NULL); + utils::setenv("HOME", "/the/user/does/not/exist"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_THROW_RE(engine::error, "200", + cli::load_config(mock_cmdline, true)); + + const config::tree config = cli::load_config(mock_cmdline, false); + require_eq(engine::default_config(), config); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__overrides__no); +ATF_TEST_CASE_BODY(load_config__overrides__no) +{ + utils::setenv("KYUA_CONFDIR", fs::current_path().str()); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + options["variable"].push_back("architecture=1"); + options["variable"].push_back("platform=2"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + ATF_REQUIRE_EQ("1", + user_config.lookup< config::string_node >("architecture")); + ATF_REQUIRE_EQ("2", + user_config.lookup< config::string_node >("platform")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__overrides__yes); +ATF_TEST_CASE_BODY(load_config__overrides__yes) +{ + atf::utils::create_file( + "config", + "syntax(2)\n" + "architecture = 'do not see me'\n" + "platform = 'see me'\n"); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back("config"); + options["variable"].push_back("architecture=overriden"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + const config::tree user_config = cli::load_config(mock_cmdline, true); + ATF_REQUIRE_EQ("overriden", + user_config.lookup< config::string_node >("architecture")); + ATF_REQUIRE_EQ("see me", + user_config.lookup< config::string_node >("platform")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(load_config__overrides__fail); +ATF_TEST_CASE_BODY(load_config__overrides__fail) +{ + utils::setenv("KYUA_CONFDIR", fs::current_path().str()); + + std::map< std::string, std::vector< std::string > > options; + options["config"].push_back(cli::config_option.default_value()); + options["variable"].push_back(".a=d"); + const cmdline::parsed_cmdline mock_cmdline(options, cmdline::args_vector()); + + ATF_REQUIRE_THROW_RE(engine::error, "Empty component in key.*'\\.a'", + cli::load_config(mock_cmdline, true)); + + const config::tree config = cli::load_config(mock_cmdline, false); + require_eq(engine::default_config(), config); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, load_config__none); + ATF_ADD_TEST_CASE(tcs, load_config__explicit__ok); + ATF_ADD_TEST_CASE(tcs, load_config__explicit__disable); + ATF_ADD_TEST_CASE(tcs, load_config__explicit__fail); + ATF_ADD_TEST_CASE(tcs, load_config__user__ok); + ATF_ADD_TEST_CASE(tcs, load_config__user__fail); + ATF_ADD_TEST_CASE(tcs, load_config__user__bad_home); + ATF_ADD_TEST_CASE(tcs, load_config__system__ok); + ATF_ADD_TEST_CASE(tcs, load_config__system__fail); + ATF_ADD_TEST_CASE(tcs, load_config__overrides__no); + ATF_ADD_TEST_CASE(tcs, load_config__overrides__yes); + ATF_ADD_TEST_CASE(tcs, load_config__overrides__fail); +} diff --git a/cli/main.cpp b/cli/main.cpp new file mode 100644 index 000000000000..531c252b0a75 --- /dev/null +++ b/cli/main.cpp @@ -0,0 +1,356 @@ +// 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 "cli/main.hpp" + +#if defined(HAVE_CONFIG_H) +# include "config.h" +#endif + +extern "C" { +#include <signal.h> +#include <unistd.h> +} + +#include <cstdlib> +#include <iostream> +#include <string> +#include <utility> + +#include "cli/cmd_about.hpp" +#include "cli/cmd_config.hpp" +#include "cli/cmd_db_exec.hpp" +#include "cli/cmd_db_migrate.hpp" +#include "cli/cmd_debug.hpp" +#include "cli/cmd_help.hpp" +#include "cli/cmd_list.hpp" +#include "cli/cmd_report.hpp" +#include "cli/cmd_report_html.hpp" +#include "cli/cmd_report_junit.hpp" +#include "cli/cmd_test.hpp" +#include "cli/common.ipp" +#include "cli/config.hpp" +#include "engine/atf.hpp" +#include "engine/plain.hpp" +#include "engine/scheduler.hpp" +#include "engine/tap.hpp" +#include "store/exceptions.hpp" +#include "utils/cmdline/commands_map.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.ipp" +#include "utils/cmdline/ui.hpp" +#include "utils/config/tree.ipp" +#include "utils/env.hpp" +#include "utils/format/macros.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/logging/operations.hpp" +#include "utils/optional.ipp" +#include "utils/sanity.hpp" +#include "utils/signals/exceptions.hpp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace signals = utils::signals; +namespace scheduler = engine::scheduler; + +using utils::none; +using utils::optional; + + +namespace { + + +/// Registers all valid scheduler interfaces. +/// +/// This is part of Kyua's setup but it is a bit strange to find it here. I am +/// not sure what a better location would be though, so for now this is good +/// enough. +static void +register_scheduler_interfaces(void) +{ + scheduler::register_interface( + "atf", std::shared_ptr< scheduler::interface >( + new engine::atf_interface())); + scheduler::register_interface( + "plain", std::shared_ptr< scheduler::interface >( + new engine::plain_interface())); + scheduler::register_interface( + "tap", std::shared_ptr< scheduler::interface >( + new engine::tap_interface())); +} + + +/// Executes the given subcommand with proper usage_error reporting. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param command The subcommand to execute. +/// \param args The part of the command line passed to the subcommand. The +/// first item of this collection must match the command name. +/// \param user_config The runtime configuration to pass to the subcommand. +/// +/// \return The exit code of the command. Typically 0 on success, some other +/// integer otherwise. +/// +/// \throw cmdline::usage_error If the user input to the subcommand is invalid. +/// This error does not encode the command name within it, so this function +/// extends the message in the error to specify which subcommand was +/// affected. +/// \throw std::exception This propagates any uncaught exception. Such +/// exceptions are bugs, but we let them propagate so that the runtime will +/// abort and dump core. +static int +run_subcommand(cmdline::ui* ui, cli::cli_command* command, + const cmdline::args_vector& args, + const config::tree& user_config) +{ + try { + PRE(command->name() == args[0]); + return command->main(ui, args, user_config); + } catch (const cmdline::usage_error& e) { + throw std::pair< std::string, cmdline::usage_error >( + command->name(), e); + } +} + + +/// Exception-safe version of main. +/// +/// This function provides the real meat of the entry point of the program. It +/// is allowed to throw some known exceptions which are parsed by the caller. +/// Doing so keeps this function simpler and allow tests to actually validate +/// that the errors reported are accurate. +/// +/// \return The exit code of the program. Should be EXIT_SUCCESS on success and +/// EXIT_FAILURE on failure. The caller extends this to additional integers for +/// errors reported through exceptions. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param argc The number of arguments passed on the command line. +/// \param argv NULL-terminated array containing the command line arguments. +/// \param mock_command An extra command provided for testing purposes; should +/// just be NULL other than for tests. +/// +/// \throw cmdline::usage_error If the user ran the program with invalid +/// arguments. +/// \throw std::exception This propagates any uncaught exception. Such +/// exceptions are bugs, but we let them propagate so that the runtime will +/// abort and dump core. +static int +safe_main(cmdline::ui* ui, int argc, const char* const argv[], + cli::cli_command_ptr mock_command) +{ + cmdline::options_vector options; + options.push_back(&cli::config_option); + options.push_back(&cli::variable_option); + const cmdline::string_option loglevel_option( + "loglevel", "Level of the messages to log", "level", "info"); + options.push_back(&loglevel_option); + const cmdline::path_option logfile_option( + "logfile", "Path to the log file", "file", + cli::detail::default_log_name().c_str()); + options.push_back(&logfile_option); + + cmdline::commands_map< cli::cli_command > commands; + + commands.insert(new cli::cmd_about()); + commands.insert(new cli::cmd_config()); + commands.insert(new cli::cmd_db_exec()); + commands.insert(new cli::cmd_db_migrate()); + commands.insert(new cli::cmd_help(&options, &commands)); + + commands.insert(new cli::cmd_debug(), "Workspace"); + commands.insert(new cli::cmd_list(), "Workspace"); + commands.insert(new cli::cmd_test(), "Workspace"); + + commands.insert(new cli::cmd_report(), "Reporting"); + commands.insert(new cli::cmd_report_html(), "Reporting"); + commands.insert(new cli::cmd_report_junit(), "Reporting"); + + if (mock_command.get() != NULL) + commands.insert(mock_command); + + const cmdline::parsed_cmdline cmdline = cmdline::parse(argc, argv, options); + + const fs::path logfile(cmdline.get_option< cmdline::path_option >( + "logfile")); + fs::mkdir_p(logfile.branch_path(), 0755); + LD(F("Log file is %s") % logfile); + utils::install_crash_handlers(logfile.str()); + try { + logging::set_persistency(cmdline.get_option< cmdline::string_option >( + "loglevel"), logfile); + } catch (const std::range_error& e) { + throw cmdline::usage_error(e.what()); + } + + if (cmdline.arguments().empty()) + throw cmdline::usage_error("No command provided"); + const std::string cmdname = cmdline.arguments()[0]; + + const config::tree user_config = cli::load_config(cmdline, + cmdname != "help"); + + cli::cli_command* command = commands.find(cmdname); + if (command == NULL) + throw cmdline::usage_error(F("Unknown command '%s'") % cmdname); + register_scheduler_interfaces(); + return run_subcommand(ui, command, cmdline.arguments(), user_config); +} + + +} // anonymous namespace + + +/// Gets the name of the default log file. +/// +/// \return The path to the log file. +fs::path +cli::detail::default_log_name(void) +{ + // Update doc/troubleshooting.texi if you change this algorithm. + const optional< std::string > home(utils::getenv("HOME")); + if (home) { + return logging::generate_log_name(fs::path(home.get()) / ".kyua" / + "logs", cmdline::progname()); + } else { + const optional< std::string > tmpdir(utils::getenv("TMPDIR")); + if (tmpdir) { + return logging::generate_log_name(fs::path(tmpdir.get()), + cmdline::progname()); + } else { + return logging::generate_log_name(fs::path("/tmp"), + cmdline::progname()); + } + } +} + + +/// Testable entry point, with catch-all exception handlers. +/// +/// This entry point does not perform any initialization of global state; it is +/// provided to allow unit-testing of the utility's entry point. +/// +/// \param ui Object to interact with the I/O of the program. +/// \param argc The number of arguments passed on the command line. +/// \param argv NULL-terminated array containing the command line arguments. +/// \param mock_command An extra command provided for testing purposes; should +/// just be NULL other than for tests. +/// +/// \return 0 on success, some other integer on error. +/// +/// \throw std::exception This propagates any uncaught exception. Such +/// exceptions are bugs, but we let them propagate so that the runtime will +/// abort and dump core. +int +cli::main(cmdline::ui* ui, const int argc, const char* const* const argv, + cli_command_ptr mock_command) +{ + try { + const int exit_code = safe_main(ui, argc, argv, mock_command); + + // Codes above 1 are reserved to report conditions captured as + // exceptions below. + INV(exit_code == EXIT_SUCCESS || exit_code == EXIT_FAILURE); + + return exit_code; + } catch (const signals::interrupted_error& e) { + cmdline::print_error(ui, F("%s.") % e.what()); + // Re-deliver the interruption signal to self so that we terminate with + // the right status. At this point we should NOT have any custom signal + // handlers in place. + ::kill(getpid(), e.signo()); + LD("Interrupt signal re-delivery did not terminate program"); + // If we reach this, something went wrong because we did not exit as + // intended. Return an internal error instead. (Would be nicer to + // abort in principle, but it wouldn't be a nice experience if it ever + // happened.) + return 2; + } catch (const std::pair< std::string, cmdline::usage_error >& e) { + const std::string message = F("Usage error for command %s: %s.") % + e.first % e.second.what(); + LE(message); + ui->err(message); + ui->err(F("Type '%s help %s' for usage information.") % + cmdline::progname() % e.first); + return 3; + } catch (const cmdline::usage_error& e) { + const std::string message = F("Usage error: %s.") % e.what(); + LE(message); + ui->err(message); + ui->err(F("Type '%s help' for usage information.") % + cmdline::progname()); + return 3; + } catch (const store::old_schema_error& e) { + const std::string message = F("The database has schema version %s, " + "which is too old; please use db-migrate " + "to upgrade it.") % e.old_version(); + cmdline::print_error(ui, message); + return 2; + } catch (const std::runtime_error& e) { + cmdline::print_error(ui, F("%s.") % e.what()); + return 2; + } +} + + +/// Delegate for ::main(). +/// +/// This function is supposed to be called directly from the top-level ::main() +/// function. It takes care of initializing internal libraries and then calls +/// main(ui, argc, argv). +/// +/// \pre This function can only be called once. +/// +/// \throw std::exception This propagates any uncaught exception. Such +/// exceptions are bugs, but we let them propagate so that the runtime will +/// abort and dump core. +int +cli::main(const int argc, const char* const* const argv) +{ + logging::set_inmemory(); + + LI(F("%s %s") % PACKAGE % VERSION); + + std::string plain_args; + for (const char* const* arg = argv; *arg != NULL; arg++) + plain_args += F(" %s") % *arg; + LI(F("Command line:%s") % plain_args); + + cmdline::init(argv[0]); + cmdline::ui ui; + + const int exit_code = main(&ui, argc, argv); + LI(F("Clean exit with code %s") % exit_code); + return exit_code; +} diff --git a/cli/main.hpp b/cli/main.hpp new file mode 100644 index 000000000000..00e53c5a4ab2 --- /dev/null +++ b/cli/main.hpp @@ -0,0 +1,61 @@ +// 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 cli/main.hpp +/// Entry point for the program. +/// +/// These entry points are separate from the top-level ::main() function to +/// allow unit-testing of the main code. + +#if !defined(CLI_MAIN_HPP) +#define CLI_MAIN_HPP + +#include "cli/common.hpp" +#include "utils/cmdline/ui_fwd.hpp" +#include "utils/fs/path_fwd.hpp" + +namespace cli { + + +namespace detail { + + +utils::fs::path default_log_name(void); + + +} // namespace detail + + +int main(utils::cmdline::ui*, const int, const char* const* const, + cli_command_ptr = cli_command_ptr()); +int main(const int, const char* const* const); + + +} // namespace cli + +#endif // !defined(CLI_MAIN_HPP) diff --git a/cli/main_test.cpp b/cli/main_test.cpp new file mode 100644 index 000000000000..70d167ff6963 --- /dev/null +++ b/cli/main_test.cpp @@ -0,0 +1,489 @@ +// 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 "cli/main.hpp" + +extern "C" { +#include <signal.h> +} + +#include <cstdlib> + +#include <atf-c++.hpp> + +#include "utils/cmdline/base_command.ipp" +#include "utils/cmdline/exceptions.hpp" +#include "utils/cmdline/globals.hpp" +#include "utils/cmdline/options.hpp" +#include "utils/cmdline/parser.hpp" +#include "utils/cmdline/ui_mock.hpp" +#include "utils/datetime.hpp" +#include "utils/defs.hpp" +#include "utils/env.hpp" +#include "utils/fs/operations.hpp" +#include "utils/fs/path.hpp" +#include "utils/logging/macros.hpp" +#include "utils/logging/operations.hpp" +#include "utils/process/child.ipp" +#include "utils/process/status.hpp" +#include "utils/test_utils.ipp" + +namespace cmdline = utils::cmdline; +namespace config = utils::config; +namespace datetime = utils::datetime; +namespace fs = utils::fs; +namespace logging = utils::logging; +namespace process = utils::process; + + +namespace { + + +/// Fake command implementation that crashes during its execution. +class cmd_mock_crash : public cli::cli_command { +public: + /// Constructs a new mock command. + /// + /// All command parameters are set to irrelevant values. + cmd_mock_crash(void) : + cli::cli_command("mock_error", "", 0, 0, "Mock command that crashes") + { + } + + /// Runs the mock command. + /// + /// \return Nothing because this function always aborts. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& /* cmdline */, + const config::tree& /* user_config */) + { + utils::abort_without_coredump(); + } +}; + + +/// Fake command implementation that throws an exception during its execution. +class cmd_mock_error : public cli::cli_command { + /// Whether the command raises an exception captured by the parent or not. + /// + /// If this is true, the command will raise a std::runtime_error exception + /// or a subclass of it. The main program is in charge of capturing these + /// and reporting them appropriately. If false, this raises another + /// exception that does not inherit from std::runtime_error. + bool _unhandled; + +public: + /// Constructs a new mock command. + /// + /// \param unhandled If true, make run raise an exception not catched by the + /// main program. + cmd_mock_error(const bool unhandled) : + cli::cli_command("mock_error", "", 0, 0, + "Mock command that raises an error"), + _unhandled(unhandled) + { + } + + /// Runs the mock command. + /// + /// \return Nothing because this function always aborts. + /// + /// \throw std::logic_error If _unhandled is true. + /// \throw std::runtime_error If _unhandled is false. + int + run(cmdline::ui* /* ui */, + const cmdline::parsed_cmdline& /* cmdline */, + const config::tree& /* user_config */) + { + if (_unhandled) + throw std::logic_error("This is unhandled"); + else + throw std::runtime_error("Runtime error"); + } +}; + + +/// Fake command implementation that prints messages during its execution. +class cmd_mock_write : public cli::cli_command { +public: + /// Constructs a new mock command. + /// + /// All command parameters are set to irrelevant values. + cmd_mock_write(void) : cli::cli_command( + "mock_write", "", 0, 0, "Mock command that prints output") + { + } + + /// Runs the mock command. + /// + /// \param ui Object to interact with the I/O of the program. + /// + /// \return Nothing because this function always aborts. + int + run(cmdline::ui* ui, + const cmdline::parsed_cmdline& /* cmdline */, + const config::tree& /* user_config */) + { + ui->out("stdout message from subcommand"); + ui->err("stderr message from subcommand"); + return EXIT_FAILURE; + } +}; + + +} // anonymous namespace + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__default_log_name__home); +ATF_TEST_CASE_BODY(detail__default_log_name__home) +{ + datetime::set_mock_now(2011, 2, 21, 21, 10, 30, 0); + cmdline::init("progname1"); + + utils::setenv("HOME", "/home//fake"); + utils::setenv("TMPDIR", "/do/not/use/this"); + ATF_REQUIRE_EQ( + fs::path("/home/fake/.kyua/logs/progname1.20110221-211030.log"), + cli::detail::default_log_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__default_log_name__tmpdir); +ATF_TEST_CASE_BODY(detail__default_log_name__tmpdir) +{ + datetime::set_mock_now(2011, 2, 21, 21, 10, 50, 987); + cmdline::init("progname2"); + + utils::unsetenv("HOME"); + utils::setenv("TMPDIR", "/a/b//c"); + ATF_REQUIRE_EQ(fs::path("/a/b/c/progname2.20110221-211050.log"), + cli::detail::default_log_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(detail__default_log_name__hardcoded); +ATF_TEST_CASE_BODY(detail__default_log_name__hardcoded) +{ + datetime::set_mock_now(2011, 2, 21, 21, 15, 00, 123456); + cmdline::init("progname3"); + + utils::unsetenv("HOME"); + utils::unsetenv("TMPDIR"); + ATF_REQUIRE_EQ(fs::path("/tmp/progname3.20110221-211500.log"), + cli::detail::default_log_name()); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__no_args); +ATF_TEST_CASE_BODY(main__no_args) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 1; + const char* const argv[] = {"progname", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(atf::utils::grep_collection("Usage error: No command provided", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Type.*progname help", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__unknown_command); +ATF_TEST_CASE_BODY(main__unknown_command) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "foo", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(atf::utils::grep_collection("Usage error: Unknown command.*foo", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Type.*progname help", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__logfile__default); +ATF_TEST_CASE_BODY(main__logfile__default) +{ + logging::set_inmemory(); + datetime::set_mock_now(2011, 2, 21, 21, 30, 00, 0); + cmdline::init("progname"); + + const int argc = 1; + const char* const argv[] = {"progname", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE(!fs::exists(fs::path( + ".kyua/logs/progname.20110221-213000.log"))); + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(fs::exists(fs::path( + ".kyua/logs/progname.20110221-213000.log"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__logfile__override); +ATF_TEST_CASE_BODY(main__logfile__override) +{ + logging::set_inmemory(); + datetime::set_mock_now(2011, 2, 21, 21, 30, 00, 321); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "--logfile=test.log", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE(!fs::exists(fs::path("test.log"))); + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(!fs::exists(fs::path( + ".kyua/logs/progname.20110221-213000.log"))); + ATF_REQUIRE(fs::exists(fs::path("test.log"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__loglevel__default); +ATF_TEST_CASE_BODY(main__loglevel__default) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "--logfile=test.log", NULL}; + + LD("Mock debug message"); + LE("Mock error message"); + LI("Mock info message"); + LW("Mock warning message"); + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(!atf::utils::grep_file("Mock debug message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock error message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock info message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock warning message", "test.log")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__loglevel__higher); +ATF_TEST_CASE_BODY(main__loglevel__higher) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 3; + const char* const argv[] = {"progname", "--logfile=test.log", + "--loglevel=debug", NULL}; + + LD("Mock debug message"); + LE("Mock error message"); + LI("Mock info message"); + LW("Mock warning message"); + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(atf::utils::grep_file("Mock debug message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock error message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock info message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock warning message", "test.log")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__loglevel__lower); +ATF_TEST_CASE_BODY(main__loglevel__lower) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 3; + const char* const argv[] = {"progname", "--logfile=test.log", + "--loglevel=warning", NULL}; + + LD("Mock debug message"); + LE("Mock error message"); + LI("Mock info message"); + LW("Mock warning message"); + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(!atf::utils::grep_file("Mock debug message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock error message", "test.log")); + ATF_REQUIRE(!atf::utils::grep_file("Mock info message", "test.log")); + ATF_REQUIRE(atf::utils::grep_file("Mock warning message", "test.log")); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__loglevel__error); +ATF_TEST_CASE_BODY(main__loglevel__error) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 3; + const char* const argv[] = {"progname", "--logfile=test.log", + "--loglevel=i-am-invalid", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, cli::main(&ui, argc, argv)); + ATF_REQUIRE(atf::utils::grep_collection("Usage error.*i-am-invalid", + ui.err_log())); + ATF_REQUIRE(!fs::exists(fs::path("test.log"))); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__subcommand__ok); +ATF_TEST_CASE_BODY(main__subcommand__ok) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "mock_write", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(EXIT_FAILURE, + cli::main(&ui, argc, argv, + cli::cli_command_ptr(new cmd_mock_write()))); + ATF_REQUIRE_EQ(1, ui.out_log().size()); + ATF_REQUIRE_EQ("stdout message from subcommand", ui.out_log()[0]); + ATF_REQUIRE_EQ(1, ui.err_log().size()); + ATF_REQUIRE_EQ("stderr message from subcommand", ui.err_log()[0]); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__subcommand__invalid_args); +ATF_TEST_CASE_BODY(main__subcommand__invalid_args) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 3; + const char* const argv[] = {"progname", "mock_write", "bar", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(3, + cli::main(&ui, argc, argv, + cli::cli_command_ptr(new cmd_mock_write()))); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(atf::utils::grep_collection( + "Usage error for command mock_write: Too many arguments.", + ui.err_log())); + ATF_REQUIRE(atf::utils::grep_collection("Type.*progname help", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__subcommand__runtime_error); +ATF_TEST_CASE_BODY(main__subcommand__runtime_error) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "mock_error", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_EQ(2, cli::main(&ui, argc, argv, + cli::cli_command_ptr(new cmd_mock_error(false)))); + ATF_REQUIRE(ui.out_log().empty()); + ATF_REQUIRE(atf::utils::grep_collection("progname: E: Runtime error.", + ui.err_log())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__subcommand__unhandled_exception); +ATF_TEST_CASE_BODY(main__subcommand__unhandled_exception) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "mock_error", NULL}; + + cmdline::ui_mock ui; + ATF_REQUIRE_THROW(std::logic_error, cli::main(&ui, argc, argv, + cli::cli_command_ptr(new cmd_mock_error(true)))); +} + + +static void +do_subcommand_crash(void) +{ + logging::set_inmemory(); + cmdline::init("progname"); + + const int argc = 2; + const char* const argv[] = {"progname", "mock_error", NULL}; + + cmdline::ui_mock ui; + cli::main(&ui, argc, argv, + cli::cli_command_ptr(new cmd_mock_crash())); +} + + +ATF_TEST_CASE_WITHOUT_HEAD(main__subcommand__crash); +ATF_TEST_CASE_BODY(main__subcommand__crash) +{ + const process::status status = process::child::fork_files( + do_subcommand_crash, fs::path("stdout.txt"), + fs::path("stderr.txt"))->wait(); + ATF_REQUIRE(status.signaled()); + ATF_REQUIRE_EQ(SIGABRT, status.termsig()); + ATF_REQUIRE(atf::utils::grep_file("Fatal signal", "stderr.txt")); +} + + +ATF_INIT_TEST_CASES(tcs) +{ + ATF_ADD_TEST_CASE(tcs, detail__default_log_name__home); + ATF_ADD_TEST_CASE(tcs, detail__default_log_name__tmpdir); + ATF_ADD_TEST_CASE(tcs, detail__default_log_name__hardcoded); + + ATF_ADD_TEST_CASE(tcs, main__no_args); + ATF_ADD_TEST_CASE(tcs, main__unknown_command); + ATF_ADD_TEST_CASE(tcs, main__logfile__default); + ATF_ADD_TEST_CASE(tcs, main__logfile__override); + ATF_ADD_TEST_CASE(tcs, main__loglevel__default); + ATF_ADD_TEST_CASE(tcs, main__loglevel__higher); + ATF_ADD_TEST_CASE(tcs, main__loglevel__lower); + ATF_ADD_TEST_CASE(tcs, main__loglevel__error); + ATF_ADD_TEST_CASE(tcs, main__subcommand__ok); + ATF_ADD_TEST_CASE(tcs, main__subcommand__invalid_args); + ATF_ADD_TEST_CASE(tcs, main__subcommand__runtime_error); + ATF_ADD_TEST_CASE(tcs, main__subcommand__unhandled_exception); + ATF_ADD_TEST_CASE(tcs, main__subcommand__crash); +} |