[lit] Format JSONMetricValue strings better.
[oota-llvm.git] / utils / lit / lit / Test.py
1 import os
2 from xml.sax.saxutils import escape
3 from json import JSONEncoder
4
5 # Test result codes.
6
7 class 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
29 PASS        = ResultCode('PASS', False)
30 XFAIL       = ResultCode('XFAIL', False)
31 FAIL        = ResultCode('FAIL', True)
32 XPASS       = ResultCode('XPASS', True)
33 UNRESOLVED  = ResultCode('UNRESOLVED', True)
34 UNSUPPORTED = ResultCode('UNSUPPORTED', False)
35
36 # Test metric values.
37
38 class 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
57 class 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
67 class 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
77 class 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
100 def 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
115 class 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
147 class 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
166 class 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