xref: /minix3/external/bsd/kyua-cli/dist/cli/cmd_report_html.cpp (revision 6e5a113837816c382dd4f2a25924f16fbbb1c493)
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