Add python tap runner to the tree
[babeltrace.git] / tests / utils / python / tap / parser.py
diff --git a/tests/utils/python/tap/parser.py b/tests/utils/python/tap/parser.py
new file mode 100644 (file)
index 0000000..7b2f096
--- /dev/null
@@ -0,0 +1,128 @@
+# Copyright (c) 2016, Matt Layman
+
+from io import StringIO
+import re
+import sys
+
+from tap.directive import Directive
+from tap.i18n import _
+from tap.line import Bail, Diagnostic, Plan, Result, Unknown, Version
+
+
+class Parser(object):
+    """A parser for TAP files and lines."""
+
+    # ok and not ok share most of the same characteristics.
+    result_base = r"""
+        \s*                    # Optional whitespace.
+        (?P<number>\d*)        # Optional test number.
+        \s*                    # Optional whitespace.
+        (?P<description>[^#]*) # Optional description before #.
+        \#?                    # Optional directive marker.
+        \s*                    # Optional whitespace.
+        (?P<directive>.*)      # Optional directive text.
+    """
+    ok = re.compile(r'^ok' + result_base, re.VERBOSE)
+    not_ok = re.compile(r'^not\ ok' + result_base, re.VERBOSE)
+    plan = re.compile(r"""
+        ^1..(?P<expected>\d+) # Match the plan details.
+        [^#]*                 # Consume any non-hash character to confirm only
+                              # directives appear with the plan details.
+        \#?                   # Optional directive marker.
+        \s*                   # Optional whitespace.
+        (?P<directive>.*)     # Optional directive text.
+    """, re.VERBOSE)
+    diagnostic = re.compile(r'^#')
+    bail = re.compile(r"""
+        ^Bail\ out!
+        \s*            # Optional whitespace.
+        (?P<reason>.*) # Optional reason.
+    """, re.VERBOSE)
+    version = re.compile(r'^TAP version (?P<version>\d+)$')
+
+    TAP_MINIMUM_DECLARED_VERSION = 13
+
+    def parse_file(self, filename):
+        """Parse a TAP file to an iterable of tap.line.Line objects.
+
+        This is a generator method that will yield an object for each
+        parsed line. The file given by `filename` is assumed to exist.
+        """
+        return self.parse(open(filename, 'r'))
+
+    def parse_stdin(self):
+        """Parse a TAP stream from standard input.
+
+        Note: this has the side effect of closing the standard input
+        filehandle after parsing.
+        """
+        return self.parse(sys.stdin)
+
+    def parse_text(self, text):
+        """Parse a string containing one or more lines of TAP output."""
+        return self.parse(StringIO(text))
+
+    def parse(self, fh):
+        """Generate tap.line.Line objects, given a file-like object `fh`.
+
+        `fh` may be any object that implements both the iterator and
+        context management protocol (i.e. it can be used in both a
+        "with" statement and a "for...in" statement.)
+
+        Trailing whitespace and newline characters will be automatically
+        stripped from the input lines.
+        """
+        with fh:
+            for line in fh:
+                yield self.parse_line(line.rstrip())
+
+    def parse_line(self, text):
+        """Parse a line into whatever TAP category it belongs."""
+        match = self.ok.match(text)
+        if match:
+            return self._parse_result(True, match)
+
+        match = self.not_ok.match(text)
+        if match:
+            return self._parse_result(False, match)
+
+        if self.diagnostic.match(text):
+            return Diagnostic(text)
+
+        match = self.plan.match(text)
+        if match:
+            return self._parse_plan(match)
+
+        match = self.bail.match(text)
+        if match:
+            return Bail(match.group('reason'))
+
+        match = self.version.match(text)
+        if match:
+            return self._parse_version(match)
+
+        return Unknown()
+
+    def _parse_plan(self, match):
+        """Parse a matching plan line."""
+        expected_tests = int(match.group('expected'))
+        directive = Directive(match.group('directive'))
+
+        # Only SKIP directives are allowed in the plan.
+        if directive.text and not directive.skip:
+            return Unknown()
+
+        return Plan(expected_tests, directive)
+
+    def _parse_result(self, ok, match):
+        """Parse a matching result line into a result instance."""
+        return Result(
+            ok, match.group('number'), match.group('description').strip(),
+            Directive(match.group('directive')))
+
+    def _parse_version(self, match):
+        version = int(match.group('version'))
+        if version < self.TAP_MINIMUM_DECLARED_VERSION:
+            raise ValueError(_('It is an error to explicitly specify '
+                               'any version lower than 13.'))
+        return Version(version)
This page took 0.02394 seconds and 4 git commands to generate.