xref: /netbsd-src/external/bsd/atf/dist/tools/atf-report.cpp (revision 3816d47b2c42fcd6e549e3407f842a5b1a1d23ad)
1 //
2 // Automated Testing Framework (atf)
3 //
4 // Copyright (c) 2007, 2008 The NetBSD Foundation, Inc.
5 // All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or without
8 // modification, are permitted provided that the following conditions
9 // are met:
10 // 1. Redistributions of source code must retain the above copyright
11 //    notice, this list of conditions and the following disclaimer.
12 // 2. Redistributions in binary form must reproduce the above copyright
13 //    notice, this list of conditions and the following disclaimer in the
14 //    documentation and/or other materials provided with the distribution.
15 //
16 // THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND
17 // CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
18 // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
19 // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 // IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY
21 // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
23 // GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
25 // IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
26 // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
27 // IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 //
29 
30 #include <cstdlib>
31 #include <fstream>
32 #include <iostream>
33 #include <memory>
34 #include <utility>
35 #include <vector>
36 
37 #include "atf-c++/application.hpp"
38 #include "atf-c++/fs.hpp"
39 #include "atf-c++/formats.hpp"
40 #include "atf-c++/sanity.hpp"
41 #include "atf-c++/text.hpp"
42 #include "atf-c++/ui.hpp"
43 
44 typedef std::auto_ptr< std::ostream > ostream_ptr;
45 
46 ostream_ptr
47 open_outfile(const atf::fs::path& path)
48 {
49     ostream_ptr osp;
50     if (path.str() == "-")
51         osp = ostream_ptr(new std::ofstream("/dev/stdout"));
52     else
53         osp = ostream_ptr(new std::ofstream(path.c_str()));
54     if (!(*osp))
55         throw std::runtime_error("Could not create file " + path.str());
56     return osp;
57 }
58 
59 // ------------------------------------------------------------------------
60 // The "writer" interface.
61 // ------------------------------------------------------------------------
62 
63 //!
64 //! \brief A base class that defines an output format.
65 //!
66 //! The writer base class defines a generic interface to output formats.
67 //! This is meant to be subclassed, and each subclass can redefine any
68 //! method to format the information as it wishes.
69 //!
70 //! This class is not tied to a output stream nor a file because, depending
71 //! on the output format, we will want to write to a single file or to
72 //! multiple ones.
73 //!
74 class writer {
75 public:
76     writer(void) {}
77     virtual ~writer(void) {}
78 
79     virtual void write_info(const std::string&, const std::string&) {}
80     virtual void write_ntps(size_t) {}
81     virtual void write_tp_start(const std::string&, size_t) {}
82     virtual void write_tp_end(const std::string&) {}
83     virtual void write_tc_start(const std::string&) {}
84     virtual void write_tc_stdout_line(const std::string&) {}
85     virtual void write_tc_stderr_line(const std::string&) {}
86     virtual void write_tc_end(const atf::tests::tcr&) {}
87     virtual void write_eof(void) {}
88 };
89 
90 // ------------------------------------------------------------------------
91 // The "csv_writer" class.
92 // ------------------------------------------------------------------------
93 
94 //!
95 //! \brief A very simple plain-text output format.
96 //!
97 //! The csv_writer class implements a very simple plain-text output
98 //! format that summarizes the results of each executed test case.  The
99 //! results are meant to be easily parseable by third-party tools, hence
100 //! they are formatted as a CSV file.
101 //!
102 class csv_writer : public writer {
103     ostream_ptr m_os;
104     bool m_failed;
105 
106     std::string m_tpname;
107     std::string m_tcname;
108 
109 public:
110     csv_writer(const atf::fs::path& p) :
111         m_os(open_outfile(p))
112     {
113     }
114 
115     virtual
116     void
117     write_tp_start(const std::string& name, size_t ntcs)
118     {
119         m_tpname = name;
120         m_failed = false;
121     }
122 
123     virtual
124     void
125     write_tp_end(const std::string& reason)
126     {
127         if (!reason.empty())
128             (*m_os) << "tp, " << m_tpname << ", bogus, " << reason
129                     << std::endl;
130         else if (m_failed)
131             (*m_os) << "tp, " << m_tpname << ", failed" << std::endl;
132         else
133             (*m_os) << "tp, " << m_tpname << ", passed" << std::endl;
134     }
135 
136     virtual
137     void
138     write_tc_start(const std::string& name)
139     {
140         m_tcname = name;
141     }
142 
143     virtual
144     void
145     write_tc_end(const atf::tests::tcr& tcr)
146     {
147         std::string str = "tc, ";
148         if (tcr.get_state() == atf::tests::tcr::passed_state) {
149             str += m_tpname + ", " + m_tcname + ", passed";
150         } else if (tcr.get_state() == atf::tests::tcr::failed_state) {
151             str += m_tpname + ", " + m_tcname + ", failed, " +
152                    tcr.get_reason();
153             m_failed = true;
154         } else if (tcr.get_state() == atf::tests::tcr::skipped_state) {
155             str += m_tpname + ", " + m_tcname + ", skipped, " +
156                    tcr.get_reason();
157         } else
158             UNREACHABLE;
159         (*m_os) << str << std::endl;
160     }
161 };
162 
163 // ------------------------------------------------------------------------
164 // The "ticker_writer" class.
165 // ------------------------------------------------------------------------
166 
167 //!
168 //! \brief A console-friendly output format.
169 //!
170 //! The ticker_writer class implements a formatter that is user-friendly
171 //! in the sense that it shows the execution of test cases in an easy to
172 //! read format.  It is not meant to be parseable and its format can
173 //! freely change across releases.
174 //!
175 class ticker_writer : public writer {
176     ostream_ptr m_os;
177 
178     size_t m_curtp, m_ntps;
179     size_t m_tcs_passed, m_tcs_failed, m_tcs_skipped;
180     std::string m_tcname, m_tpname;
181     std::vector< std::string > m_failed_tcs;
182     std::vector< std::string > m_failed_tps;
183 
184     void
185     write_info(const std::string& what, const std::string& val)
186     {
187         if (what == "tests.root") {
188             (*m_os) << "Tests root: " << val << std::endl
189                     << std::endl;
190         }
191     }
192 
193     void
194     write_ntps(size_t ntps)
195     {
196         m_curtp = 1;
197         m_tcs_passed = 0;
198         m_tcs_failed = 0;
199         m_tcs_skipped = 0;
200         m_ntps = ntps;
201     }
202 
203     void
204     write_tp_start(const std::string& tp, size_t ntcs)
205     {
206         using atf::text::to_string;
207         using atf::ui::format_text;
208 
209         m_tpname = tp;
210 
211         (*m_os) << format_text(tp + " (" + to_string(m_curtp) +
212                                "/" + to_string(m_ntps) + "): " +
213                                to_string(ntcs) + " test cases")
214                 << std::endl;
215         (*m_os).flush();
216     }
217 
218     void
219     write_tp_end(const std::string& reason)
220     {
221         using atf::ui::format_text_with_tag;
222 
223         m_curtp++;
224 
225         if (!reason.empty()) {
226             (*m_os) << format_text_with_tag("BOGUS TEST PROGRAM: Cannot "
227                                             "trust its results because "
228                                             "of `" + reason + "'",
229                                             m_tpname + ": ", false)
230                     << std::endl;
231             m_failed_tps.push_back(m_tpname);
232         }
233         (*m_os) << std::endl;
234         (*m_os).flush();
235 
236         m_tpname.clear();
237     }
238 
239     void
240     write_tc_start(const std::string& tcname)
241     {
242         m_tcname = tcname;
243 
244         (*m_os) << "    " + tcname + ": ";
245         (*m_os).flush();
246     }
247 
248     void
249     write_tc_end(const atf::tests::tcr& tcr)
250     {
251         std::string str;
252 
253         atf::tests::tcr::state s = tcr.get_state();
254         if (s == atf::tests::tcr::passed_state) {
255             str = "Passed.";
256             m_tcs_passed++;
257         } else if (s == atf::tests::tcr::failed_state) {
258             str = "Failed: " + tcr.get_reason();
259             m_tcs_failed++;
260             m_failed_tcs.push_back(m_tpname + ":" + m_tcname);
261         } else if (s == atf::tests::tcr::skipped_state) {
262             str = "Skipped: " + tcr.get_reason();
263             m_tcs_skipped++;
264         } else
265             UNREACHABLE;
266 
267         // XXX Wrap text.  format_text_with_tag does not currently allow
268         // to specify the current column, which is needed because we have
269         // already printed the tc's name.
270         (*m_os) << str << std::endl;
271 
272         m_tcname = "";
273     }
274 
275     void
276     write_eof(void)
277     {
278         using atf::text::join;
279         using atf::text::to_string;
280         using atf::ui::format_text;
281         using atf::ui::format_text_with_tag;
282 
283         if (!m_failed_tps.empty()) {
284             (*m_os) << format_text("Failed (bogus) test programs:")
285                     << std::endl;
286             (*m_os) << format_text_with_tag(join(m_failed_tps, ", "),
287                                             "    ", false) << std::endl
288                     << std::endl;
289         }
290 
291         if (!m_failed_tcs.empty()) {
292             (*m_os) << format_text("Failed test cases:") << std::endl;
293             (*m_os) << format_text_with_tag(join(m_failed_tcs, ", "),
294                                             "    ", false) << std::endl
295                     << std::endl;
296         }
297 
298         (*m_os) << format_text("Summary for " + to_string(m_ntps) +
299                                " test programs:")
300                 << std::endl;
301         (*m_os) << format_text_with_tag(to_string(m_tcs_passed) +
302                                         " passed test cases.",
303                                         "    ", false)
304                 << std::endl;
305         (*m_os) << format_text_with_tag(to_string(m_tcs_failed) +
306                                         " failed test cases.",
307                                         "    ", false)
308                 << std::endl;
309         (*m_os) << format_text_with_tag(to_string(m_tcs_skipped) +
310                                         " skipped test cases.",
311                                         "    ", false)
312                 << std::endl;
313     }
314 
315 public:
316     ticker_writer(const atf::fs::path& p) :
317         m_os(open_outfile(p))
318     {
319     }
320 };
321 
322 // ------------------------------------------------------------------------
323 // The "xml" class.
324 // ------------------------------------------------------------------------
325 
326 //!
327 //! \brief A single-file XML output format.
328 //!
329 //! The xml_writer class implements a formatter that prints the results
330 //! of test cases in an XML format easily parseable later on by other
331 //! utilities.
332 //!
333 class xml_writer : public writer {
334     ostream_ptr m_os;
335 
336     size_t m_curtp, m_ntps;
337     size_t m_tcs_passed, m_tcs_failed, m_tcs_skipped;
338     std::string m_tcname, m_tpname;
339     std::vector< std::string > m_failed_tcs;
340     std::vector< std::string > m_failed_tps;
341 
342     static
343     std::string
344     attrval(const std::string& str)
345     {
346         return str;
347     }
348 
349     static
350     std::string
351     elemval(const std::string& str)
352     {
353         std::string ostr;
354         for (std::string::const_iterator iter = str.begin();
355              iter != str.end(); iter++) {
356             switch (*iter) {
357             case '&': ostr += "&amp;"; break;
358             case '<': ostr += "&lt;"; break;
359             case '>': ostr += "&gt;"; break;
360             default:  ostr += *iter;
361             }
362         }
363         return ostr;
364     }
365 
366     void
367     write_info(const std::string& what, const std::string& val)
368     {
369         (*m_os) << "<info class=\"" << what << "\">" << val << "</info>"
370                 << std::endl;
371     }
372 
373     void
374     write_tp_start(const std::string& tp, size_t ntcs)
375     {
376         (*m_os) << "<tp id=\"" << attrval(tp) << "\">" << std::endl;
377     }
378 
379     void
380     write_tp_end(const std::string& reason)
381     {
382         if (!reason.empty())
383             (*m_os) << "<failed>" << elemval(reason) << "</failed>"
384                     << std::endl;
385         (*m_os) << "</tp>" << std::endl;
386     }
387 
388     void
389     write_tc_start(const std::string& tcname)
390     {
391         (*m_os) << "<tc id=\"" << attrval(tcname) << "\">" << std::endl;
392     }
393 
394     void
395     write_tc_stdout_line(const std::string& line)
396     {
397         (*m_os) << "<so>" << elemval(line) << "</so>" << std::endl;
398     }
399 
400     void
401     write_tc_stderr_line(const std::string& line)
402     {
403         (*m_os) << "<se>" << elemval(line) << "</se>" << std::endl;
404     }
405 
406     void
407     write_tc_end(const atf::tests::tcr& tcr)
408     {
409         std::string str;
410 
411         atf::tests::tcr::state s = tcr.get_state();
412         if (s == atf::tests::tcr::passed_state) {
413             (*m_os) << "<passed />" << std::endl;
414         } else if (s == atf::tests::tcr::failed_state) {
415             (*m_os) << "<failed>" << elemval(tcr.get_reason())
416                     << "</failed>" << std::endl;
417         } else if (s == atf::tests::tcr::skipped_state) {
418             (*m_os) << "<skipped>" << elemval(tcr.get_reason())
419                     << "</skipped>" << std::endl;
420         } else
421             UNREACHABLE;
422         (*m_os) << "</tc>" << std::endl;
423     }
424 
425     void
426     write_eof(void)
427     {
428         (*m_os) << "</tests-results>" << std::endl;
429     }
430 
431 public:
432     xml_writer(const atf::fs::path& p) :
433         m_os(open_outfile(p))
434     {
435         (*m_os) << "<?xml version=\"1.0\"?>" << std::endl
436                 << "<!DOCTYPE tests-results PUBLIC "
437                    "\"-//NetBSD//DTD ATF Tests Results 0.1//EN\" "
438                    "\"http://www.NetBSD.org/XML/atf/tests-results.dtd\">"
439                 << std::endl
440                 << std::endl
441                 << "<tests-results>" << std::endl;
442     }
443 };
444 
445 // ------------------------------------------------------------------------
446 // The "converter" class.
447 // ------------------------------------------------------------------------
448 
449 //!
450 //! \brief A reader that redirects events to multiple writers.
451 //!
452 //! The converter class implements an atf_tps_reader that, for each event
453 //! raised by the parser, redirects it to multiple writers so that they
454 //! can reformat it according to their output rules.
455 //!
456 class converter : public atf::formats::atf_tps_reader {
457     typedef std::vector< writer* > outs_vector;
458     outs_vector m_outs;
459 
460     void
461     got_info(const std::string& what, const std::string& val)
462     {
463         for (outs_vector::iterator iter = m_outs.begin();
464              iter != m_outs.end(); iter++)
465             (*iter)->write_info(what, val);
466     }
467 
468     void
469     got_ntps(size_t ntps)
470     {
471         for (outs_vector::iterator iter = m_outs.begin();
472              iter != m_outs.end(); iter++)
473             (*iter)->write_ntps(ntps);
474     }
475 
476     void
477     got_tp_start(const std::string& tp, size_t ntcs)
478     {
479         for (outs_vector::iterator iter = m_outs.begin();
480              iter != m_outs.end(); iter++)
481             (*iter)->write_tp_start(tp, ntcs);
482     }
483 
484     void
485     got_tp_end(const std::string& reason)
486     {
487         for (outs_vector::iterator iter = m_outs.begin();
488              iter != m_outs.end(); iter++)
489             (*iter)->write_tp_end(reason);
490     }
491 
492     void
493     got_tc_start(const std::string& tcname)
494     {
495         for (outs_vector::iterator iter = m_outs.begin();
496              iter != m_outs.end(); iter++)
497             (*iter)->write_tc_start(tcname);
498     }
499 
500     void
501     got_tc_stdout_line(const std::string& line)
502     {
503         for (outs_vector::iterator iter = m_outs.begin();
504              iter != m_outs.end(); iter++)
505             (*iter)->write_tc_stdout_line(line);
506     }
507 
508     void
509     got_tc_stderr_line(const std::string& line)
510     {
511         for (outs_vector::iterator iter = m_outs.begin();
512              iter != m_outs.end(); iter++)
513             (*iter)->write_tc_stderr_line(line);
514     }
515 
516     void
517     got_tc_end(const atf::tests::tcr& tcr)
518     {
519         for (outs_vector::iterator iter = m_outs.begin();
520              iter != m_outs.end(); iter++)
521             (*iter)->write_tc_end(tcr);
522     }
523 
524     void
525     got_eof(void)
526     {
527         for (outs_vector::iterator iter = m_outs.begin();
528              iter != m_outs.end(); iter++)
529             (*iter)->write_eof();
530     }
531 
532 public:
533     converter(std::istream& is) :
534         atf::formats::atf_tps_reader(is)
535     {
536     }
537 
538     ~converter(void)
539     {
540         for (outs_vector::iterator iter = m_outs.begin();
541              iter != m_outs.end(); iter++)
542             delete *iter;
543     }
544 
545     void
546     add_output(const std::string& fmt, const atf::fs::path& p)
547     {
548         if (fmt == "csv") {
549             m_outs.push_back(new csv_writer(p));
550         } else if (fmt == "ticker") {
551             m_outs.push_back(new ticker_writer(p));
552         } else if (fmt == "xml") {
553             m_outs.push_back(new xml_writer(p));
554         } else
555             throw std::runtime_error("Unknown format `" + fmt + "'");
556     }
557 };
558 
559 // ------------------------------------------------------------------------
560 // The "atf_report" class.
561 // ------------------------------------------------------------------------
562 
563 class atf_report : public atf::application::app {
564     static const char* m_description;
565 
566     typedef std::pair< std::string, atf::fs::path > fmt_path_pair;
567     std::vector< fmt_path_pair > m_oflags;
568 
569     void process_option(int, const char*);
570     options_set specific_options(void) const;
571 
572 public:
573     atf_report(void);
574 
575     int main(void);
576 };
577 
578 const char* atf_report::m_description =
579     "atf-report is a tool that parses the output of atf-run and "
580     "generates user-friendly reports in multiple different formats.";
581 
582 atf_report::atf_report(void) :
583     app(m_description, "atf-report(1)", "atf(7)")
584 {
585 }
586 
587 void
588 atf_report::process_option(int ch, const char* arg)
589 {
590     switch (ch) {
591     case 'o':
592         {
593             std::string str(arg);
594             std::string::size_type pos = str.find(':');
595             if (pos == std::string::npos)
596                 throw std::runtime_error("Syntax error in -o option");
597             else {
598                 std::string fmt = str.substr(0, pos);
599                 atf::fs::path path = atf::fs::path(str.substr(pos + 1));
600                 m_oflags.push_back(fmt_path_pair(fmt, path));
601             }
602         }
603         break;
604 
605     default:
606         UNREACHABLE;
607     }
608 }
609 
610 atf_report::options_set
611 atf_report::specific_options(void)
612     const
613 {
614     using atf::application::option;
615     options_set opts;
616     opts.insert(option('o', "fmt:path", "Adds a new output file; multiple "
617                                         "ones can be specified, and a - "
618                                         "path means stdout"));
619     return opts;
620 }
621 
622 int
623 atf_report::main(void)
624 {
625     if (m_oflags.empty())
626         m_oflags.push_back(fmt_path_pair("ticker", atf::fs::path("-")));
627 
628     // Look for path duplicates.
629     std::set< atf::fs::path > paths;
630     for (std::vector< fmt_path_pair >::const_iterator iter = m_oflags.begin();
631          iter != m_oflags.end(); iter++) {
632         atf::fs::path p = (*iter).second;
633         if (p == atf::fs::path("/dev/stdout"))
634             p = atf::fs::path("-");
635         if (paths.find(p) != paths.end())
636             throw std::runtime_error("The file `" + p.str() + "' was "
637                                      "specified more than once");
638         paths.insert((*iter).second);
639     }
640 
641     // Generate the output files.
642     converter cnv(std::cin);
643     for (std::vector< fmt_path_pair >::const_iterator iter = m_oflags.begin();
644          iter != m_oflags.end(); iter++)
645         cnv.add_output((*iter).first, (*iter).second);
646     cnv.read();
647 
648     return EXIT_SUCCESS;
649 }
650 
651 int
652 main(int argc, char* const* argv)
653 {
654     return atf_report().run(argc, argv);
655 }
656