xref: /freebsd/contrib/kyua/engine/tap_parser.cpp (revision b0d29bc47dba79f6f38e67eabadfb4b32ffd9390)
1 // Copyright 2015 The Kyua Authors.
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 "engine/tap_parser.hpp"
30 
31 #include <fstream>
32 
33 #include "engine/exceptions.hpp"
34 #include "utils/format/macros.hpp"
35 #include "utils/noncopyable.hpp"
36 #include "utils/optional.ipp"
37 #include "utils/sanity.hpp"
38 #include "utils/text/exceptions.hpp"
39 #include "utils/text/operations.ipp"
40 #include "utils/text/regex.hpp"
41 
42 namespace fs = utils::fs;
43 namespace text = utils::text;
44 
45 using utils::optional;
46 
47 
48 /// TAP plan representing all tests being skipped.
49 const engine::tap_plan engine::all_skipped_plan(1, 0);
50 
51 
52 namespace {
53 
54 
55 /// Implementation of the TAP parser.
56 ///
57 /// This is a class only to simplify keeping global constant values around (like
58 /// prebuilt regular expressions).
59 class tap_parser : utils::noncopyable {
60     /// Regular expression to match plan lines.
61     text::regex _plan_regex;
62 
63     /// Regular expression to match a TODO and extract the reason.
64     text::regex _todo_regex;
65 
66     /// Regular expression to match a SKIP and extract the reason.
67     text::regex _skip_regex;
68 
69     /// Regular expression to match a single test result.
70     text::regex _result_regex;
71 
72     /// Checks if a line contains a TAP plan and extracts its data.
73     ///
74     /// \param line The line to try to parse.
75     /// \param [in,out] out_plan Used to store the found plan, if any.  The same
76     ///     output variable should be given to all calls to this function so
77     ///     that duplicate plan entries can be discovered.
78     /// \param [out] out_all_skipped_reason Used to store the reason for all
79     ///     tests being skipped, if any.  If this is set to a non-empty value,
80     ///     then the out_plan is set to 1..0.
81     ///
82     /// \return True if the line matched a plan; false otherwise.
83     ///
84     /// \throw engine::format_error If the input is invalid.
85     /// \throw text::error If the input is invalid.
86     bool
try_parse_plan(const std::string & line,optional<engine::tap_plan> & out_plan,std::string & out_all_skipped_reason)87     try_parse_plan(const std::string& line,
88                    optional< engine::tap_plan >& out_plan,
89                    std::string& out_all_skipped_reason)
90     {
91         const text::regex_matches plan_matches = _plan_regex.match(line);
92         if (!plan_matches)
93             return false;
94         const engine::tap_plan plan(
95             text::to_type< std::size_t >(plan_matches.get(1)),
96             text::to_type< std::size_t >(plan_matches.get(2)));
97 
98         if (out_plan)
99             throw engine::format_error(
100                 F("Found duplicate plan %s..%s (saw %s..%s earlier)") %
101                 plan.first % plan.second %
102                 out_plan.get().first % out_plan.get().second);
103 
104         std::string all_skipped_reason;
105         const text::regex_matches skip_matches = _skip_regex.match(line);
106         if (skip_matches) {
107             if (plan != engine::all_skipped_plan) {
108                 throw engine::format_error(F("Skipped plan must be %s..%s") %
109                                            engine::all_skipped_plan.first %
110                                            engine::all_skipped_plan.second);
111             }
112             all_skipped_reason = skip_matches.get(2);
113             if (all_skipped_reason.empty())
114                 all_skipped_reason = "No reason specified";
115         } else {
116             if (plan.first > plan.second)
117                 throw engine::format_error(F("Found reversed plan %s..%s") %
118                                            plan.first % plan.second);
119         }
120 
121         INV(!out_plan);
122         out_plan = plan;
123         out_all_skipped_reason = all_skipped_reason;
124 
125         POST(out_plan);
126         POST(out_all_skipped_reason.empty() ||
127              out_plan.get() == engine::all_skipped_plan);
128 
129         return true;
130     }
131 
132     /// Checks if a line contains a TAP test result and extracts its data.
133     ///
134     /// \param line The line to try to parse.
135     /// \param [in,out] out_ok_count Accumulator for 'ok' results.
136     /// \param [in,out] out_not_ok_count Accumulator for 'not ok' results.
137     /// \param [out] out_bailed_out Set to true if the test bailed out.
138     ///
139     /// \return True if the line matched a result; false otherwise.
140     ///
141     /// \throw engine::format_error If the input is invalid.
142     /// \throw text::error If the input is invalid.
143     bool
try_parse_result(const std::string & line,std::size_t & out_ok_count,std::size_t & out_not_ok_count,bool & out_bailed_out)144     try_parse_result(const std::string& line, std::size_t& out_ok_count,
145                      std::size_t& out_not_ok_count, bool& out_bailed_out)
146     {
147         PRE(!out_bailed_out);
148 
149         const text::regex_matches result_matches = _result_regex.match(line);
150         if (result_matches) {
151             if (result_matches.get(1) == "ok") {
152                 ++out_ok_count;
153             } else {
154                 INV(result_matches.get(1) == "not ok");
155                 if (_todo_regex.match(line) || _skip_regex.match(line)) {
156                     ++out_ok_count;
157                 } else {
158                     ++out_not_ok_count;
159                 }
160             }
161             return true;
162         } else {
163             if (line.find("Bail out!") == 0) {
164                 out_bailed_out = true;
165                 return true;
166             } else {
167                 return false;
168             }
169         }
170     }
171 
172 public:
173     /// Sets up the TAP parser state.
tap_parser(void)174     tap_parser(void) :
175         _plan_regex(text::regex::compile("^([0-9]+)\\.\\.([0-9]+)", 2)),
176         _todo_regex(text::regex::compile("TODO[ \t]*(.*)$", 2, true)),
177         _skip_regex(text::regex::compile("(SKIP|Skipped:?)[ \t]*(.*)$", 2,
178                                          true)),
179         _result_regex(text::regex::compile("^(not ok|ok)[ \t-]+[0-9]*", 1))
180     {
181     }
182 
183     /// Parses an input file containing TAP output.
184     ///
185     /// \param input The stream to read from.
186     ///
187     /// \return The results of the parsing in the form of a tap_summary object.
188     ///
189     /// \throw engine::format_error If there are any syntax errors in the input.
190     /// \throw text::error If there are any syntax errors in the input.
191     engine::tap_summary
parse(std::ifstream & input)192     parse(std::ifstream& input)
193     {
194         optional< engine::tap_plan > plan;
195         std::string all_skipped_reason;
196         bool bailed_out = false;
197         std::size_t ok_count = 0, not_ok_count = 0;
198 
199         std::string line;
200         while (!bailed_out && std::getline(input, line)) {
201             if (try_parse_result(line, ok_count, not_ok_count, bailed_out))
202                 continue;
203             (void)try_parse_plan(line, plan, all_skipped_reason);
204         }
205 
206         if (bailed_out) {
207             return engine::tap_summary::new_bailed_out();
208         } else {
209             if (!plan)
210                 throw engine::format_error(
211                     "Output did not contain any TAP plan and the program did "
212                     "not bail out");
213 
214             if (plan.get() == engine::all_skipped_plan) {
215                 return engine::tap_summary::new_all_skipped(all_skipped_reason);
216             } else {
217                 const std::size_t exp_count = plan.get().second -
218                     plan.get().first + 1;
219                 const std::size_t actual_count = ok_count + not_ok_count;
220                 if (exp_count != actual_count) {
221                     throw engine::format_error(
222                         "Reported plan differs from actual executed tests");
223                 }
224                 return engine::tap_summary::new_results(plan.get(), ok_count,
225                                                         not_ok_count);
226             }
227         }
228     }
229 };
230 
231 
232 }  // anonymous namespace
233 
234 
235 /// Constructs a TAP summary with the results of parsing a TAP output.
236 ///
237 /// \param bailed_out_ Whether the test program bailed out early or not.
238 /// \param plan_ The TAP plan.
239 /// \param all_skipped_reason_ The reason for skipping all tests, if any.
240 /// \param ok_count_ Number of 'ok' test results.
241 /// \param not_ok_count_ Number of 'not ok' test results.
tap_summary(const bool bailed_out_,const tap_plan & plan_,const std::string & all_skipped_reason_,const std::size_t ok_count_,const std::size_t not_ok_count_)242 engine::tap_summary::tap_summary(const bool bailed_out_,
243                                  const tap_plan& plan_,
244                                  const std::string& all_skipped_reason_,
245                                  const std::size_t ok_count_,
246                                  const std::size_t not_ok_count_) :
247     _bailed_out(bailed_out_), _plan(plan_),
248     _all_skipped_reason(all_skipped_reason_),
249     _ok_count(ok_count_), _not_ok_count(not_ok_count_)
250 {
251 }
252 
253 
254 /// Constructs a TAP summary for a bailed out test program.
255 ///
256 /// \return The new tap_summary object.
257 engine::tap_summary
new_bailed_out(void)258 engine::tap_summary::new_bailed_out(void)
259 {
260     return tap_summary(true, tap_plan(0, 0), "", 0, 0);
261 }
262 
263 
264 /// Constructs a TAP summary for a test program that skipped all tests.
265 ///
266 /// \param reason Textual reason describing why the tests were skipped.
267 ///
268 /// \return The new tap_summary object.
269 engine::tap_summary
new_all_skipped(const std::string & reason)270 engine::tap_summary::new_all_skipped(const std::string& reason)
271 {
272     return tap_summary(false, all_skipped_plan, reason, 0, 0);
273 }
274 
275 
276 /// Constructs a TAP summary for a test program that reported results.
277 ///
278 /// \param plan_ The TAP plan.
279 /// \param ok_count_ Total number of 'ok' results.
280 /// \param not_ok_count_ Total number of 'not ok' results.
281 ///
282 /// \return The new tap_summary object.
283 engine::tap_summary
new_results(const tap_plan & plan_,const std::size_t ok_count_,const std::size_t not_ok_count_)284 engine::tap_summary::new_results(const tap_plan& plan_,
285                                  const std::size_t ok_count_,
286                                  const std::size_t not_ok_count_)
287 {
288     PRE((plan_.second - plan_.first + 1) == (ok_count_ + not_ok_count_));
289     return tap_summary(false, plan_, "", ok_count_, not_ok_count_);
290 }
291 
292 
293 /// Checks whether the test program bailed out early or not.
294 ///
295 /// \return True if the test program aborted execution before completing.
296 bool
bailed_out(void) const297 engine::tap_summary::bailed_out(void) const
298 {
299     return _bailed_out;
300 }
301 
302 
303 /// Gets the TAP plan of the test program.
304 ///
305 /// \pre bailed_out() must be false.
306 ///
307 /// \return The TAP plan.  If 1..0, then all_skipped_reason() will have some
308 /// contents.
309 const engine::tap_plan&
plan(void) const310 engine::tap_summary::plan(void) const
311 {
312     PRE(!_bailed_out);
313     return _plan;
314 }
315 
316 
317 /// Gets the reason for skipping all the tests, if any.
318 ///
319 /// \pre bailed_out() must be false.
320 /// \pre plan() returns 1..0.
321 ///
322 /// \return The reason for skipping all the tests.
323 const std::string&
all_skipped_reason(void) const324 engine::tap_summary::all_skipped_reason(void) const
325 {
326     PRE(!_bailed_out);
327     PRE(_plan == all_skipped_plan);
328     return _all_skipped_reason;
329 }
330 
331 
332 /// Gets the number of 'ok' test results.
333 ///
334 /// \pre bailed_out() must be false.
335 ///
336 /// \return The number of test results that reported 'ok'.
337 std::size_t
ok_count(void) const338 engine::tap_summary::ok_count(void) const
339 {
340     PRE(!bailed_out());
341     PRE(_all_skipped_reason.empty());
342     return _ok_count;
343 }
344 
345 
346 /// Gets the number of 'not ok' test results.
347 ///
348 /// \pre bailed_out() must be false.
349 ///
350 /// \return The number of test results that reported 'not ok'.
351 std::size_t
not_ok_count(void) const352 engine::tap_summary::not_ok_count(void) const
353 {
354     PRE(!_bailed_out);
355     PRE(_all_skipped_reason.empty());
356     return _not_ok_count;
357 }
358 
359 
360 /// Checks two tap_summary objects for equality.
361 ///
362 /// \param other The object to compare this one to.
363 ///
364 /// \return True if the two objects are equal; false otherwise.
365 bool
operator ==(const tap_summary & other) const366 engine::tap_summary::operator==(const tap_summary& other) const
367 {
368     return (_bailed_out == other._bailed_out &&
369             _plan == other._plan &&
370             _all_skipped_reason == other._all_skipped_reason &&
371             _ok_count == other._ok_count &&
372             _not_ok_count == other._not_ok_count);
373 }
374 
375 
376 /// Checks two tap_summary objects for inequality.
377 ///
378 /// \param other The object to compare this one to.
379 ///
380 /// \return True if the two objects are different; false otherwise.
381 bool
operator !=(const tap_summary & other) const382 engine::tap_summary::operator!=(const tap_summary& other) const
383 {
384     return !(*this == other);
385 }
386 
387 
388 /// Formats a tap_summary into a stream.
389 ///
390 /// \param output The stream into which to inject the object.
391 /// \param summary The summary to format.
392 ///
393 /// \return The output stream.
394 std::ostream&
operator <<(std::ostream & output,const tap_summary & summary)395 engine::operator<<(std::ostream& output, const tap_summary& summary)
396 {
397     output << "tap_summary{";
398     if (summary.bailed_out()) {
399         output << "bailed_out=true";
400     } else {
401         const tap_plan& plan = summary.plan();
402         output << "bailed_out=false"
403                << ", plan=" << plan.first << ".." << plan.second;
404         if (plan == all_skipped_plan) {
405             output << ", all_skipped_reason=" << summary.all_skipped_reason();
406         } else {
407             output << ", ok_count=" << summary.ok_count()
408                    << ", not_ok_count=" << summary.not_ok_count();
409         }
410     }
411     output << "}";
412     return output;
413 }
414 
415 
416 /// Parses an input file containing the TAP output of a test program.
417 ///
418 /// \param filename Path to the file to parse.
419 ///
420 /// \return The parsed data in the form of a tap_summary.
421 ///
422 /// \throw load_error If there are any problems parsing the file.  Such problems
423 ///     should be considered as test program breakage.
424 engine::tap_summary
parse_tap_output(const utils::fs::path & filename)425 engine::parse_tap_output(const utils::fs::path& filename)
426 {
427     std::ifstream input(filename.str().c_str());
428     if (!input)
429         throw engine::load_error(filename, "Failed to open TAP output file");
430 
431     try {
432         return tap_summary(tap_parser().parse(input));
433     } catch (const engine::format_error& e) {
434         throw engine::load_error(filename, e.what());
435     } catch (const text::error& e) {
436         throw engine::load_error(filename, e.what());
437     }
438 }
439