1import os
2from xml.sax.saxutils import escape
3from json import JSONEncoder
4
5# Test result codes.
6
7class ResultCode(object):
8    """Test result codes."""
9
10    # We override __new__ and __getnewargs__ to ensure that pickling still
11    # provides unique ResultCode objects in any particular instance.
12    _instances = {}
13    def __new__(cls, name, isFailure):
14        res = cls._instances.get(name)
15        if res is None:
16            cls._instances[name] = res = super(ResultCode, cls).__new__(cls)
17        return res
18    def __getnewargs__(self):
19        return (self.name, self.isFailure)
20
21    def __init__(self, name, isFailure):
22        self.name = name
23        self.isFailure = isFailure
24
25    def __repr__(self):
26        return '%s%r' % (self.__class__.__name__,
27                         (self.name, self.isFailure))
28
29PASS        = ResultCode('PASS', False)
30XFAIL       = ResultCode('XFAIL', False)
31FAIL        = ResultCode('FAIL', True)
32XPASS       = ResultCode('XPASS', True)
33UNRESOLVED  = ResultCode('UNRESOLVED', True)
34UNSUPPORTED = ResultCode('UNSUPPORTED', False)
35
36# Test metric values.
37
38class MetricValue(object):
39    def format(self):
40        """
41        format() -> str
42
43        Convert this metric to a string suitable for displaying as part of the
44        console output.
45        """
46        raise RuntimeError("abstract method")
47
48    def todata(self):
49        """
50        todata() -> json-serializable data
51
52        Convert this metric to content suitable for serializing in the JSON test
53        output.
54        """
55        raise RuntimeError("abstract method")
56
57class IntMetricValue(MetricValue):
58    def __init__(self, value):
59        self.value = value
60
61    def format(self):
62        return str(self.value)
63
64    def todata(self):
65        return self.value
66
67class RealMetricValue(MetricValue):
68    def __init__(self, value):
69        self.value = value
70
71    def format(self):
72        return '%.4f' % self.value
73
74    def todata(self):
75        return self.value
76
77class JSONMetricValue(MetricValue):
78    """
79        JSONMetricValue is used for types that are representable in the output
80        but that are otherwise uninterpreted.
81    """
82    def __init__(self, value):
83        # Ensure the value is a serializable by trying to encode it.
84        # WARNING: The value may change before it is encoded again, and may
85        #          not be encodable after the change.
86        try:
87            e = JSONEncoder()
88            e.encode(value)
89        except TypeError:
90            raise
91        self.value = value
92
93    def format(self):
94        e = JSONEncoder(indent=2, sort_keys=True)
95        return e.encode(self.value)
96
97    def todata(self):
98        return self.value
99
100def toMetricValue(value):
101    if isinstance(value, MetricValue):
102        return value
103    elif isinstance(value, int) or isinstance(value, long):
104        return IntMetricValue(value)
105    elif isinstance(value, float):
106        return RealMetricValue(value)
107    else:
108        # Try to create a JSONMetricValue and let the constructor throw
109        # if value is not a valid type.
110        return JSONMetricValue(value)
111
112
113# Test results.
114
115class Result(object):
116    """Wrapper for the results of executing an individual test."""
117
118    def __init__(self, code, output='', elapsed=None):
119        # The result code.
120        self.code = code
121        # The test output.
122        self.output = output
123        # The wall timing to execute the test, if timing.
124        self.elapsed = elapsed
125        # The metrics reported by this test.
126        self.metrics = {}
127
128    def addMetric(self, name, value):
129        """
130        addMetric(name, value)
131
132        Attach a test metric to the test result, with the given name and list of
133        values. It is an error to attempt to attach the metrics with the same
134        name multiple times.
135
136        Each value must be an instance of a MetricValue subclass.
137        """
138        if name in self.metrics:
139            raise ValueError("result already includes metrics for %r" % (
140                    name,))
141        if not isinstance(value, MetricValue):
142            raise TypeError("unexpected metric value: %r" % (value,))
143        self.metrics[name] = value
144
145# Test classes.
146
147class TestSuite:
148    """TestSuite - Information on a group of tests.
149
150    A test suite groups together a set of logically related tests.
151    """
152
153    def __init__(self, name, source_root, exec_root, config):
154        self.name = name
155        self.source_root = source_root
156        self.exec_root = exec_root
157        # The test suite configuration.
158        self.config = config
159
160    def getSourcePath(self, components):
161        return os.path.join(self.source_root, *components)
162
163    def getExecPath(self, components):
164        return os.path.join(self.exec_root, *components)
165
166class Test:
167    """Test - Information on a single test instance."""
168
169    def __init__(self, suite, path_in_suite, config, file_path = None):
170        self.suite = suite
171        self.path_in_suite = path_in_suite
172        self.config = config
173        self.file_path = file_path
174        # A list of conditions under which this test is expected to fail. These
175        # can optionally be provided by test format handlers, and will be
176        # honored when the test result is supplied.
177        self.xfails = []
178        # The test result, once complete.
179        self.result = None
180
181    def setResult(self, result):
182        if self.result is not None:
183            raise ArgumentError("test result already set")
184        if not isinstance(result, Result):
185            raise ArgumentError("unexpected result type")
186
187        self.result = result
188
189        # Apply the XFAIL handling to resolve the result exit code.
190        if self.isExpectedToFail():
191            if self.result.code == PASS:
192                self.result.code = XPASS
193            elif self.result.code == FAIL:
194                self.result.code = XFAIL
195
196    def getFullName(self):
197        return self.suite.config.name + ' :: ' + '/'.join(self.path_in_suite)
198
199    def getFilePath(self):
200        if self.file_path:
201            return self.file_path
202        return self.getSourcePath()
203
204    def getSourcePath(self):
205        return self.suite.getSourcePath(self.path_in_suite)
206
207    def getExecPath(self):
208        return self.suite.getExecPath(self.path_in_suite)
209
210    def isExpectedToFail(self):
211        """
212        isExpectedToFail() -> bool
213
214        Check whether this test is expected to fail in the current
215        configuration. This check relies on the test xfails property which by
216        some test formats may not be computed until the test has first been
217        executed.
218        """
219
220        # Check if any of the xfails match an available feature or the target.
221        for item in self.xfails:
222            # If this is the wildcard, it always fails.
223            if item == '*':
224                return True
225
226            # If this is an exact match for one of the features, it fails.
227            if item in self.config.available_features:
228                return True
229
230            # If this is a part of the target triple, it fails.
231            if item in self.suite.config.target_triple:
232                return True
233
234        return False
235
236
237    def getJUnitXML(self):
238        test_name = self.path_in_suite[-1]
239        test_path = self.path_in_suite[:-1]
240        safe_test_path = [x.replace(".","_") for x in test_path]
241        safe_name = self.suite.name.replace(".","-")
242
243        if safe_test_path:
244            class_name = safe_name + "." + "/".join(safe_test_path)
245        else:
246            class_name = safe_name + "." + safe_name
247
248        xml = "<testcase classname='" + class_name + "' name='" + \
249            test_name + "'"
250        xml += " time='%.2f'" % (self.result.elapsed,)
251        if self.result.code.isFailure:
252            xml += ">\n\t<failure >\n" + escape(self.result.output)
253            xml += "\n\t</failure>\n</testcase>"
254        else:
255            xml += "/>"
256        return xml
257