1 // Copyright 2012 Google Inc. 2 // All rights reserved. 3 // 4 // Redistribution and use in source and binary forms, with or without 5 // modification, are permitted provided that the following conditions are 6 // met: 7 // 8 // * Redistributions of source code must retain the above copyright 9 // notice, this list of conditions and the following disclaimer. 10 // * Redistributions in binary form must reproduce the above copyright 11 // notice, this list of conditions and the following disclaimer in the 12 // documentation and/or other materials provided with the distribution. 13 // * Neither the name of Google Inc. nor the names of its contributors 14 // may be used to endorse or promote products derived from this software 15 // without specific prior written permission. 16 // 17 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 29 #include "cli/cmd_report_html.hpp" 30 31 #include <cerrno> 32 #include <cstdlib> 33 #include <stdexcept> 34 35 #include "cli/common.ipp" 36 #include "engine/action.hpp" 37 #include "engine/context.hpp" 38 #include "engine/drivers/scan_action.hpp" 39 #include "engine/test_result.hpp" 40 #include "utils/cmdline/options.hpp" 41 #include "utils/cmdline/parser.ipp" 42 #include "utils/datetime.hpp" 43 #include "utils/env.hpp" 44 #include "utils/format/macros.hpp" 45 #include "utils/fs/exceptions.hpp" 46 #include "utils/fs/operations.hpp" 47 #include "utils/fs/path.hpp" 48 #include "utils/optional.ipp" 49 #include "utils/text/templates.hpp" 50 51 namespace cmdline = utils::cmdline; 52 namespace config = utils::config; 53 namespace datetime = utils::datetime; 54 namespace fs = utils::fs; 55 namespace scan_action = engine::drivers::scan_action; 56 namespace text = utils::text; 57 58 using utils::optional; 59 60 61 namespace { 62 63 64 /// Creates the report's top directory and fails if it exists. 65 /// 66 /// \param directory The directory to create. 67 /// \param force Whether to wipe an existing directory or not. 68 /// 69 /// \throw std::runtime_error If the directory already exists; this is a user 70 /// error that the user must correct. 71 /// \throw fs::error If the directory creation fails for any other reason. 72 static void 73 create_top_directory(const fs::path& directory, const bool force) 74 { 75 if (force) { 76 if (fs::exists(directory)) 77 fs::rm_r(directory); 78 } 79 80 try { 81 fs::mkdir(directory, 0755); 82 } catch (const fs::system_error& e) { 83 if (e.original_errno() == EEXIST) 84 throw std::runtime_error(F("Output directory '%s' already exists; " 85 "maybe use --force?") % 86 directory); 87 else 88 throw e; 89 } 90 } 91 92 93 /// Generates a flat unique filename for a given test case. 94 /// 95 /// \param test_case The test case for which to genereate the name. 96 /// 97 /// \return A filename unique within a directory with a trailing HTML extension. 98 static std::string 99 test_case_filename(const engine::test_case& test_case) 100 { 101 static const char* special_characters = "/:"; 102 103 std::string name = cli::format_test_case_id(test_case); 104 std::string::size_type pos = name.find_first_of(special_characters); 105 while (pos != std::string::npos) { 106 name.replace(pos, 1, "_"); 107 pos = name.find_first_of(special_characters, pos + 1); 108 } 109 return name + ".html"; 110 } 111 112 113 /// Adds a string to string map to the templates. 114 /// 115 /// \param [in,out] templates The templates to add the map to. 116 /// \param props The map to add to the templates. 117 /// \param key_vector Name of the template vector that holds the keys. 118 /// \param value_vector Name of the template vector that holds the values. 119 static void 120 add_map(text::templates_def& templates, const config::properties_map& props, 121 const std::string& key_vector, const std::string& value_vector) 122 { 123 templates.add_vector(key_vector); 124 templates.add_vector(value_vector); 125 126 for (config::properties_map::const_iterator iter = props.begin(); 127 iter != props.end(); ++iter) { 128 templates.add_to_vector(key_vector, (*iter).first); 129 templates.add_to_vector(value_vector, (*iter).second); 130 } 131 } 132 133 134 /// Generates an HTML report. 135 class html_hooks : public scan_action::base_hooks { 136 /// User interface object where to report progress. 137 cmdline::ui* _ui; 138 139 /// The top directory in which to create the HTML files. 140 fs::path _directory; 141 142 /// Templates accumulator to generate the index.html file. 143 text::templates_def _summary_templates; 144 145 /// Generates a common set of templates for all of our files. 146 /// 147 /// \return A new templates object with common parameters. 148 static text::templates_def 149 common_templates(void) 150 { 151 text::templates_def templates; 152 templates.add_variable("css", "report.css"); 153 return templates; 154 } 155 156 /// Adds a test case result to the summary. 157 /// 158 /// \param test_case The test case to be added. 159 /// \param result The result of the test case. 160 void 161 add_to_summary(const engine::test_case& test_case, 162 const engine::test_result& result) 163 { 164 std::string test_cases_vector; 165 std::string test_cases_file_vector; 166 switch (result.type()) { 167 case engine::test_result::broken: 168 test_cases_vector = "broken_test_cases"; 169 test_cases_file_vector = "broken_test_cases_file"; 170 break; 171 172 case engine::test_result::expected_failure: 173 test_cases_vector = "xfail_test_cases"; 174 test_cases_file_vector = "xfail_test_cases_file"; 175 break; 176 177 case engine::test_result::failed: 178 test_cases_vector = "failed_test_cases"; 179 test_cases_file_vector = "failed_test_cases_file"; 180 break; 181 182 case engine::test_result::passed: 183 test_cases_vector = "passed_test_cases"; 184 test_cases_file_vector = "passed_test_cases_file"; 185 break; 186 187 case engine::test_result::skipped: 188 test_cases_vector = "skipped_test_cases"; 189 test_cases_file_vector = "skipped_test_cases_file"; 190 break; 191 } 192 INV(!test_cases_vector.empty()); 193 INV(!test_cases_file_vector.empty()); 194 195 _summary_templates.add_to_vector(test_cases_vector, 196 cli::format_test_case_id(test_case)); 197 _summary_templates.add_to_vector(test_cases_file_vector, 198 test_case_filename(test_case)); 199 } 200 201 /// Instantiate a template to generate an HTML file in the output directory. 202 /// 203 /// \param templates The templates to use. 204 /// \param template_name The name of the template. This is automatically 205 /// searched for in the installed directory, so do not provide a path. 206 /// \param output_name The name of the output file. This is a basename to 207 /// be created within the output directory. 208 /// 209 /// \throw text::error If there is any problem applying the templates. 210 void 211 generate(const text::templates_def& templates, 212 const std::string& template_name, 213 const std::string& output_name) const 214 { 215 const fs::path miscdir(utils::getenv_with_default( 216 "KYUA_MISCDIR", KYUA_MISCDIR)); 217 const fs::path template_file = miscdir / template_name; 218 const fs::path output_path(_directory / output_name); 219 220 _ui->out(F("Generating %s") % output_path); 221 text::instantiate(templates, template_file, output_path); 222 } 223 224 public: 225 /// Constructor for the hooks. 226 /// 227 /// \param ui_ User interface object where to report progress. 228 /// \param directory_ The directory in which to create the HTML files. 229 html_hooks(cmdline::ui* ui_, const fs::path& directory_) : 230 _ui(ui_), 231 _directory(directory_), 232 _summary_templates(common_templates()) 233 { 234 // Keep in sync with add_to_summary(). 235 _summary_templates.add_vector("broken_test_cases"); 236 _summary_templates.add_vector("broken_test_cases_file"); 237 _summary_templates.add_vector("xfail_test_cases"); 238 _summary_templates.add_vector("xfail_test_cases_file"); 239 _summary_templates.add_vector("failed_test_cases"); 240 _summary_templates.add_vector("failed_test_cases_file"); 241 _summary_templates.add_vector("passed_test_cases"); 242 _summary_templates.add_vector("passed_test_cases_file"); 243 _summary_templates.add_vector("skipped_test_cases"); 244 _summary_templates.add_vector("skipped_test_cases_file"); 245 } 246 247 /// Callback executed when an action is found. 248 /// 249 /// \param action_id The identifier of the loaded action. 250 /// \param action The action loaded from the database. 251 void 252 got_action(const int64_t action_id, 253 const engine::action& action) 254 { 255 _summary_templates.add_variable("action_id", F("%s") % action_id); 256 257 const engine::context& context = action.runtime_context(); 258 text::templates_def templates = common_templates(); 259 templates.add_variable("action_id", F("%s") % action_id); 260 templates.add_variable("cwd", context.cwd().str()); 261 add_map(templates, context.env(), "env_var", "env_var_value"); 262 generate(templates, "context.html", "context.html"); 263 } 264 265 /// Callback executed when a test results is found. 266 /// 267 /// \param iter Container for the test result's data. 268 void 269 got_result(store::results_iterator& iter) 270 { 271 const engine::test_program_ptr test_program = iter.test_program(); 272 const engine::test_result result = iter.result(); 273 274 const engine::test_case& test_case = *test_program->find( 275 iter.test_case_name()); 276 277 add_to_summary(test_case, result); 278 279 text::templates_def templates = common_templates(); 280 templates.add_variable("test_case", 281 cli::format_test_case_id(test_case)); 282 templates.add_variable("test_program", 283 test_program->absolute_path().str()); 284 templates.add_variable("result", cli::format_result(result)); 285 templates.add_variable("duration", cli::format_delta(iter.duration())); 286 287 add_map(templates, test_case.get_metadata().to_properties(), 288 "metadata_var", "metadata_value"); 289 290 { 291 const std::string stdout_text = iter.stdout_contents(); 292 if (!stdout_text.empty()) 293 templates.add_variable("stdout", stdout_text); 294 } 295 { 296 const std::string stderr_text = iter.stderr_contents(); 297 if (!stderr_text.empty()) 298 templates.add_variable("stderr", stderr_text); 299 } 300 301 generate(templates, "test_result.html", test_case_filename(test_case)); 302 } 303 304 /// Writes the index.html file in the output directory. 305 /// 306 /// This should only be called once all the processing has been done; 307 /// i.e. when the scan_action driver returns. 308 void 309 write_summary(void) 310 { 311 const std::size_t bad_count = 312 _summary_templates.get_vector("broken_test_cases").size() + 313 _summary_templates.get_vector("failed_test_cases").size(); 314 _summary_templates.add_variable("bad_tests_count", F("%s") % bad_count); 315 316 generate(text::templates_def(), "report.css", "report.css"); 317 generate(_summary_templates, "index.html", "index.html"); 318 } 319 }; 320 321 322 } // anonymous namespace 323 324 325 /// Default constructor for cmd_report_html. 326 cli::cmd_report_html::cmd_report_html(void) : cli_command( 327 "report-html", "", 0, 0, 328 "Generates an HTML report with the result of a previous action") 329 { 330 add_option(store_option); 331 add_option(cmdline::int_option( 332 "action", "The action to report; if not specified, defaults to the " 333 "latest action in the database", "id")); 334 add_option(cmdline::bool_option( 335 "force", "Wipe the output directory before generating the new report; " 336 "use care")); 337 add_option(cmdline::path_option( 338 "output", "The directory in which to store the HTML files", 339 "path", "html")); 340 } 341 342 343 /// Entry point for the "report-html" subcommand. 344 /// 345 /// \param ui Object to interact with the I/O of the program. 346 /// \param cmdline Representation of the command line to the subcommand. 347 /// \param unused_user_config The runtime configuration of the program. 348 /// 349 /// \return 0 if everything is OK, 1 if the statement is invalid or if there is 350 /// any other problem. 351 int 352 cli::cmd_report_html::run(cmdline::ui* ui, 353 const cmdline::parsed_cmdline& cmdline, 354 const config::tree& UTILS_UNUSED_PARAM(user_config)) 355 { 356 optional< int64_t > action_id; 357 if (cmdline.has_option("action")) 358 action_id = cmdline.get_option< cmdline::int_option >("action"); 359 360 const fs::path directory = 361 cmdline.get_option< cmdline::path_option >("output"); 362 create_top_directory(directory, cmdline.has_option("force")); 363 html_hooks hooks(ui, directory); 364 scan_action::drive(store_path(cmdline), action_id, hooks); 365 hooks.write_summary(); 366 367 return EXIT_SUCCESS; 368 } 369