test.py 9.26 KB
Newer Older
1
from difflib import unified_diff as diff
Clément Gillard's avatar
Clément Gillard committed
2
import atexit
3
4
import inspect
import os
Akim Demaille's avatar
Akim Demaille committed
5
import re
6
import sys
Akim Demaille's avatar
Akim Demaille committed
7

8
9
import vcsn

10
11
12
13
14
15
try:
    repatterntype = re.Pattern
except AttributeError:
    # Before Python 3.7
    repatterntype = re._pattern_type

16
ntest = 0
17
18
npass = 0
nfail = 0
Akim Demaille's avatar
Akim Demaille committed
19

20
21
22
23
24
25
26
27
28
29
def num_fail():
    return nfail

def num_pass():
    return npass

def num_test():
    return ntest


30
31
32
33
# For build-checks, use our abs_srcdir from tests/bin/vcsn. For
# install checks, since the latter is not run (it runs
# /usr/local/bin/vcsn), use Automake's srcdir.
srcdir = os.environ['abs_srcdir'] if 'abs_srcdir' in os.environ \
Akim Demaille's avatar
Akim Demaille committed
34
    else os.environ['srcdir']
35
36
37
38

# The directory associated to the current test.
medir = sys.argv[0].replace(".py", ".dir")

Akim Demaille's avatar
Akim Demaille committed
39

40
41
42
43
44
def set_medir(dir):
    global medir
    medir = dir


Akim Demaille's avatar
Akim Demaille committed
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def mefile(fn, ext=None):
    '''The path to the test's file `fn.ext`,
    where `ext` is possibly empty.'''
    return medir + '/' + fn + ('.' + ext if ext else '')

def metext(fn, ext=None):
    '''The content of the test's file `fn.ext`,
    where `ext` is possibly empty.'''
    return open(mefile(fn, ext)).read().strip()

def meaut(fn, ext=None):
    '''The automaton stored in the test's file `fn.ext`,
    where `ext` is possibly empty.'''
    return vcsn.automaton(filename=mefile(fn, ext))


61
62
63
# http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python
def which(program):
    'Return the file name for program if it exists, None otherwise.'
Akim Demaille's avatar
Akim Demaille committed
64

65
66
67
    def is_exe(fpath):
        return os.path.isfile(fpath) and os.access(fpath, os.X_OK)

Clément Gillard's avatar
Clément Gillard committed
68
    fpath, _ = os.path.split(program)
69
70
71
72
73
74
75
76
77
78
79
80
    if fpath:
        if is_exe(program):
            return program
    else:
        for path in os.environ["PATH"].split(os.pathsep):
            path = path.strip('"')
            exe_file = os.path.join(path, program)
            if is_exe(exe_file):
                return exe_file

    return None

Akim Demaille's avatar
Akim Demaille committed
81

82
83
84
85
86
87
88
def rst_file(name, content):
    print(name + "::")
    print()
    for l in content.splitlines():
        print("\t" + l)
    print()

Akim Demaille's avatar
Akim Demaille committed
89

90
def format(d):
91
    '''Pretty-print `d` into a diffable string.'''
92
93
94
95
96
97
    if isinstance(d, dict):
        return "\n".join(["{}: {}".format(k, format(d[k]))
                          for k in sorted(d.keys())])
    else:
        return str(d)

Akim Demaille's avatar
Akim Demaille committed
98

99
def rst_diff(expected, effective):
Akim Demaille's avatar
Akim Demaille committed
100
    "Report the difference bw `expected` and `effective`."
101
102
    exp = format(expected)
    eff = format(effective)
103
104
105
106
107
108
109
110
111
    if exp[:-1] != '\n':
        exp += '\n'
    if eff[:-1] != '\n':
        eff += '\n'
    rst_file("Diff on output",
             ''.join(diff(exp.splitlines(1),
                          eff.splitlines(1),
                          fromfile='expected', tofile='effective')))

Akim Demaille's avatar
Akim Demaille committed
112

113
def load(fname):
Akim Demaille's avatar
Akim Demaille committed
114
    "Load the library automaton file `fname`."
Akim Demaille's avatar
Akim Demaille committed
115
116
    return vcsn.automaton(filename=vcsn.datadir + "/" + fname)

117
118
119

def here():
    # Find the top-level call.
Akim Demaille's avatar
Akim Demaille committed
120
    frame = inspect.currentframe()
121
122
123
124
125
    while frame.f_back:
        frame = frame.f_back
    finfo = inspect.getframeinfo(frame)
    return finfo.filename + ":" + str(finfo.lineno)

Akim Demaille's avatar
Akim Demaille committed
126

127
128
129
130
131
132
def automaton(a):
    if not isinstance(a, vcsn.automaton):
        a = vcsn.automaton(a)
    return a


133
def FAIL(*msg, **kwargs):
134
135
    global ntest, nfail # pylint: disable=global-statement
    ntest += 1
136
    nfail += 1
137
138
    # Don't display multi-line failure messages, only the first line
    # will be reported anyway by the TAP driver.
139
    m = ' '.join(msg)
140
    if m.count("\n") == 0:
141
        print('not ok', ntest, m)
142
    else:
143
        print('not ok', ntest)
Akim Demaille's avatar
Akim Demaille committed
144
    loc = kwargs['loc'] if 'loc' in kwargs and kwargs['loc'] else here()
145
    print(loc + ": fail:", *msg)
Akim Demaille's avatar
Akim Demaille committed
146
    print()
Akim Demaille's avatar
Akim Demaille committed
147
    sys.stdout.flush()
Akim Demaille's avatar
Akim Demaille committed
148

Akim Demaille's avatar
Akim Demaille committed
149

150
def PASS(*msg, **kwargs):
151
152
    global ntest, npass # pylint: disable=global-statement
    ntest += 1
153
    npass += 1
154
    print('ok ', ntest, *msg)
Akim Demaille's avatar
Akim Demaille committed
155
    loc = kwargs['loc'] if 'loc' in kwargs and kwargs['loc'] else here()
156
    print(loc + ": pass:", *msg)
Akim Demaille's avatar
Akim Demaille committed
157
    print()
Akim Demaille's avatar
Akim Demaille committed
158
    sys.stdout.flush()
Akim Demaille's avatar
Akim Demaille committed
159

Akim Demaille's avatar
Akim Demaille committed
160

161
def SKIP(*msg):
Akim Demaille's avatar
Akim Demaille committed
162
    PASS('# SKIP', *msg)
163

Akim Demaille's avatar
Akim Demaille committed
164

165
def XFAIL(fun, exp=None):
166
167
    '''Run `fun`: it should fail.  If `exp` is given, check that the
    exception includes it.'''
Akim Demaille's avatar
Akim Demaille committed
168
169
    try:
        fun()
170
    except RuntimeError as e:
171
172
173
174
175
176
        # Error messages about files that we read will have $srcdir as
        # prefix.  Remove it.  E.g., from
        #   while reading automaton: ../../tests/python/efsm.dir/bad_final_weight.efsm
        # to
        #   while reading automaton: efsm.dir/bad_final_weight.efsm
        eff = str(e).replace(medir + '/', '')
177
        if (exp is None
178
            or isinstance(exp, repatterntype) and re.match(exp, eff)
179
            or isinstance(exp, str) and exp in eff):
180
181
            PASS()
        else:
182
            FAIL('Unexpected error message')
183
            rst_file("Expected error", exp)
184
            rst_file("Effective error", eff)
185
            rst_diff(exp, str(e))
Akim Demaille's avatar
Akim Demaille committed
186
    else:
187
        FAIL('did not raise an exception', str(fun))
Akim Demaille's avatar
Akim Demaille committed
188

Akim Demaille's avatar
Akim Demaille committed
189

190
def CHECK(effective, msg='', loc=None):
191
192
193
194
    "Check that `effective` is `True`."
    if effective:
        PASS(loc=loc)
    else:
195
        FAIL("assertion failed", msg, loc=loc)
196

Akim Demaille's avatar
Akim Demaille committed
197
198

def CHECK_EQ(expected, effective, loc=None):
199
200
    'Check that `effective` is equal to `expected`.'
    aut = isinstance(effective, vcsn.automaton)
201
202
    if isinstance(expected, str) and not isinstance(effective, str):
        effective = str(effective)
203
    if expected == effective:
204
        PASS(loc=loc)
205
    else:
206
207
208
209
        msg = 'Unexpected result'
        if aut:
            expaut = automaton(expected)
            effaut = automaton(effective)
210
211
212
213
            if not effaut.is_accessible():
                msg += (" (different, but cannot check whether isomorphic"
                        ", as is not accessible)")
            elif expaut.is_isomorphic(effaut):
214
215
216
                msg += ' (different but isomorphic)'
            else:
                msg += ' (different and not even isomorphic)'
217
218
        exp = format(expected)
        eff = format(effective)
219
        FAIL(msg, loc=loc)
220
221
        rst_file("Expected output", exp)
        rst_file("Effective output", eff)
222
223
        rst_diff(exp, eff)

Akim Demaille's avatar
Akim Demaille committed
224
225

def CHECK_NE(expected, effective, loc=None):
226
227
228
229
230
231
232
233
234
235
236
237
238
    "Check that `effective` is not equal to `expected`."
    if isinstance(expected, str) and not isinstance(effective, str):
        effective = str(effective)
    if expected != effective:
        PASS(loc=loc)
    else:
        exp = format(expected)
        eff = format(effective)
        FAIL("Unexpected equality", loc=loc)
        rst_file("First argument", exp)
        rst_file("Second argument", eff)
        rst_diff(exp, eff)

Akim Demaille's avatar
Akim Demaille committed
239

240
241
def normalize(a):
    '''Turn automaton `a` into something we can check equivalence with.'''
Akim Demaille's avatar
Akim Demaille committed
242
    a = a.strip().realtime()
Akim Demaille's avatar
Akim Demaille committed
243
244
245
    # Eliminate nullablesets if there are that remain.  This is safe:
    # if there are \e that remain, the following conversion _will_
    # fail.
Akim Demaille's avatar
Akim Demaille committed
246
247
    to = re.sub(r'nullableset<(lal_char\(.*?\)|letterset<char_letters\(.*?\)>)>',
                r'\1',
248
                a.context().format('sname'))
249
250
    return a.automaton(vcsn.context(to))

Akim Demaille's avatar
Akim Demaille committed
251

252
def can_test_equivalence(a):
253
    ctx = a.context().format('sname')
Akim Demaille's avatar
Akim Demaille committed
254
255
256
257
258
259
260
261
262
263
    # Cannot check on Zmin and the like.
    if ctx.endswith('min'):
        return False
    # Cannot check on expressionset as weightset.
    if 'expressionset<' in ctx:
        return False
    # Cannot check equivalence on labelset.
    if 'lat<' in ctx:
        return False
    return True
264

265
266
267
268
def shortest(e, num=20):
    if isinstance(e, vcsn.automaton):
        e = e.proper()
    return e.shortest(num)
Akim Demaille's avatar
Akim Demaille committed
269

Akim Demaille's avatar
Akim Demaille committed
270
def CHECK_EQUIV(a1, a2):
Akim Demaille's avatar
Akim Demaille committed
271
272
    '''Check that `a1` and `a2` are equivalent.  Works for
    two automata, or two expressions.'''
273
274
    # If we cannot check equivalence, check equality of the `num`
    # shortest monomials.
Akim Demaille's avatar
Akim Demaille committed
275
    num = 20
276
    # Cannot compute equivalence on Zmin, approximate with shortest.
Akim Demaille's avatar
Akim Demaille committed
277
    try:
278
        if can_test_equivalence(a1) and can_test_equivalence(a2):
Akim Demaille's avatar
Akim Demaille committed
279
            res = a1.is_equivalent(a2)
Akim Demaille's avatar
Akim Demaille committed
280
            via = '(via is_equivalent)'
281
        else:
282
            res = shortest(a1, num) == shortest(a2, num)
Akim Demaille's avatar
Akim Demaille committed
283
            via = '(via shortests)'
Akim Demaille's avatar
Akim Demaille committed
284
285
286
    except RuntimeError as e:
        FAIL("cannot check equivalence: " + str(e))
        res = False
287
        via = ''
Akim Demaille's avatar
Akim Demaille committed
288
289
290
291

    if res:
        PASS()
    else:
Akim Demaille's avatar
Akim Demaille committed
292
        FAIL("not equivalent", via)
Akim Demaille's avatar
Akim Demaille committed
293
294
        rst_file("Left", format(a1))
        rst_file("Right", format(a2))
Akim Demaille's avatar
Akim Demaille committed
295
        try:
296
297
            s1 = shortest(a1, num).format('list')
            s2 = shortest(a2, num).format('list')
Akim Demaille's avatar
Akim Demaille committed
298
299
300
301
302
303
            rst_file("Left shortest", s1)
            rst_file("Right shortest", s2)
            rst_diff(s1, s2)
        except RuntimeError as e:
            FAIL("cannot run shortest: " + str(e))

Akim Demaille's avatar
Akim Demaille committed
304

305
def CHECK_ISOMORPHIC(a1, a2):
306
    "Check that `a1` and `a2` are isomorphic."
307
    if a1.is_isomorphic(a2):
308
309
        PASS()
    else:
310
311
        a1 = format(a1)
        a2 = format(a2)
312
        FAIL("automata are not isomorphic")
313
314
315
        rst_file("Left automaton", a1)
        rst_file("Right automaton", a2)
        rst_diff(a1, a2)
316

Akim Demaille's avatar
Akim Demaille committed
317

Nicolas Barray's avatar
Nicolas Barray committed
318
319
320
321
322
323
324
325
326
def CHECK_IS_EPS_ACYCLIC(a):
    "Check if `a` is epsilon acyclic."
    if a.is_eps_acyclic():
        PASS()
    else:
        a = format(a)
        FAIL("automata is not epsilon acyclic")
        rst_file(a)

Akim Demaille's avatar
Akim Demaille committed
327

Akim Demaille's avatar
Akim Demaille committed
328
def PLAN():
329
    "TAP requires that we announce the plan: the number of tests."
330
    print('1..' + str(ntest))
331
332
    print('PASS:', npass)
    print('FAIL:', nfail)
333

Sébastien Piat's avatar
Sébastien Piat committed
334
def weightset_of(ctx):
Sébastien Piat's avatar
Sébastien Piat committed
335
    s = str(ctx).lower().split('-> ')
Sébastien Piat's avatar
Sébastien Piat committed
336
    return s[-1] if 0 < len(s) else None
Sébastien Piat's avatar
Sébastien Piat committed
337

338
atexit.register(PLAN)