aboutsummaryrefslogtreecommitdiff
path: root/cli
diff options
context:
space:
mode:
Diffstat (limited to 'cli')
-rw-r--r--cli/Kyuafile14
-rw-r--r--cli/Makefile.am.inc123
-rw-r--r--cli/cmd_about.cpp160
-rw-r--r--cli/cmd_about.hpp57
-rw-r--r--cli/cmd_about_test.cpp306
-rw-r--r--cli/cmd_config.cpp122
-rw-r--r--cli/cmd_config.hpp54
-rw-r--r--cli/cmd_config_test.cpp144
-rw-r--r--cli/cmd_db_exec.cpp200
-rw-r--r--cli/cmd_db_exec.hpp61
-rw-r--r--cli/cmd_db_exec_test.cpp165
-rw-r--r--cli/cmd_db_migrate.cpp82
-rw-r--r--cli/cmd_db_migrate.hpp54
-rw-r--r--cli/cmd_debug.cpp94
-rw-r--r--cli/cmd_debug.hpp54
-rw-r--r--cli/cmd_debug_test.cpp82
-rw-r--r--cli/cmd_help.cpp250
-rw-r--r--cli/cmd_help.hpp62
-rw-r--r--cli/cmd_help_test.cpp347
-rw-r--r--cli/cmd_list.cpp161
-rw-r--r--cli/cmd_list.hpp65
-rw-r--r--cli/cmd_list_test.cpp112
-rw-r--r--cli/cmd_report.cpp421
-rw-r--r--cli/cmd_report.hpp54
-rw-r--r--cli/cmd_report_html.cpp474
-rw-r--r--cli/cmd_report_html.hpp55
-rw-r--r--cli/cmd_report_junit.cpp89
-rw-r--r--cli/cmd_report_junit.hpp54
-rw-r--r--cli/cmd_test.cpp186
-rw-r--r--cli/cmd_test.hpp54
-rw-r--r--cli/cmd_test_test.cpp63
-rw-r--r--cli/common.cpp411
-rw-r--r--cli/common.hpp104
-rw-r--r--cli/common.ipp30
-rw-r--r--cli/common_test.cpp488
-rw-r--r--cli/config.cpp223
-rw-r--r--cli/config.hpp55
-rw-r--r--cli/config_test.cpp351
-rw-r--r--cli/main.cpp356
-rw-r--r--cli/main.hpp61
-rw-r--r--cli/main_test.cpp489
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);
+}