-
-
Notifications
You must be signed in to change notification settings - Fork 191
Expand file tree
/
Copy pathsh_spec.py
More file actions
executable file
·1668 lines (1301 loc) · 47.7 KB
/
sh_spec.py
File metadata and controls
executable file
·1668 lines (1301 loc) · 47.7 KB
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
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python2
from __future__ import print_function
"""
sh_spec.py -- Test framework to compare shells.
Assertion help:
stdout: A single line of expected stdout. Newline is implicit.
stdout-json: JSON-encoded string. Use for the empty string (no newline),
for unicode chars, etc.
stderr: Ditto for stderr stream.
status: Expected shell return code. If not specified, the case must exit 0.
Results:
PASS - we got the ideal, expected value
OK - we got a value that was not ideal, but expected
For OSH this is behavior that was defined to be different?
N-I - Not implemented (e.g. $''). Assertions still checked (in case it
starts working)
BUG - we verified the value of a known bug
FAIL - we got an unexpected value. If the implementation can't be changed,
it should be converted to BUG or OK. Otherwise it should be made to
PASS.
NOTE: The difference between OK and BUG is a matter of judgement. If the ideal
behavior is a compile time error (code 2), a runtime error is generally OK.
If ALL shells agree on a broken behavior, they are all marked OK (but our
implementation will be PASS.) But if the behavior is NOT POSIX compliant, then
it will be a BUG.
If one shell disagrees with others, that is generally a BUG.
Example test case:
#### hello and fail
echo hello
echo world
exit 1
## status: 1
#
# ignored comment
#
## STDOUT:
hello
world
## END
"""
import collections
import cgi
import cStringIO
import errno
import json
import optparse
import os
import pprint
import re
import shutil
import signal
import subprocess
import sys
from test import spec_lib
from doctools import html_head
log = spec_lib.log
# Magic strings for other variants of OSH.
# NOTE: osh_ALT is usually _bin/osh -- the release binary.
# It would be better to rename these osh-cpython and osh-ovm. Have the concept
# of a suffix?
OSH_CPYTHON = ('osh', 'osh-dbg')
OTHER_OSH = ('osh_ALT', )
YSH_CPYTHON = ('ysh', 'ysh-dbg')
OTHER_YSH = ('oil_ALT', )
# For now, only count the Oils CPython failures. TODO: the spec-cpp job should
# assert the osh-cpp and ysh-cpp deltas.
OTHER_OILS = OTHER_OSH + OTHER_YSH + ('osh-cpp', 'ysh-cpp')
class ParseError(Exception):
pass
# EXAMPLES:
## stdout: foo
## stdout-json: ""
#
# In other words, it could be (name, value) or (qualifier, name, value)
KEY_VALUE_RE = re.compile(
r'''
[#][#] \s+
# optional prefix with qualifier and shells
(?: (OK(?:-\d)? | BUG(?:-\d)? | N-I) \s+ ([\w+/]+) \s+ )?
([\w\-]+) # key
:
\s* (.*) # value
''', re.VERBOSE)
END_MULTILINE_RE = re.compile(r'''
[#][#] \s+ END
''', re.VERBOSE)
# Line types
TEST_CASE_BEGIN = 0 # Starts with ####
KEY_VALUE = 1 # Metadata
KEY_VALUE_MULTILINE = 2 # STDOUT STDERR
END_MULTILINE = 3 # STDOUT STDERR
PLAIN_LINE = 4 # Uncommented
EOF = 5
LEX_OUTER = 0 # Ignore blank lines, e.g. for separating cases
LEX_RAW = 1 # Blank lines are significant
class Tokenizer(object):
"""Modal lexer!"""
def __init__(self, f):
self.f = f
self.cursor = None
self.line_num = 0
self.next()
def _ClassifyLine(self, line, lex_mode):
if not line: # empty
return self.line_num, EOF, ''
if lex_mode == LEX_OUTER and not line.strip():
return None
if line.startswith('####'):
desc = line[4:].strip()
return self.line_num, TEST_CASE_BEGIN, desc
m = KEY_VALUE_RE.match(line)
if m:
qualifier, shells, name, value = m.groups()
# HACK: Expected data should have the newline.
if name in ('stdout', 'stderr'):
value += '\n'
if name in ('STDOUT', 'STDERR'):
token_type = KEY_VALUE_MULTILINE
else:
token_type = KEY_VALUE
return self.line_num, token_type, (qualifier, shells, name, value)
m = END_MULTILINE_RE.match(line)
if m:
return self.line_num, END_MULTILINE, None
# If it starts with ##, it should be metadata. This finds some typos.
if line.startswith('##'):
raise RuntimeError('Invalid ## line %r' % line)
# TODO: comments should not be ignored in code or STDOUT blocks!
if line.lstrip().startswith('#'): # Ignore comments
return None # try again
# Non-empty line that doesn't start with '#'
# NOTE: We need the original line to test the whitespace sensitive <<-.
# And we need rstrip because we add newlines back below.
return self.line_num, PLAIN_LINE, line
def next(self, lex_mode=LEX_OUTER):
"""Raises StopIteration when exhausted."""
while True:
line = self.f.readline()
self.line_num += 1
tok = self._ClassifyLine(line, lex_mode)
if tok is not None:
break
self.cursor = tok
return self.cursor
def peek(self):
return self.cursor
def AddMetadataToCase(case, qualifier, shells, name, value, line_num):
shells = shells.split('/') # bash/dash/mksh
for shell in shells:
if shell not in case:
case[shell] = {}
# Check a duplicate specification
name_without_type = re.sub(r'-json$', '', name)
if (name_without_type in case[shell] or
name_without_type + '-json' in case[shell]):
raise ParseError('Line %d: duplicate spec %r for %r' %
(line_num, name, shell))
# Check inconsistent qualifier
if 'qualifier' in case[shell] and qualifier != case[shell]['qualifier']:
raise ParseError(
'Line %d: inconsistent qualifier %r is specified for %r, '
'but %r was previously specified. ' %
(line_num, qualifier, shell, case[shell]['qualifier']))
case[shell][name] = value
case[shell]['qualifier'] = qualifier
# Format of a test script.
#
# -- Code is either literal lines, or a commented out code: value.
# code = PLAIN_LINE*
# | '## code:' VALUE
#
# -- Key value pairs can be single- or multi-line
# key_value = '##' KEY ':' VALUE
# | KEY_VALUE_MULTILINE PLAIN_LINE* END_MULTILINE
#
# -- Description, then key-value pairs surrounding code.
# test_case = '####' DESC
# key_value*
# code
# key_value*
#
# -- Should be a blank line after each test case. Leading comments and code
# -- are OK.
#
# test_file =
# key_value* -- file level metadata
# (test_case '\n')*
def ParseKeyValue(tokens, case):
"""Parse commented-out metadata in a test case.
The metadata must be contiguous.
Args:
tokens: Tokenizer
case: dictionary to add to
"""
while True:
line_num, kind, item = tokens.peek()
if kind == KEY_VALUE_MULTILINE:
qualifier, shells, name, empty_value = item
if empty_value:
raise ParseError(
'Line %d: got value %r for %r, but the value should be on the '
'following lines' % (line_num, empty_value, name))
value_lines = []
while True:
tokens.next(lex_mode=LEX_RAW) # empty lines aren't skipped
_, kind2, item2 = tokens.peek()
if kind2 != PLAIN_LINE:
break
value_lines.append(item2)
value = ''.join(value_lines)
name = name.lower() # STDOUT -> stdout
if qualifier:
AddMetadataToCase(case, qualifier, shells, name, value,
line_num)
else:
case[name] = value
# END token is optional.
if kind2 == END_MULTILINE:
tokens.next()
elif kind == KEY_VALUE:
qualifier, shells, name, value = item
if qualifier:
AddMetadataToCase(case, qualifier, shells, name, value,
line_num)
else:
case[name] = value
tokens.next()
else: # Unknown token type
break
def ParseCodeLines(tokens, case):
"""Parse uncommented code in a test case."""
_, kind, item = tokens.peek()
if kind != PLAIN_LINE:
raise ParseError('Expected a line of code (got %r, %r)' % (kind, item))
code_lines = []
while True:
_, kind, item = tokens.peek()
if kind != PLAIN_LINE:
case['code'] = ''.join(code_lines)
return
code_lines.append(item)
tokens.next(lex_mode=LEX_RAW)
def ParseTestCase(tokens):
"""Parse a single test case and return it.
If at EOF, return None.
"""
line_num, kind, item = tokens.peek()
if kind == EOF:
return None
if kind != TEST_CASE_BEGIN:
raise RuntimeError("line %d: Expected TEST_CASE_BEGIN, got %r" %
(line_num, [kind, item]))
tokens.next()
case = {'desc': item, 'line_num': line_num}
ParseKeyValue(tokens, case)
# For broken code
if 'code' in case: # Got it through a key value pair
return case
ParseCodeLines(tokens, case)
ParseKeyValue(tokens, case)
return case
_META_FIELDS = [
'our_shell',
'compare_shells',
'suite',
'tags',
'oils_failures_allowed',
'oils_cpp_failures_allowed',
'legacy_tmp_dir',
]
def ParseTestFile(test_file, tokens):
"""
test_file: Only for error message
"""
file_metadata = {}
test_cases = []
try:
# Skip over the header. Setup code can go here, although would we have to
# execute it on every case?
while True:
line_num, kind, item = tokens.peek()
if kind != KEY_VALUE:
break
qualifier, shells, name, value = item
if qualifier is not None:
raise RuntimeError('Invalid qualifier in spec file metadata')
if shells is not None:
raise RuntimeError('Invalid shells in spec file metadata')
file_metadata[name] = value
tokens.next()
while True: # Loop over cases
test_case = ParseTestCase(tokens)
if test_case is None:
break
test_cases.append(test_case)
except StopIteration:
raise RuntimeError('Unexpected EOF parsing test cases')
for name in file_metadata:
if name not in _META_FIELDS:
raise RuntimeError('Invalid file metadata %r in %r' %
(name, test_file))
return file_metadata, test_cases
def CreateStringAssertion(d, key, assertions, qualifier=False):
found = False
exp = d.get(key)
if exp is not None:
a = EqualAssertion(key, exp, qualifier=qualifier)
assertions.append(a)
found = True
exp_json = d.get(key + '-json')
if exp_json is not None:
exp = json.loads(exp_json, encoding='utf-8')
a = EqualAssertion(key, exp, qualifier=qualifier)
assertions.append(a)
found = True
return found
def CreateIntAssertion(d, key, assertions, qualifier=False):
exp = d.get(key) # expected
if exp is not None:
# For now, turn it into int
a = EqualAssertion(key, int(exp), qualifier=qualifier)
assertions.append(a)
return True
return False
def CreateAssertions(case, sh_label):
"""Given a raw test case and a shell label, create EqualAssertion instances
to run."""
assertions = []
# Whether we found assertions
stdout = False
stderr = False
status = False
# So the assertion are exactly the same for osh and osh_ALT
if sh_label.startswith('osh'):
case_sh = 'osh'
elif sh_label.startswith('bash'):
case_sh = 'bash'
else:
case_sh = sh_label
if case_sh in case:
q = case[case_sh]['qualifier']
if CreateStringAssertion(case[case_sh],
'stdout',
assertions,
qualifier=q):
stdout = True
if CreateStringAssertion(case[case_sh],
'stderr',
assertions,
qualifier=q):
stderr = True
if CreateIntAssertion(case[case_sh], 'status', assertions,
qualifier=q):
status = True
if not stdout:
CreateStringAssertion(case, 'stdout', assertions)
if not stderr:
CreateStringAssertion(case, 'stderr', assertions)
if not status:
if 'status' in case:
CreateIntAssertion(case, 'status', assertions)
else:
# If the user didn't specify a 'status' assertion, assert that the exit
# code is 0.
a = EqualAssertion('status', 0)
assertions.append(a)
no_traceback = SubstringAssertion('stderr', 'Traceback (most recent')
assertions.append(no_traceback)
#print 'SHELL', shell
#pprint.pprint(case)
#print(assertions)
return assertions
class Result(object):
"""Result of an stdout/stderr/status assertion or of a (case, shell) cell.
Order is important: the result of a cell is the minimum of the results of
each assertion.
"""
TIMEOUT = 0 # ONLY a cell result, not an assertion result
FAIL = 1
BUG = 2
BUG_2 = 3
NI = 4
OK = 5
OK_2 = 6
OK_3 = 7
OK_4 = 8
PASS = 9
length = 10 # for loops
def QualifierToResult(qualifier):
# type: (str) -> Result
if qualifier == 'BUG': # equal, but known bad
return Result.BUG
if qualifier == 'BUG-2':
return Result.BUG_2
if qualifier == 'N-I': # equal, and known UNIMPLEMENTED
return Result.NI
if qualifier == 'OK': # equal, but ok (not ideal)
return Result.OK
if qualifier == 'OK-2':
return Result.OK_2
if qualifier == 'OK-3':
return Result.OK_3
if qualifier == 'OK-4':
return Result.OK_4
return Result.PASS # ideal behavior
class EqualAssertion(object):
"""Check that two values are equal."""
def __init__(self, key, expected, qualifier=None):
self.key = key
self.expected = expected # expected value
self.qualifier = qualifier # whether this was a special case?
def __repr__(self):
return '<EqualAssertion %s == %r>' % (self.key, self.expected)
def Check(self, shell, record):
actual = record[self.key]
if actual != self.expected:
msg = '''
[%s %s]
Expected %r
Got %r
''' % (shell, self.key, self.expected, actual)
# TODO: Make this better and add a flag for it.
if 0:
import difflib
for line in difflib.unified_diff(self.expected,
actual,
fromfile='expected',
tofile='actual'):
print(repr(line))
return Result.FAIL, msg
return QualifierToResult(self.qualifier), ''
class SubstringAssertion(object):
"""Check that a string like stderr doesn't have a substring."""
def __init__(self, key, substring):
self.key = key
self.substring = substring
def __repr__(self):
return '<SubstringAssertion %s == %r>' % (self.key, self.substring)
def Check(self, shell, record):
actual = record[self.key]
if self.substring in actual:
msg = '[%s %s] Found %r' % (shell, self.key, self.substring)
return Result.FAIL, msg
return Result.PASS, ''
class Stats(object):
def __init__(self, num_cases, sh_labels):
self.counters = collections.defaultdict(int)
c = self.counters
c['num_cases'] = num_cases
c['oils_num_passed'] = 0
c['oils_num_failed'] = 0
c['oils_cpp_num_failed'] = 0
# Number of osh_ALT results that differed from osh.
c['oils_ALT_delta'] = 0
self.by_shell = {}
for sh in sh_labels:
self.by_shell[sh] = collections.defaultdict(int)
self.nonzero_results = collections.defaultdict(int)
self.tsv_rows = []
def Inc(self, counter_name):
self.counters[counter_name] += 1
def Get(self, counter_name):
return self.counters[counter_name]
def Set(self, counter_name, val):
self.counters[counter_name] = val
def ReportCell(self, case_num, cell_result, sh_label):
self.tsv_rows.append(
(str(case_num), sh_label, TEXT_CELLS[cell_result]))
self.by_shell[sh_label][cell_result] += 1
self.nonzero_results[cell_result] += 1
c = self.counters
if cell_result == Result.TIMEOUT:
c['num_timeout'] += 1
elif cell_result == Result.FAIL:
# Special logic: don't count osh_ALT because its failures will be
# counted in the delta.
if sh_label not in OTHER_OILS:
c['num_failed'] += 1
if sh_label in OSH_CPYTHON + YSH_CPYTHON:
c['oils_num_failed'] += 1
if sh_label in ('osh-cpp', 'ysh-cpp'):
c['oils_cpp_num_failed'] += 1
elif cell_result in (Result.BUG, Result.BUG_2):
c['num_bug'] += 1
elif cell_result == Result.NI:
c['num_ni'] += 1
elif cell_result in (Result.OK, Result.OK_2, Result.OK_3, Result.OK_4):
c['num_ok'] += 1
elif cell_result == Result.PASS:
c['num_passed'] += 1
if sh_label in OSH_CPYTHON + YSH_CPYTHON:
c['oils_num_passed'] += 1
else:
raise AssertionError()
def WriteTsv(self, f):
f.write('case\tshell\tresult\n')
for row in self.tsv_rows:
f.write('\t'.join(row))
f.write('\n')
PIPE = subprocess.PIPE
def _TimedOut(status):
# timeout 1s sleep 5 ==> status 124
# timeout -s KILL 1s sleep 5 ==> status 137
# the latter is more robust
#
# SIGKILL == -9
#return status in (124, 137)
return status == -9
def _PrepareCaseTempDir(case_tmp_dir, legacy_tmp_dir=False):
# Clean up after the previous run. (The previous run doesn't clean
# up after itself, because it's useful to manually inspect the
# state.)
try:
shutil.rmtree(case_tmp_dir)
except OSError as e:
#log('Error cleaning up %r: %s', case_tmp_dir, e)
pass
try:
os.makedirs(case_tmp_dir)
except OSError as e:
if e.errno != errno.EEXIST:
raise
# Some tests assume _tmp exists
if legacy_tmp_dir:
try:
os.mkdir(os.path.join(case_tmp_dir, '_tmp'))
except OSError as e:
if e.errno != errno.EEXIST:
raise
class ActualOutput(object):
def __init__(self, f):
self.f = f
def _Write(self, msg, *args):
if not self.f:
return
if args:
msg = msg % args
self.f.write(msg)
self.f.write('\n')
def WriteActualRow(self, record):
"""
"""
# Check for different stdout:
unique = {}
for a in record['actual']:
# Don't count stdout for features not implemented
if a['cell_result'] == Result.NI:
continue
# look for existing shells
if a['sh_label'] == 'osh':
continue
stdout = a['stdout']
if stdout not in unique:
unique[stdout] = []
unique[stdout].append(a['sh_label'])
# 4 is more interesting than 3
if len(unique) >= 4:
self._Write('---')
self._Write('TEST CASE %d' % record['case_num'])
self._Write('DESC %s' % record['desc'])
self._Write('LINE %d' % record['line_num'])
self._Write('NUM UNIQUE %d' % len(unique))
self._Write('')
i = 0
for stdout, shells in unique.iteritems():
self._Write('UNIQUE STDOUT %d', (i + 1))
self._Write('SHELLS: %s', ' '.join(shells))
shells = unique[stdout]
self._Write('')
self._Write(stdout)
self._Write('')
i += 1
#json.dump(record, sys.stderr, indent=2)
def ResetSignals():
"""
Added for spec/builtin-trap.sh
Python 3 doesn't have this issue, it has restore_signals=True
https://docs.python.org/3/library/subprocess.html
But Python 2 does
"""
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
signal.signal(signal.SIGXFSZ, signal.SIG_DFL)
def RunCases(cases,
case_predicate,
shells,
env,
out,
actual_out,
opts,
legacy_tmp_dir=False):
"""
Run a list of test 'cases' for all 'shells' and write output to 'out'.
"""
if opts.trace:
for _, sh in shells:
log('\tshell: %s', sh)
print('\twhich $SH: ', end='', file=sys.stderr)
subprocess.call(['which', sh])
#pprint.pprint(cases)
if (isinstance(case_predicate, spec_lib.RangePredicate) and
(case_predicate.begin > (len(cases) - 1))):
raise RuntimeError(
"valid case indexes are from 0 to %s. given range: %s-%s" %
((len(cases) - 1), case_predicate.begin, case_predicate.end))
sh_labels = [sh_label for sh_label, _ in shells]
out.WriteHeader(sh_labels)
stats = Stats(len(cases), sh_labels)
# Make an environment for each shell. $SH is the path to the shell, so we
# can test flags, etc.
sh_env = []
for _, sh_path in shells:
e = dict(env)
e[opts.sh_env_var_name] = sh_path
sh_env.append(e)
# Determine which one (if any) is osh-cpython, for comparison against other
# shells.
osh_cpython_index = -1
for i, (sh_label, _) in enumerate(shells):
if sh_label in OSH_CPYTHON:
osh_cpython_index = i
break
timeout_dir = os.path.abspath('_tmp/spec/timeouts')
try:
shutil.rmtree(timeout_dir)
os.mkdir(timeout_dir)
except OSError:
pass
# Now run each case, and print a table.
for i, case in enumerate(cases):
case['case_num'] = i # for reporting
line_num = case['line_num']
desc = case['desc']
code = case['code']
if opts.trace:
log('case %d: %s', i, desc)
if not case_predicate(i, case):
stats.Inc('num_skipped')
continue
if opts.do_print:
print('#### %s' % case['desc'])
print(case['code'])
print()
continue
stats.Inc('num_cases_run')
result_row = []
case_actual = dict(case)
case_actual['actual'] = []
for shell_index, (sh_label, sh_path) in enumerate(shells):
timeout_file = os.path.join(timeout_dir, '%02d-%s' % (i, sh_label))
if opts.timeout:
if opts.timeout_bin:
# This is what smoosh itself uses. See smoosh/tests/shell_tests.sh
# QUIRK: interval can only be a whole number
argv = [
opts.timeout_bin,
'-t',
opts.timeout,
# Somehow I'm not able to get this timeout file working? I think
# it has a bug when using stdin. It waits for the background
# process too.
#'-i', '1',
#'-l', timeout_file
]
else:
# This kills hanging tests properly, but somehow they fail with code
# -9?
argv = ['timeout', '-s', 'KILL', opts.timeout + 's']
# s suffix for seconds
#argv = ['timeout', opts.timeout + 's']
else:
argv = []
argv.append(sh_path)
# dash doesn't support -o posix
if opts.posix and sh_label != 'dash':
argv.extend(['-o', 'posix'])
if opts.trace:
log('\targv: %s', ' '.join(argv))
case_env = sh_env[shell_index]
# Unique dir for every test case and shell
tmp_base = os.path.normpath(opts.tmp_env) # no . or ..
case_tmp_dir = os.path.join(tmp_base, '%02d-%s' % (i, sh_label))
_PrepareCaseTempDir(case_tmp_dir, legacy_tmp_dir=legacy_tmp_dir)
case_env['TMP'] = case_tmp_dir
if opts.pyann_out_dir:
case_env = dict(case_env)
case_env['PYANN_OUT'] = os.path.join(opts.pyann_out_dir,
'%d.json' % i)
try:
p = subprocess.Popen(argv,
env=case_env,
cwd=case_tmp_dir,
stdin=PIPE,
stdout=PIPE,
stderr=PIPE,
preexec_fn=ResetSignals)
except OSError as e:
print('Error running %r: %s' % (sh_path, e), file=sys.stderr)
sys.exit(1)
p.stdin.write(code)
actual = {'sh_label': sh_label}
actual['stdout'], actual['stderr'] = p.communicate()
actual['status'] = p.wait()
case_actual['actual'].append(actual)
if opts.timeout_bin and os.path.exists(timeout_file):
cell_result = Result.TIMEOUT
elif not opts.timeout_bin and _TimedOut(actual['status']):
cell_result = Result.TIMEOUT
else:
messages = []
cell_result = Result.PASS
# TODO: Warn about no assertions? Well it will always test the error
# code.
assertions = CreateAssertions(case, sh_label)
for a in assertions:
result, msg = a.Check(sh_label, actual)
# The minimum one wins.
# If any failed, then the result is FAIL.
# If any are OK, but none are FAIL, the result is OK.
cell_result = min(cell_result, result)
if msg:
messages.append(msg)
if cell_result != Result.PASS or opts.details:
d = (i, sh_label, actual['stdout'], actual['stderr'],
messages)
out.AddDetails(d)
result_row.append(cell_result)
actual['cell_result'] = cell_result
stats.ReportCell(i, cell_result, sh_label)
if sh_label in OTHER_OSH:
# This is only an error if we tried to run ANY OSH.
if osh_cpython_index == -1:
raise RuntimeError(
"Couldn't determine index of osh-cpython")
other_result = result_row[shell_index]
cpython_result = result_row[osh_cpython_index]
if other_result != cpython_result:
stats.Inc('oils_ALT_delta')
out.WriteRow(i, line_num, result_row, desc)
actual_out.WriteActualRow(case_actual)
return stats
# ANSI color constants
_RESET = '\033[0;0m'
_BOLD = '\033[1m'
_RED = '\033[31m'
_GREEN = '\033[32m'
_YELLOW = '\033[33m'
_BLUE = '\033[34m'
_PURPLE = '\033[35m'
_CYAN = '\033[36m'
#_WHITE = '\033[37m'
TEXT_CELLS = {
Result.TIMEOUT: 'TIME',
Result.FAIL: 'FAIL',
Result.BUG: 'BUG',
Result.BUG_2: 'BUG-2',
Result.NI: 'N-I',
Result.OK: 'ok',
Result.OK_2: 'ok-2',
Result.OK_3: 'ok-3',
Result.OK_4: 'ok-4',
Result.PASS: 'pass',
}
ANSI_COLORS = {
Result.TIMEOUT: _PURPLE,
Result.FAIL: _RED,
Result.BUG: _YELLOW,
Result.BUG_2: _BLUE,
Result.NI: _YELLOW,
Result.OK: _YELLOW,
Result.OK_2: _BLUE,
Result.OK_3: _CYAN,
Result.OK_4: _PURPLE,
Result.PASS: _GREEN,
}
def _AnsiCells():
lookup = {}
for i in xrange(Result.length):
lookup[i] = ''.join([ANSI_COLORS[i], _BOLD, TEXT_CELLS[i], _RESET])
return lookup
ANSI_CELLS = _AnsiCells()
HTML_CELLS = {
Result.TIMEOUT: '<td class="timeout">TIME',
Result.FAIL: '<td class="fail">FAIL',
Result.BUG: '<td class="bug">BUG',
Result.BUG_2: '<td class="bug-2">BUG-2',
Result.NI: '<td class="n-i">N-I',
Result.OK: '<td class="ok">ok',