| 1 | schorsch | 1.1 | # -*- coding: utf-8 -*- | 
| 2 |  |  | ''' Text comparison functions for Radiance unit testing. | 
| 3 |  |  |  | 
| 4 |  |  | The comparison allows for differences in whitespace, so we need to | 
| 5 |  |  | split the text into tokens first. Tokens are then converted into an | 
| 6 |  |  | appropriate data type, so that we can do an "almost equal" comparison | 
| 7 |  |  | for floating point values, ignoring binary rounding errors. | 
| 8 |  |  | ''' | 
| 9 |  |  | from __future__ import division, print_function, unicode_literals | 
| 10 |  |  | __all__ = ['error', 'lcompare', 'llcompare', | 
| 11 |  |  | 'split_headers', 'split_rad', 'split_radfile'] | 
| 12 |  |  |  | 
| 13 | schorsch | 1.2 | import re | 
| 14 |  |  | import shlex | 
| 15 | schorsch | 1.1 | # Py2.7/3.x compatibility | 
| 16 |  |  | try: from itertools import (izip_longest as zip_longest, chain, | 
| 17 |  |  | ifilter as filter, izip as zip) | 
| 18 |  |  | except ImportError: | 
| 19 |  |  | from itertools import zip_longest, chain | 
| 20 |  |  |  | 
| 21 |  |  | class error(Exception): pass | 
| 22 |  |  |  | 
| 23 | schorsch | 1.2 | _strtypes = (type(b''), type(u'')) | 
| 24 |  |  |  | 
| 25 | schorsch | 1.1 | # internal functions | 
| 26 |  |  | def _icompare(itest, iref): | 
| 27 |  |  | '''compare ints (not public)''' | 
| 28 | schorsch | 1.2 | if isinstance(itest, _strtypes): | 
| 29 | schorsch | 1.1 | iftest = int(itest) | 
| 30 |  |  | else: iftest = itest | 
| 31 | schorsch | 1.2 | if iftest == iref: return True | 
| 32 |  |  | return False | 
| 33 | schorsch | 1.1 |  | 
| 34 |  |  | def _fcompare(ftest, fref): | 
| 35 |  |  | '''compare floats (not public)''' | 
| 36 |  |  | FUZZ = 0.0000001 # XXX heuristically determined (quite low?) | 
| 37 | schorsch | 1.2 | if isinstance(ftest, _strtypes): | 
| 38 | schorsch | 1.1 | fftest = float(ftest) | 
| 39 |  |  | else: fftest = ftest | 
| 40 |  |  | if (fftest < (fref + FUZZ)) and (fftest > (fref - FUZZ)): | 
| 41 |  |  | return True | 
| 42 |  |  | return False | 
| 43 |  |  |  | 
| 44 |  |  | def _typify_token(t): | 
| 45 |  |  | '''return the token as int resp. float if possible (not public)''' | 
| 46 |  |  | try: return int(t) | 
| 47 |  |  | except ValueError: pass | 
| 48 |  |  | try: return float(t) | 
| 49 |  |  | except ValueError: pass | 
| 50 |  |  | return t | 
| 51 |  |  |  | 
| 52 |  |  |  | 
| 53 |  |  | # public comparison functions | 
| 54 |  |  |  | 
| 55 |  |  | def lcompare(ltest, lref): | 
| 56 |  |  | '''Compare a list/iterator of tokens. | 
| 57 |  |  | Raise an error if there are intolerable differences. | 
| 58 |  |  | The reference tokens in lref should already be of the correct type. | 
| 59 |  |  | ''' | 
| 60 |  |  | i = 0 | 
| 61 |  |  | for ttest, tref in zip_longest(ltest, lref, fillvalue=False): | 
| 62 |  |  | if ttest is False: | 
| 63 |  |  | raise error('List comparision failed: Fewer tokens than expected' | 
| 64 |  |  | ' (%d  !=  >= %d)' % (i, i+1)) | 
| 65 |  |  | if tref is False: | 
| 66 |  |  | raise error('List comparision failed: More tokens than expected' | 
| 67 |  |  | ' (>= %d  !=  %d)' % (i+1, i)) | 
| 68 | schorsch | 1.2 | if isinstance(tref, _strtypes) and tref != ttest: | 
| 69 | schorsch | 1.1 | raise error('String token comparison failed: "%s" != "%s"' | 
| 70 |  |  | % (ttest, tref)) | 
| 71 |  |  | elif type(tref) == int and not _icompare(ttest, tref): | 
| 72 |  |  | raise error('Int token comparison failed: %s != %s' % (ttest, tref)) | 
| 73 |  |  | elif type(tref) == float and not _fcompare(ttest, tref): | 
| 74 |  |  | raise error('Float token comparison failed: %s != %s' | 
| 75 |  |  | % (ttest, tref)) | 
| 76 |  |  | i += 1 | 
| 77 |  |  |  | 
| 78 |  |  | def llcompare(lltest, llref, ignore_empty=False, _recurse=[]): | 
| 79 |  |  | '''Compare a list/iterator of lists/iterators of tokens recursively. | 
| 80 |  |  | Raise an error if there are intolerable differences. | 
| 81 |  |  | The reference tokens in lref should already be of the correct type. | 
| 82 |  |  | If ignore_empty is true, empty lines are not included in the comparison. | 
| 83 |  |  | The _recurse argument is only used internally. | 
| 84 |  |  | ''' | 
| 85 |  |  | if ignore_empty: | 
| 86 |  |  | lltest = filter(None, lltest) | 
| 87 |  |  | llref = filter(None, llref) | 
| 88 |  |  | i = 0 | 
| 89 |  |  | for ltest, lref in zip_longest(lltest, llref, fillvalue=False): | 
| 90 |  |  | i += 1 | 
| 91 |  |  | if ltest is False: | 
| 92 |  |  | raise error('List comparision failed: Fewer entries than expected' | 
| 93 |  |  | ' (%d  !=  >= %d)' % (i, i+1)) | 
| 94 |  |  | if lref is False: | 
| 95 |  |  | raise error('List comparision failed: More entries than expected' | 
| 96 |  |  | ' (>= %d  !=  %d)' % (i+1, i)) | 
| 97 | schorsch | 1.2 | if lref and not isinstance(lref, _strtypes): | 
| 98 |  |  |  | 
| 99 | schorsch | 1.1 | if hasattr(lref, '__getitem__'): | 
| 100 |  |  | rfirst = lref[0] | 
| 101 |  |  | elif hasattr(lref, 'next') or hasattr(lref, '__next__'): | 
| 102 |  |  | rfirst = next(lref) # "peek" at first | 
| 103 |  |  | lref = chain([rfirst], lref) # "push" back | 
| 104 |  |  | else: rfirst = None | 
| 105 | schorsch | 1.2 | if isinstance(rfirst, _strtypes): | 
| 106 | schorsch | 1.1 | rfirst = None | 
| 107 |  |  | if hasattr(rfirst, '__iter__') or isinstance(rfirst, (list, tuple)): | 
| 108 | schorsch | 1.2 | llcompare(ltest, lref, | 
| 109 | schorsch | 1.1 | _recurse=_recurse + [i], ignore_empty=ignore_empty) | 
| 110 |  |  | try: lcompare(ltest, lref) | 
| 111 | schorsch | 1.2 | #               except TypeError: | 
| 112 |  |  | #                       print(ltest, lref) | 
| 113 |  |  | #                       raise | 
| 114 | schorsch | 1.1 | except error as e: | 
| 115 |  |  | if _recurse: | 
| 116 |  |  | raise error('%s (line %s)' % (str(e), _recurse + [i + 1])) | 
| 117 |  |  | else: raise error('%s (line %d)' % (str(e), i + 1)) | 
| 118 |  |  |  | 
| 119 | schorsch | 1.2 | _HLPATS  = '(\s*)(?:([^=\s]*)\s*=\s*(.*)\s*|(.*)\s*)' | 
| 120 |  |  | _hlpat = re.compile(_HLPATS) | 
| 121 | schorsch | 1.1 | def split_headers(s): | 
| 122 |  |  | '''split Radiance file headers (eg. the output of getinfo). | 
| 123 |  |  | Return a list of lists of tokens suitable for llcompare().''' | 
| 124 | schorsch | 1.2 | ll = s.split('\n') | 
| 125 | schorsch | 1.1 | nll = [] | 
| 126 |  |  | for l in ll: | 
| 127 | schorsch | 1.2 | m = _hlpat.match(l) | 
| 128 |  |  | groups = m.groups() | 
| 129 |  |  | indent = groups[0] | 
| 130 |  |  | if groups[1]: | 
| 131 |  |  | left = groups[1] | 
| 132 |  |  | right = [_typify_token(s) for s in shlex.split(groups[2])] | 
| 133 |  |  | nll.append([indent, left, '='] + [right]) | 
| 134 |  |  | else: | 
| 135 |  |  | full = [_typify_token(s) for s in shlex.split(groups[3])] | 
| 136 |  |  | nll.append([indent] + [full]) | 
| 137 | schorsch | 1.1 | return nll | 
| 138 |  |  |  | 
| 139 |  |  | def split_rad(s): | 
| 140 |  |  | '''Split the contents of a scene description string. | 
| 141 |  |  | Return a list of list of tokens suitable for llcompare().''' | 
| 142 |  |  | ll = [ss.strip() for ss in s.split('\n')] | 
| 143 |  |  | return [[_typify_token(s) for s in l.split()] for l in ll] | 
| 144 |  |  |  | 
| 145 |  |  | def split_radfile(fn): | 
| 146 |  |  | '''Split the contents of a file containing a scene description. | 
| 147 |  |  | Return a list of list of tokens suitable for llcompare().''' | 
| 148 |  |  | with open(fn, 'r') as f: | 
| 149 |  |  | return split_rad(f.read()) | 
| 150 |  |  |  |