1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 package org.tap4j.parser;
25
26 import java.io.File;
27 import java.util.Map;
28 import java.util.Scanner;
29 import java.util.Stack;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32
33 import org.apache.commons.lang.StringUtils;
34 import org.tap4j.model.BailOut;
35 import org.tap4j.model.Comment;
36 import org.tap4j.model.Directive;
37 import org.tap4j.model.Footer;
38 import org.tap4j.model.Header;
39 import org.tap4j.model.Plan;
40 import org.tap4j.model.SkipPlan;
41 import org.tap4j.model.TapElement;
42 import org.tap4j.model.TestResult;
43 import org.tap4j.model.TestSet;
44 import org.tap4j.model.Text;
45 import org.tap4j.util.DirectiveValues;
46 import org.tap4j.util.StatusValues;
47 import org.yaml.snakeyaml.Yaml;
48
49
50
51
52
53
54 public class Tap13YamlParser implements Parser {
55
56 protected static final Pattern INDENTANTION_PATTERN = Pattern
57 .compile("((\\s|\\t)*)?.*");
58
59 private TestSet testSet;
60
61 private Stack<Memento> mementos = new Stack<Memento>();
62
63 private boolean firstLine;
64
65 private boolean planBeforeTestResult;
66
67 private boolean currentlyInYAML;
68
69
70 private String lastLine = null;
71
72 private TapElement lastParsedElement;
73
74
75
76
77
78 private int baseIndentationLevel;
79
80
81
82
83
84
85 private int currentIndentationLevel;
86
87
88
89
90 private Yaml yaml;
91
92 private StringBuilder diagnosticBuffer;
93
94 public Tap13YamlParser() {
95 super();
96 this.init();
97 }
98
99
100
101
102
103 public final void init() {
104 this.baseIndentationLevel = -1;
105 this.currentIndentationLevel = -1;
106 this.currentlyInYAML = Boolean.FALSE;
107 this.diagnosticBuffer = new StringBuilder();
108 this.lastParsedElement = null;
109 this.firstLine = Boolean.TRUE;
110 this.planBeforeTestResult = Boolean.FALSE;
111 this.testSet = new TestSet();
112 yaml = new Yaml();
113 }
114
115
116
117
118 private void saveMemento() {
119 Memento memento = new Memento();
120 memento.setBaseIndentationLevel(this.baseIndentationLevel);
121 memento.setCurrentIndentationLevel(this.currentIndentationLevel);
122 memento.setCurrentlyInYaml(this.currentlyInYAML);
123 memento.setDiagnosticBuffer(this.diagnosticBuffer);
124 memento.setLastParsedElement(this.lastParsedElement);
125 memento.setFirstLine(this.firstLine);
126 memento.setPlanBeforeTestResult(this.planBeforeTestResult);
127 memento.setTestSet(this.testSet);
128 this.mementos.push(memento);
129 }
130
131
132
133
134 private void loadMemento() {
135 Memento memento = this.mementos.pop();
136 this.baseIndentationLevel = memento.getBaseIndentationLevel();
137 this.currentIndentationLevel = memento.getCurrentIndentationLevel();
138 this.currentlyInYAML = memento.isCurrentlyInYaml();
139 this.diagnosticBuffer = memento.getDiagnosticBuffer();
140 this.lastParsedElement = memento.getLastParsedElement();
141 this.firstLine = memento.isFirstLine();
142 this.planBeforeTestResult = memento.isPlanBeforeTestResult();
143 this.testSet = memento.getTestSet();
144 }
145
146
147
148
149
150 public TestSet getTestSet() {
151 return this.testSet;
152 }
153
154
155
156
157
158 public void parseLine(String tapLine) {
159 Matcher matcher = null;
160
161
162 matcher = COMMENT_PATTERN.matcher(tapLine);
163 if (matcher.matches()) {
164 this.extractComment(matcher);
165 return;
166 }
167
168
169 lastLine = tapLine;
170
171
172
173
174
175 if (this.isBaseIndentationAlreadyDefined()) {
176 matcher = INDENTANTION_PATTERN.matcher(tapLine);
177 if (matcher.matches()) {
178 String spaces = matcher.group(1);
179 int indentation = spaces.length();
180 this.currentIndentationLevel = indentation;
181 if (indentation > this.baseIndentationLevel) {
182
183
184
185
186 if (tapLine.trim().equals("---")) {
187 this.currentlyInYAML = true;
188 return;
189 } else if (tapLine.trim().equals("...")) {
190 this.currentlyInYAML = false;
191 return;
192 } else if (this.currentlyInYAML) {
193 this.appendTapLineToDiagnosticBuffer(tapLine);
194 return;
195 } else {
196
197
198 if (this.lastParsedElement instanceof TestResult) {
199 indentation = baseIndentationLevel;
200 TestResult lastTestResult = (TestResult) this.lastParsedElement;
201 TestSet newTestSet = new TestSet();
202 lastTestResult.setSubtest(newTestSet);
203 this.saveMemento();
204 this.init();
205 this.testSet = newTestSet;
206 }
207 }
208 }
209
210
211 this.checkIndentationLevel(indentation, tapLine);
212 }
213 }
214
215
216 this.checkAndParseTapDiagnostic();
217
218
219 matcher = HEADER_PATTERN.matcher(tapLine);
220 if (matcher.matches()) {
221 this.setIndentationLevelIfNotDefined(tapLine);
222
223 this.currentIndentationLevel = this.baseIndentationLevel;
224
225 this.checkTAPHeaderParsingLocationAndDuplicity();
226
227 this.extractHeader(matcher);
228 this.firstLine = false;
229
230 this.lastParsedElement = this.testSet.getHeader();
231
232 return;
233 }
234
235
236
237
238
239 matcher = PLAN_PATTERN.matcher(tapLine);
240 if (matcher.matches()) {
241 this.checkTAPPlanDuplicity();
242
243 this.checkIfTAPPlanIsSetBeforeTestResultsOrBailOut();
244
245 this.setIndentationLevelIfNotDefined(tapLine);
246
247 this.extractPlan(matcher);
248 this.firstLine = false;
249
250 this.lastParsedElement = this.testSet.getPlan();
251
252 return;
253 }
254
255
256 matcher = TEST_RESULT_PATTERN.matcher(tapLine);
257 if (matcher.matches()) {
258 this.setIndentationLevelIfNotDefined(tapLine);
259
260 this.extractTestResult(matcher);
261
262 this.lastParsedElement = this.testSet.getTapLines()
263 .get((this.testSet.getTapLines().size() - 1));
264
265 return;
266 }
267
268
269 matcher = BAIL_OUT_PATTERN.matcher(tapLine);
270 if (matcher.matches()) {
271
272 this.setIndentationLevelIfNotDefined(tapLine);
273
274 this.extractBailOut(matcher);
275
276 this.lastParsedElement = this.testSet.getTapLines()
277 .get((this.testSet.getTapLines().size() - 1));
278
279 return;
280 }
281
282
283 matcher = FOOTER_PATTERN.matcher(tapLine);
284 if (matcher.matches()) {
285 this.extractFooter(matcher);
286
287 this.lastParsedElement = this.testSet.getFooter();
288
289 return;
290 }
291
292
293 final Text text = new Text(tapLine);
294 this.lastParsedElement = text;
295 this.testSet.addTapLine(text);
296
297 }
298
299
300
301
302 protected void checkIfTAPPlanIsSetBeforeTestResultsOrBailOut() {
303 if (this.testSet.getTestResults().size() <= 0 &&
304 this.testSet.getBailOuts().size() <= 0) {
305 this.planBeforeTestResult = true;
306 }
307 }
308
309
310
311
312
313
314 protected void checkTAPHeaderParsingLocationAndDuplicity() {
315 if (this.testSet.getHeader() != null) {
316 throw new ParserException("Duplicated TAP Header found.");
317 }
318 if (!firstLine) {
319 throw new ParserException(
320 "Invalid position of TAP Header. It must be the first element (apart of Comments) in the TAP Stream.");
321 }
322 }
323
324
325
326
327 protected void checkTAPPlanDuplicity() {
328 if (this.testSet.getPlan() != null) {
329 throw new ParserException("Duplicated TAP Plan found.");
330 }
331 }
332
333
334
335
336
337
338
339
340
341 protected void checkTAPPlanPosition() {
342 if (!this.planBeforeTestResult) {
343 Matcher matcher = PLAN_PATTERN.matcher(lastLine);
344
345 if (matcher.matches()) {
346 return;
347 }
348
349 throw new ParserException("Invalid position of TAP Plan.");
350 }
351 }
352
353
354
355
356
357
358 protected void checkTAPPlanIsSet() {
359 if (this.testSet.getPlan() == null) {
360 throw new ParserException("Missing TAP Plan.");
361 }
362 }
363
364
365
366
367
368
369 protected void extractHeader(Matcher matcher) {
370 final Integer version = Integer.parseInt(matcher.group(1));
371
372 final Header header = new Header(version);
373
374 final String commentToken = matcher.group(2);
375
376 if (commentToken != null) {
377 String text = matcher.group(3);
378 final Comment comment = new Comment(text);
379 header.setComment(comment);
380 }
381
382 this.testSet.setHeader(header);
383 }
384
385
386
387
388 protected void extractPlan(Matcher matcher) {
389 Integer initialTest = Integer.parseInt(matcher.group(1));
390 Integer lastTest = Integer.parseInt(matcher.group(3));
391
392 Plan plan = null;
393 plan = new Plan(initialTest, lastTest);
394
395 String skipToken = matcher.group(4);
396 if (skipToken != null) {
397 String reason = matcher.group(5);
398 final SkipPlan skip = new SkipPlan(reason);
399 plan.setSkip(skip);
400 }
401
402 String commentToken = matcher.group(6);
403 if (commentToken != null) {
404 String text = matcher.group(7);
405 final Comment comment = new Comment(text);
406 plan.setComment(comment);
407 }
408
409 this.testSet.setPlan(plan);
410 }
411
412
413
414
415 protected void extractTestResult(Matcher matcher) {
416 TestResult testResult = null;
417
418 final String okOrNotOk = matcher.group(1);
419 StatusValues status = null;
420 if (okOrNotOk.trim().equals("ok")) {
421 status = StatusValues.OK;
422 } else
423 {
424 status = StatusValues.NOT_OK;
425 }
426
427 Integer testNumber = this.getTestNumber(matcher.group(2));
428
429 testResult = new TestResult(status, testNumber);
430
431 testResult.setDescription(matcher.group(3));
432
433 String directiveToken = matcher.group(4);
434 if (directiveToken != null) {
435 String directiveText = matcher.group(5);
436 DirectiveValues directiveValue = null;
437 if (directiveText.trim().equalsIgnoreCase("todo")) {
438 directiveValue = DirectiveValues.TODO;
439 } else {
440 directiveValue = DirectiveValues.SKIP;
441 }
442 String reason = matcher.group(6);
443 Directive directive = new Directive(directiveValue, reason);
444 testResult.setDirective(directive);
445 }
446
447 String commentToken = matcher.group(7);
448 if (commentToken != null) {
449 String text = matcher.group(8);
450 final Comment comment = new Comment(text);
451 comment.setInline(Boolean.TRUE);
452 testResult.addComment(comment);
453 }
454
455 this.testSet.addTestResult(testResult);
456 }
457
458
459
460
461
462
463
464
465
466 private Integer getTestNumber(String testNumber) {
467 Integer integerTestNumber = null;
468 if (StringUtils.isEmpty(testNumber)) {
469 integerTestNumber = (this.testSet.getTestResults().size() + 1);
470 } else {
471 integerTestNumber = Integer.parseInt(testNumber);
472 }
473 return integerTestNumber;
474 }
475
476
477
478
479 protected void extractBailOut(Matcher matcher) {
480 String reason = matcher.group(1);
481
482 BailOut bailOut = new BailOut(reason);
483
484 String commentToken = matcher.group(2);
485
486 if (commentToken != null) {
487 String text = matcher.group(3);
488 Comment comment = new Comment(text);
489 bailOut.setComment(comment);
490 }
491
492 this.testSet.addBailOut(bailOut);
493 }
494
495
496
497
498 protected void extractComment(Matcher matcher) {
499 String text = matcher.group(1);
500 Comment comment = new Comment(text);
501
502 this.testSet.addComment(comment);
503
504 if (lastParsedElement instanceof TestResult) {
505 TestResult lastTestResult = (TestResult) lastParsedElement;
506 lastTestResult.addComment(comment);
507 }
508 }
509
510
511
512
513
514
515 protected void extractFooter(Matcher matcher) {
516 String text = matcher.group(1);
517 Footer footer = new Footer(text);
518
519 final String commentToken = matcher.group(2);
520
521 if (commentToken != null) {
522 String commentText = matcher.group(3);
523 final Comment comment = new Comment(commentText);
524 footer.setComment(comment);
525 }
526
527 this.testSet.setFooter(footer);
528 }
529
530
531
532
533
534 public TestSet parseTapStream(String tapStream) {
535
536 this.init();
537
538 Scanner scanner = null;
539
540 try {
541 scanner = new Scanner(tapStream);
542 String line = null;
543
544 while (scanner.hasNextLine()) {
545 line = scanner.nextLine();
546 if (StringUtils.isNotEmpty(line)) {
547 this.parseLine(line);
548 }
549 }
550 this.postProcess();
551 } catch (Exception e) {
552 throw new ParserException("Error parsing TAP Stream: " +
553 e.getMessage(), e);
554 } finally {
555 if (scanner != null) {
556 scanner.close();
557 }
558 }
559
560 return this.getTestSet();
561
562 }
563
564
565
566
567
568 public TestSet parseFile(File tapFile) {
569
570 this.init();
571
572 Scanner scanner = null;
573
574 try {
575 scanner = new Scanner(tapFile);
576 String line = null;
577
578 while (scanner.hasNextLine()) {
579 line = scanner.nextLine();
580 if (StringUtils.isNotBlank(line)) {
581 this.parseLine(line);
582 }
583 }
584 this.postProcess();
585 } catch (Exception e) {
586 throw new ParserException("Error parsing TAP Stream: " +
587 e.getMessage(), e);
588 } finally {
589 if (scanner != null) {
590 scanner.close();
591 }
592 }
593
594 return this.getTestSet();
595 }
596
597
598
599
600 private void setIndentationLevelIfNotDefined(String tapLine) {
601 if (this.isBaseIndentationAlreadyDefined() == Boolean.FALSE) {
602 this.baseIndentationLevel = this.getIndentationLevel(tapLine);
603 }
604 }
605
606
607
608
609
610
611
612
613
614 private void checkIndentationLevel(int indentation, String tapLine) {
615 if (indentation < this.baseIndentationLevel) {
616 if (!this.currentlyInYAML &&
617 this.mementos.isEmpty() == Boolean.FALSE) {
618 while (!this.mementos.isEmpty() &&
619 indentation < this.baseIndentationLevel) {
620 this.loadMemento();
621 }
622 } else {
623 throw new ParserException("Invalid indentantion. " +
624 "Check your TAP Stream. Line: " +
625 tapLine);
626 }
627 }
628 }
629
630
631
632
633
634
635
636 private int getIndentationLevel(String tapLine) {
637 int indentationLevel = 0;
638
639 final Matcher indentMatcher = INDENTANTION_PATTERN.matcher(tapLine);
640
641 if (indentMatcher.matches()) {
642 String spaces = indentMatcher.group(1);
643 indentationLevel = spaces.length();
644 }
645 return indentationLevel;
646 }
647
648
649
650
651
652
653
654
655
656
657
658 private void checkAndParseTapDiagnostic() {
659
660 if (diagnosticBuffer.length() > 0) {
661
662 if (this.lastParsedElement == null) {
663 throw new ParserException(
664 "Found diagnostic information without a previous TAP element.");
665 }
666
667 try {
668
669
670 @SuppressWarnings("unchecked")
671 Map<String, Object> metaIterable = (Map<String, Object>) yaml
672 .load(diagnosticBuffer.toString());
673 this.lastParsedElement.setDiagnostic(metaIterable);
674 } catch (Exception ex) {
675 throw new ParserException("Error parsing YAML [" +
676 diagnosticBuffer.toString() + "]: " +
677 ex.getMessage(), ex);
678 }
679
680 diagnosticBuffer = new StringBuilder();
681 }
682 }
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705 private void appendTapLineToDiagnosticBuffer(String diagnosticLine) {
706 if (diagnosticLine.trim().equals("---") ||
707 diagnosticLine.trim().equals("...")) {
708 return;
709 }
710
711 if (this.currentlyInYAML) {
712 diagnosticBuffer.append(diagnosticLine);
713 diagnosticBuffer.append('\n');
714 }
715 }
716
717
718
719
720 protected boolean isBaseIndentationAlreadyDefined() {
721 return this.baseIndentationLevel >= 0;
722 }
723
724
725
726
727
728 protected void postProcess() {
729 this.checkTAPPlanIsSet();
730 this.checkAndParseTapDiagnostic();
731 }
732
733 }