File: validation.py

package info (click to toggle)
espresso 6.7-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 311,068 kB
  • sloc: f90: 447,429; ansic: 52,566; sh: 40,631; xml: 37,561; tcl: 20,077; lisp: 5,923; makefile: 4,503; python: 4,379; perl: 1,219; cpp: 761; fortran: 618; java: 568; awk: 128
file content (271 lines) | stat: -rw-r--r-- 11,388 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
'''
testcode2.validation
--------------------

Classes and functions for comparing data.

:copyright: (c) 2012 James Spencer.
:license: modified BSD; see LICENSE for more details.
'''

from __future__ import print_function
import re
import sys
import warnings

import testcode2.ansi as ansi
import testcode2.compatibility as compat
import testcode2.exceptions as exceptions

class Status:
    '''Enum-esque object for storing whether an object passed a comparison.

bools: iterable of boolean objects.  If all booleans are True (False) then the
       status is set to pass (fail) and if only some booleans are True, the
       status is set to warning (partial pass).
status: existing status to use.  bools is ignored if status is supplied.
name: name of status (unknown, skipped, passed, partial, failed) to use.
      Setting name overrides bools and status.
'''
    def __init__(self, bools=None, status=None, name=None):
        (self._unknown, self._skipped) = (-2, -1)
        (self._passed, self._partial, self._failed) = (0, 1, 2)
        if name is not None:
            setattr(self, 'status', getattr(self, '_'+name))
        elif status is not None:
            self.status = status
        elif bools:
            if compat.compat_all(bools):
                self.status = self._passed
            elif compat.compat_any(bools):
                self.status = self._partial
            else:
                self.status = self._failed
        else:
            self.status = self._unknown
    def unknown(self):
        '''Return true if stored status is unknown.'''
        return self.status == self._unknown
    def skipped(self):
        '''Return true if stored status is skipped.'''
        return self.status == self._skipped
    def passed(self):
        '''Return true if stored status is passed.'''
        return self.status == self._passed
    def warning(self):
        '''Return true if stored status is a partial pass.'''
        return self.status == self._partial
    def failed(self):
        '''Return true if stored status is failed.'''
        return self.status == self._failed
    def print_status(self, msg=None, verbose=1, vspace=True):
        '''Print status.

msg: optional message to print out after status.
verbose: 0: suppress all output except for . (for pass), U (for unknown),
            W (for warning/partial pass) and F (for fail) without a newline.
         1: print 'Passed', 'Unknown', 'WARNING' or '**FAILED**'.
         2: as for 1 plus print msg (if supplied).
         3: as for 2 plus print a blank line.
vspace: print out extra new line afterwards if verbose > 1.
'''
        if verbose > 0:
            if self.status == self._unknown:
                print('Unknown.')
            elif self.status == self._passed:
                print('Passed.')
            elif self.status == self._skipped:
                print(('%s.' % ansi.ansi_format('SKIPPED', 'blue')))
            elif self.status == self._partial:
                print(('%s.' % ansi.ansi_format('WARNING', 'blue')))
            else:
                print(('%s.' % ansi.ansi_format('**FAILED**', 'red', 'normal', 'bold')))
            if msg and verbose > 1:
                print(msg)
            if vspace and verbose >  1:
                print('')
        else:
            if self.status == self._unknown:
                sys.stdout.write('U')
            elif self.status == self._skipped:
                sys.stdout.write('S')
            elif self.status == self._passed:
                sys.stdout.write('.')
            elif self.status == self._partial:
                sys.stdout.write('W')
            else:
                sys.stdout.write('F')
            sys.stdout.flush()
    def __add__(self, other):
        '''Add two status objects.

Return the maximum level (ie most "failed") status.'''
        return Status(status=max(self.status, other.status))

class Tolerance:
    '''Store absolute and relative tolerances

Given are regarded as equal if they are within these tolerances.

name: name of tolerance object.
absolute: threshold for absolute difference between two numbers.
relative: threshold for relative difference between two numbers.
strict: if true, then require numbers to be within both thresholds.
'''
    def __init__(self, name='', absolute=None, relative=None, strict=True):
        self.name = name
        self.absolute = absolute
        self.relative = relative
        if not self.absolute and not self.relative:
            err = 'Neither absolute nor relative tolerance given.'
            raise exceptions.TestCodeError(err)
        self.strict = strict
    def __repr__(self):
        return (self.absolute, self.relative, self.strict).__repr__()
    def __hash__(self):
        return hash(self.name)
    def __eq__(self, other):
        return (isinstance(other, self.__class__) and
                self.__dict__ == other.__dict__)
    def validate(self, test_val, benchmark_val, key=''):
        '''Compare test and benchmark values to within the tolerances.'''
        status = Status([True])
        msg = ['values are within tolerance.']
        compare = '(Test: %s.  Benchmark: %s.)' % (test_val, benchmark_val)
        try:
            # Check float is not NaN (which we can't compare).
            if compat.isnan(test_val) or compat.isnan(benchmark_val):
                status = Status([False])
                msg = ['cannot compare NaNs.']
            else:
                # Check if values are within tolerances.
                (status_absolute, msg_absolute) = \
                        self.validate_absolute(benchmark_val, test_val)
                (status_relative, msg_relative) = \
                        self.validate_relative(benchmark_val, test_val)
                if self.absolute and self.relative and not self.strict:
                    # Require only one of thresholds to be met.
                    status = Status([status_relative.passed(),
                                     status_absolute.passed()])
                else:
                    # Only have one or other of thresholds (require active one
                    # to be met) or have both and strict mode is on (require
                    # both to be met).
                    status = status_relative + status_absolute
                err_stat = ''
                if status.warning():
                    err_stat = 'Warning: '
                elif status.failed():
                    err_stat = 'ERROR: '
                msg = []
                if self.absolute and msg_absolute:
                    msg.append('%s%s %s' % (err_stat, msg_absolute, compare))
                if self.relative and msg_relative:
                    msg.append('%s%s %s' % (err_stat, msg_relative, compare))
        except TypeError:
            if test_val != benchmark_val:
                # require test and benchmark values to be equal (within python's
                # definition of equality).
                status = Status([False])
                msg = ['values are different. ' + compare]
        if key and msg:
            msg.insert(0, key)
            msg = '\n    '.join(msg)
        else:
            msg = '\n'.join(msg)
        return (status, msg)

    def validate_absolute(self, benchmark_val, test_val):
        '''Compare test and benchmark values to the absolute tolerance.'''
        if self.absolute:
            diff = test_val - benchmark_val
            err = abs(diff)
            passed = err < self.absolute
            msg = ''
            if not passed:
                msg = ('absolute error %.2e greater than %.2e.' %
                    (err, self.absolute))
        else:
            passed = True
            msg = 'No absolute tolerance set.  Passing without checking.'
        return (Status([passed]), msg)

    def validate_relative(self, benchmark_val, test_val):
        '''Compare test and benchmark values to the relative tolerance.'''
        if self.relative:
            diff = test_val - benchmark_val
            if benchmark_val == 0 and diff == 0:
                err = 0
            elif benchmark_val == 0:
                err = float("Inf")
            else:
                err = abs(diff/benchmark_val)
            passed = err < self.relative
            msg = ''
            if not passed:
                msg = ('relative error %.2e greater than %.2e.' %
                        (err, self.relative))
        else:
            passed = True
            msg = 'No relative tolerance set.  Passing without checking.'
        return (Status([passed]), msg)


def compare_data(benchmark, test, default_tolerance, tolerances,
        ignore_fields=None):
    '''Compare two data dictionaries.'''
    ignored_params = compat.compat_set(ignore_fields or tuple())
    bench_params = compat.compat_set(benchmark) - ignored_params
    test_params = compat.compat_set(test) - ignored_params
    # Check both the key names and the number of keys in case there are
    # different numbers of duplicate keys.
    comparable = (bench_params == test_params)
    key_counts = dict((key,0) for key in bench_params | test_params)
    for (key, val) in list(benchmark.items()):
        if key not in ignored_params:
            key_counts[key] += len(val)
    for (key, val) in list(test.items()):
        if key not in ignored_params:
            key_counts[key] -= len(val)
    comparable = comparable and compat.compat_all(kc == 0 for kc in list(key_counts.values()))
    status = Status()
    msg = []
    
    if not comparable:
        status = Status([False])
        bench_only = bench_params - test_params
        test_only = test_params - bench_params
        msg.append('Different sets of data extracted from benchmark and test.')
        if bench_only:
            msg.append("    Data only in benchmark: %s." % ", ".join(bench_only))
        if test_only:
            msg.append("    Data only in test: %s." % ", ".join(test_only))
        bench_more = [key for key in key_counts
                        if key_counts[key] > 0 and key not in bench_only]
        test_more = [key for key in key_counts
                        if key_counts[key] < 0 and key not in test_only]
        if bench_more:
            msg.append("    More data in benchmark than in test: %s." %
                           ", ".join(bench_more))
        if test_more:
            msg.append("    More data in test than in benchmark: %s." %
                           ", ".join(test_more))

    for param in (bench_params & test_params):
        param_tol = tolerances.get(param, default_tolerance)
        if param_tol == default_tolerance:
            # See if there's a regex that matches.
            tol_matches = [tol for tol in list(tolerances.values())
                               if tol.name and re.match(tol.name, param)]
            if tol_matches:
                param_tol = tol_matches[0]
                if len(tol_matches) > 1:
                    warnings.warn('Multiple tolerance regexes match.  '
                                  'Using %s.' % (param_tol.name))
        for bench_value, test_value in zip(benchmark[param], test[param]):
            key_status, err = param_tol.validate(test_value, bench_value, param)
            status += key_status
            if not key_status.passed() and err:
                msg.append(err)

    return (comparable, status, "\n".join(msg))