10 from collections
import namedtuple
11 from concurrent
.futures
import ThreadPoolExecutor
15 dump_on_failure
= False
18 Failure
= namedtuple('Failure', ['fname', 'expected', 'output'])
21 Per-test flags passed to test executable. Expected to be in a file with
22 same name as test, but with .flags extension.
25 def get_test_flags(f
):
26 prefix
, _ext
= os
.path
.splitext(f
)
27 path
= prefix
+ '.flags'
29 if not os
.path
.isfile(path
):
32 return shlex
.split(f
.read().strip())
35 def run_test_program(files
, program
, expect_ext
, get_flags
, use_stdin
):
37 Run the program and return a list of Failures.
40 test_dir
, test_name
= os
.path
.split(f
)
41 flags
= get_flags(test_dir
)
42 test_flags
= get_test_flags(f
)
46 cmd
+= flags
+ test_flags
48 print('Executing', ' '.join(cmd
))
51 return subprocess
.check_output(
52 cmd
, stderr
=subprocess
.STDOUT
, cwd
=test_dir
,
53 universal_newlines
=True, stdin
=stdin
)
55 with
open(f
) as stdin
:
59 except subprocess
.CalledProcessError
as e
:
60 # we don't care about nonzero exit codes... for instance, type
61 # errors cause hh_single_type_check to produce them
63 return check_result(f
, expect_ext
, output
)
65 executor
= ThreadPoolExecutor(max_workers
=max_workers
)
66 futures
= [executor
.submit(run
, f
) for f
in files
]
68 results
= [f
.result() for f
in futures
]
69 return [r
for r
in results
if r
is not None]
71 def check_result(fname
, expect_exp
, out
):
73 with
open(fname
+ expect_exp
, 'rt') as fexp
:
75 except FileNotFoundError
:
78 return Failure(fname
=fname
, expected
=exp
, output
=out
)
80 def record_failures(failures
, out_ext
):
81 for failure
in failures
:
82 outfile
= failure
.fname
+ out_ext
83 with
open(outfile
, 'wb') as f
:
84 f
.write(bytes(failure
.output
, 'UTF-8'))
86 def dump_failures(failures
):
91 expected
.splitlines(1),
93 print("Details for the failed test %s:" % f
.fname
)
94 print("\n>>>>> Expected output >>>>>>\n")
96 print("\n===== Actual output ======\n")
98 print("\n<<<<< End Actual output <<<<<<<\n")
99 print("\n>>>>> Diff >>>>>>>\n")
101 print("\n<<<<< End Diff <<<<<<<\n")
103 def get_hh_flags(test_dir
):
104 path
= os
.path
.join(test_dir
, 'HH_FLAGS')
105 if not os
.path
.isfile(path
):
107 print("No HH_FLAGS file found")
109 with
open(path
) as f
:
110 return shlex
.split(f
.read().strip())
112 def files_with_ext(files
, ext
):
114 Returns the set of filenames in :files that end in :ext
118 prefix
, suffix
= os
.path
.splitext(f
)
123 def list_test_files(root
, disabled_ext
, test_ext
):
124 if os
.path
.isfile(root
):
125 if root
.endswith(test_ext
):
129 elif os
.path
.isdir(root
):
131 children
= os
.listdir(root
)
132 disabled
= files_with_ext(children
, disabled_ext
)
133 for child
in children
:
134 if child
!= 'disabled' and child
not in disabled
:
137 os
.path
.join(root
, child
),
141 elif os
.path
.islink(root
):
142 # Some editors create broken symlinks as part of their locking scheme,
146 raise Exception('Could not find test file or directory at %s' %
149 if __name__
== '__main__':
150 parser
= argparse
.ArgumentParser()
153 help='A file or a directory. ')
154 parser
.add_argument('--program', type=os
.path
.abspath
)
155 parser
.add_argument('--out-extension', type=str, default
='.out')
156 parser
.add_argument('--expect-extension', type=str, default
='.exp')
157 parser
.add_argument('--in-extension', type=str, default
='.php')
158 parser
.add_argument('--disabled-extension', type=str,
159 default
='.no_typecheck')
160 parser
.add_argument('--verbose', action
='store_true')
161 parser
.add_argument('--max-workers', type=int, default
='48')
162 parser
.add_argument('--diff', action
='store_true',
163 help='On test failure, show the content of the files and a diff')
164 parser
.add_argument('--flags', nargs
=argparse
.REMAINDER
)
165 parser
.add_argument('--stdin', action
='store_true',
166 help='Pass test input file via stdin')
167 parser
.epilog
= "Unless --flags is passed as an argument, "\
168 "%s looks for a file named HH_FLAGS in the same directory" \
169 " as the test files it is executing. If found, the " \
170 "contents will be passed as arguments to " \
171 "<program>." % parser
.prog
172 args
= parser
.parse_args()
174 max_workers
= args
.max_workers
175 verbose
= args
.verbose
176 dump_on_failure
= args
.diff
178 if not os
.path
.isfile(args
.program
):
179 raise Exception('Could not find program at %s' % args
.program
)
181 files
= list_test_files(
183 args
.disabled_extension
,
188 'Could not find any files to test in ' + args
.test_path
)
192 def get_flags(test_dir
):
193 if args
.flags
is not None:
196 if test_dir
not in flags_cache
:
197 flags_cache
[test_dir
] = get_hh_flags(test_dir
)
198 return flags_cache
[test_dir
]
200 failures
= run_test_program(
201 files
, args
.program
, args
.expect_extension
, get_flags
, args
.stdin
)
204 print("All %d tests passed!\n" % total
)
206 record_failures(failures
, args
.out_extension
)
207 fnames
= [failure
.fname
for failure
in failures
]
208 print("To review the failures, use the following command: ")
209 print("OUT_EXT=%s EXP_EXT=%s ./hphp/hack/test/review.sh %s" %
210 (args
.out_extension
, args
.expect_extension
, " ".join(fnames
)))
212 dump_failures(failures
)
213 print("Failed %d out of %d tests." % (len(failures
), total
))