12 from concurrent
.futures
import ThreadPoolExecutor
13 from dataclasses
import dataclass
15 from typing
import Callable
, Dict
, List
, Optional
, Tuple
17 from hphp
.hack
.test
.parse_errors
import Error
, parse_errors
, sprint_errors
19 DEFAULT_OUT_EXT
= ".out"
20 DEFAULT_EXP_EXT
= ".exp"
22 flags_pessimise_unsupported
= [
24 "--enable-higher-kinded-types",
25 "--enable-global-access-check",
29 dump_on_failure
= False
47 class VerifyPessimisationOptions(Enum
):
54 def __str__(self
) -> str:
59 Per-test flags passed to test executable. Expected to be in a file with
60 same name as test, but with .flags extension.
64 def compare_errors_by_line_no(
65 errors_exp
: List
[Error
], errors_out
: List
[Error
]
66 ) -> Tuple
[List
[Error
], List
[Error
]]:
69 len_out
= len(errors_out
)
70 len_exp
= len(errors_exp
)
71 errors_in_out_not_in_exp
= []
72 errors_in_exp_not_in_out
= []
73 while i_out
< len_out
and i_exp
< len_exp
:
74 err_out
= errors_out
[i_out
]
75 err_exp
= errors_exp
[i_exp
]
76 l_out
= err_out
.message
.position
.line
77 l_exp
= err_exp
.message
.position
.line
79 errors_in_out_not_in_exp
.append(err_out
)
82 errors_in_exp_not_in_out
.append(err_exp
)
88 for i
in range(i_exp
, len_exp
):
89 errors_in_exp_not_in_out
.append(errors_exp
[i
])
90 elif i_exp
>= len_exp
:
91 for i
in range(i_out
, len_out
):
92 errors_in_out_not_in_exp
.append(errors_out
[i
])
93 return (errors_in_exp_not_in_out
, errors_in_out_not_in_exp
)
96 def compare_output_files_error_lines_only(
97 file_out
: str, file_exp
: str
98 ) -> Tuple
[bool, str]:
102 errors_out
= parse_errors(file_out
)
103 errors_exp
= parse_errors(file_exp
)
105 errors_in_exp_not_in_out
,
106 errors_in_out_not_in_exp
,
107 ) = compare_errors_by_line_no(errors_out
=errors_out
, errors_exp
=errors_exp
)
109 failed
= bool(errors_in_exp_not_in_out
) or bool(errors_in_out_not_in_exp
)
110 if errors_in_exp_not_in_out
:
111 out
+= f
"""\033[93mExpected errors which were not produced:\033[0m
112 {sprint_errors(errors_in_exp_not_in_out)}
114 if errors_in_out_not_in_exp
:
115 out
+= f
"""\033[93mProduced errors which were not expected:\033[0m
116 {sprint_errors(errors_in_out_not_in_exp)}
119 out
= f
"Warning: {e}"
123 def check_output_error_lines_only(
124 test
: str, out_ext
: str = DEFAULT_OUT_EXT
, exp_ext
: str = DEFAULT_EXP_EXT
125 ) -> Tuple
[bool, str]:
126 file_out
= test
+ out_ext
127 file_exp
= test
+ exp_ext
128 return compare_output_files_error_lines_only(file_out
=file_out
, file_exp
=file_exp
)
131 def get_test_flags(path
: str) -> List
[str]:
132 prefix
, _ext
= os
.path
.splitext(path
)
133 path
= prefix
+ ".flags"
135 if not os
.path
.isfile(path
):
137 with
open(path
) as file:
138 return shlex
.split(file.read().strip())
144 default_expect_regex
: Optional
[str],
145 ignore_error_text
: bool,
146 only_compare_error_lines
: bool,
147 normalize_paths
: bool,
148 verify_pessimisation
: VerifyPessimisationOptions
,
149 check_expected_included_in_actual
: bool,
151 if only_compare_error_lines
:
152 (failed
, out
) = check_output_error_lines_only(case
.file_path
)
153 return Result(test_case
=case
, output
=out
, is_failure
=failed
)
156 case
.file_path
+ out_extension
157 if verify_pessimisation
== VerifyPessimisationOptions
.no
158 or verify_pessimisation
== VerifyPessimisationOptions
.full
159 or out_extension
== ".pess.out"
160 else case
.file_path
+ ".pess" + out_extension
163 with
open(out_path
, "r") as f
:
164 output
: str = f
.read()
165 except FileNotFoundError
:
166 out_path
= os
.path
.realpath(out_path
)
167 output
= "Output file " + out_path
+ " was not found!"
170 default_expect_regex
,
173 verify_pessimisation
,
174 check_expected_included_in_actual
=check_expected_included_in_actual
,
179 def debug_cmd(cwd
: str, cmd
: List
[str]) -> None:
181 print("From directory", os
.path
.realpath(cwd
))
182 print("Executing", " ".join(cmd
))
187 test_cases
: List
[TestCase
],
189 default_expect_regex
: Optional
[str],
190 ignore_error_text
: bool,
193 mode_flag
: List
[str],
194 get_flags
: Callable
[[str], List
[str]],
196 normalize_paths
: bool,
197 verify_pessimisation
: VerifyPessimisationOptions
,
198 check_expected_included_in_actual
: bool,
199 only_compare_error_lines
: bool = False,
202 Run the program with batches of files and return a list of results.
204 # Each directory needs to be in a separate batch because flags are different
205 # for each directory.
206 # Compile a list of directories to test cases, and then
207 dirs_to_files
: Dict
[str, List
[TestCase
]] = {}
208 for case
in test_cases
:
209 test_dir
= os
.path
.dirname(case
.file_path
)
210 dirs_to_files
.setdefault(test_dir
, []).append(case
)
212 # run a list of test cases.
213 # The contract here is that the program will write to
214 # filename.out_extension for each file, and we read that
216 # Remark: if verify_pessimisation is set, then the result
217 # is in filename.pess.out_extension.
218 def run(test_cases
: List
[TestCase
]) -> List
[Result
]:
220 raise AssertionError()
221 first_test
= test_cases
[0]
222 test_dir
= os
.path
.dirname(first_test
.file_path
)
223 flags
= get_flags(test_dir
)
224 test_flags
= get_test_flags(first_test
.file_path
)
225 if verify_pessimisation
!= VerifyPessimisationOptions
.no
:
226 path
= os
.path
.join(test_dir
, "NO_PESS")
227 if os
.path
.isfile(path
):
229 for flag
in flags_pessimise_unsupported
:
234 cmd
+= ["--batch-files", "--out-extension", out_extension
]
235 cmd
+= flags
+ test_flags
236 cmd
+= [os
.path
.basename(case
.file_path
) for case
in test_cases
]
237 debug_cmd(test_dir
, cmd
)
238 env
= os
.environ
.copy()
239 env
["FORCE_ERROR_COLOR"] = "true" if force_color
else "false"
241 return_code
= subprocess
.call(
243 stderr
=None if no_stderr
else subprocess
.STDOUT
,
245 universal_newlines
=True,
248 except subprocess
.CalledProcessError
:
249 # we don't care about nonzero exit codes... for instance, type
250 # errors cause hh_single_type_check to produce them
252 if return_code
== -11:
254 "Segmentation fault while running the following command "
256 + os
.path
.realpath(test_dir
)
261 for case
in test_cases
:
262 result
= check_output(
264 out_extension
=out_extension
,
265 default_expect_regex
=default_expect_regex
,
266 ignore_error_text
=ignore_error_text
,
267 only_compare_error_lines
=only_compare_error_lines
,
268 normalize_paths
=normalize_paths
,
269 verify_pessimisation
=verify_pessimisation
,
270 check_expected_included_in_actual
=check_expected_included_in_actual
,
272 results
.append(result
)
275 # Create a list of batched cases.
276 all_batched_cases
: List
[List
[TestCase
]] = []
278 # For each directory, we split all the test cases
279 # into chunks of batch_size. Then each of these lists
280 # is a separate job for each thread in the threadpool.
281 for cases
in dirs_to_files
.values():
282 batched_cases
: List
[List
[TestCase
]] = [
283 cases
[i
: i
+ batch_size
] for i
in range(0, len(cases
), batch_size
)
285 all_batched_cases
+= batched_cases
287 executor
= ThreadPoolExecutor(max_workers
=max_workers
)
288 futures
= [executor
.submit(run
, test_batch
) for test_batch
in all_batched_cases
]
290 results
= [future
.result() for future
in futures
]
292 return [item
for sublist
in results
for item
in sublist
]
295 def run_test_program(
296 test_cases
: List
[TestCase
],
298 default_expect_regex
: Optional
[str],
299 ignore_error_text
: bool,
302 normalize_paths
: bool,
303 mode_flag
: List
[str],
304 get_flags
: Callable
[[str], List
[str]],
305 verify_pessimisation
: VerifyPessimisationOptions
,
306 check_expected_included_in_actual
: bool,
307 timeout
: Optional
[float] = None,
308 filter_glog_failures
: bool = False,
311 Run the program and return a list of results.
314 def run(test_case
: TestCase
) -> Result
:
315 test_dir
, test_name
= os
.path
.split(test_case
.file_path
)
316 flags
= get_flags(test_dir
)
317 test_flags
= get_test_flags(test_case
.file_path
)
320 if test_case
.input is None:
321 cmd
.append(test_name
)
322 cmd
+= flags
+ test_flags
323 debug_cmd(test_dir
, cmd
)
324 env
= os
.environ
.copy()
325 env
["FORCE_ERROR_COLOR"] = "true" if force_color
else "false"
327 output
= subprocess
.check_output(
329 stderr
=None if no_stderr
else subprocess
.STDOUT
,
331 universal_newlines
=True,
332 input=test_case
.input,
337 except subprocess
.TimeoutExpired
as e
:
338 output
= "Timed out. " + str(e
.output
)
339 except subprocess
.CalledProcessError
as e
:
340 if e
.returncode
== 126:
341 # unable to execute, typically due in buck2 to "Text file is busy" because someone still has a write handle open to it.
342 # https://www.internalfb.com/intern/qa/312685/text-file-is-busy---test-is-run-before-fclose-on-e
343 # Ugly workaround for now: just skip it
344 # This should be removed once T107518211 is closed.
345 output
= "Timed out. " + str(e
.output
)
347 # we don't care about nonzero exit codes... for instance, type
348 # errors cause hh_single_type_check to produce them
349 output
= str(e
.output
)
350 if filter_glog_failures
:
351 glog_message_re
= r
"COULD NOT CREATE A LOGGINGFILE .+\!|Could not create logging file: No such file or directory\n"
352 output
= re
.sub(glog_message_re
, r
"", output
)
355 default_expect_regex
,
358 verify_pessimisation
,
359 check_expected_included_in_actual
=check_expected_included_in_actual
,
363 executor
= ThreadPoolExecutor(max_workers
=max_workers
)
364 futures
= [executor
.submit(run
, test_case
) for test_case
in test_cases
]
366 return [future
.result() for future
in futures
]
369 def filter_ocaml_stacktrace(text
: str) -> str:
370 """take a string and remove all the lines that look like
371 they're part of an OCaml stacktrace"""
372 assert isinstance(text
, str)
373 it
= text
.splitlines()
376 drop_line
= x
.lstrip().startswith("Called") or x
.lstrip().startswith("Raised")
381 return "\n".join(out
)
384 def filter_cwd(text
: str) -> str:
385 """take a string and remove all instances of the current cwd"""
386 assert isinstance(text
, str)
387 # Get the absolute path of the current working directory
388 cwd
= os
.path
.abspath(os
.getcwd())
389 # Normalize the path separators to work on different operating systems
390 cwd
= cwd
.replace("\\", "/")
391 return text
.replace(cwd
, "/")
394 def filter_temp_hhi_path(text
: str) -> str:
395 """The .hhi files are stored in a temporary directory whose name
396 changes every time. Normalise it.
398 /tmp/ASjh5RoWbb/builtins_fb.hhi -> /tmp/hhi_dir/builtins_fb.hhi
402 r
"/tmp/[^/]*/([a-zA-Z0-9_]+\.hhi)",
408 def strip_pess_suffix(text
: str) -> str:
416 def compare_expected(
417 expected
: str, out
: str, verify_pessimisation
: VerifyPessimisationOptions
420 verify_pessimisation
== VerifyPessimisationOptions
.no
421 or verify_pessimisation
== VerifyPessimisationOptions
.all
422 or verify_pessimisation
== VerifyPessimisationOptions
.full
424 if expected
== "No errors" or out
== "No errors":
425 return expected
== out
428 elif verify_pessimisation
== VerifyPessimisationOptions
.added
:
429 return not (expected
== "No errors" and out
!= "No errors")
430 elif verify_pessimisation
== VerifyPessimisationOptions
.removed
:
431 return not (expected
!= "No errors" and out
== "No errors")
434 "Cannot happen: verify_pessimisation option %s" % verify_pessimisation
438 # Strip leading and trailing whitespace from every line
439 def strip_lines(text
: str) -> str:
440 return "\n".join(line
.strip() for line
in text
.splitlines())
445 default_expect_regex
: Optional
[str],
446 ignore_error_messages
: bool,
447 normalize_paths
: bool,
448 verify_pessimisation
: VerifyPessimisationOptions
,
449 check_expected_included_in_actual
: bool,
452 if check_expected_included_in_actual
:
453 return check_included(test_case
, out
)
455 return check_expected_equal_actual(
457 default_expect_regex
,
458 ignore_error_messages
,
460 verify_pessimisation
,
465 def check_expected_equal_actual(
467 default_expect_regex
: Optional
[str],
468 ignore_error_messages
: bool,
469 normalize_paths
: bool,
470 verify_pessimisation
: VerifyPessimisationOptions
,
474 Check that the output of the test in :out corresponds to the expected
475 output, or if a :default_expect_regex is provided,
476 check that the output in :out contains the provided regex.
478 expected
= filter_temp_hhi_path(strip_lines(test_case
.expected
))
480 filter_temp_hhi_path(strip_lines(out
))
481 if verify_pessimisation
== "no"
482 else strip_pess_suffix(filter_temp_hhi_path(strip_lines(out
)))
484 out
= filter_cwd(normalized_out
) if normalize_paths
else out
486 expected
== normalized_out
489 ignore_error_messages
490 or verify_pessimisation
!= VerifyPessimisationOptions
.no
491 and verify_pessimisation
!= VerifyPessimisationOptions
.full
493 and compare_expected(expected
, normalized_out
, verify_pessimisation
)
495 or expected
== filter_ocaml_stacktrace(normalized_out
)
497 default_expect_regex
is not None
498 and re
.search(default_expect_regex
, normalized_out
) is not None
501 or (normalize_paths
and (expected
== out
))
503 return Result(test_case
=test_case
, output
=out
, is_failure
=not is_ok
)
506 def check_included(test_case
: TestCase
, output
: str) -> Result
:
507 elts
= set(output
.splitlines())
508 expected_elts
= set(test_case
.expected
.splitlines())
510 for expected_elt
in expected_elts
:
511 if expected_elt
not in elts
:
516 for elt
in expected_elts
.intersection(elts
):
518 return Result(test_case
, output
, is_failure
)
521 def record_results(results
: List
[Result
], out_ext
: str) -> None:
522 for result
in results
:
523 outfile
= result
.test_case
.file_path
+ out_ext
524 with
open(outfile
, "wb") as f
:
525 f
.write(bytes(result
.output
, "UTF-8"))
528 def find_in_ancestors_rec(dir: str, path
: str) -> str:
529 if path
== "" or os
.path
.dirname(path
) == path
:
530 raise Exception("Could not find directory %s in ancestors." % dir)
531 if os
.path
.basename(path
) == dir:
533 return find_in_ancestors_rec(dir, os
.path
.dirname(path
))
536 def find_in_ancestors(dir: str, path
: str) -> str:
538 return find_in_ancestors_rec(dir, path
)
540 raise Exception("Could not find directory %s in ancestors of %s." % (dir, path
))
543 def get_exp_out_dirs(test_file
: str) -> Tuple
[str, str]:
545 os
.environ
.get("HACK_BUILD_ROOT") is not None
546 and os
.environ
.get("HACK_SOURCE_ROOT") is not None
548 exp_dir
= os
.environ
["HACK_SOURCE_ROOT"]
549 out_dir
= os
.environ
["HACK_BUILD_ROOT"]
551 fbcode
= find_in_ancestors("fbcode", test_file
)
552 exp_dir
= os
.path
.join(fbcode
, "hphp", "hack")
553 out_dir
= os
.path
.dirname(find_in_ancestors("test", test_file
))
554 return exp_dir
, out_dir
559 failures
: List
[Result
],
561 expect_extension
: str,
562 fallback_expect_extension
: Optional
[str],
563 verify_pessimisation
: VerifyPessimisationOptions
= VerifyPessimisationOptions
.no
,
564 no_copy
: bool = False,
565 only_compare_error_lines
: bool = False,
567 if only_compare_error_lines
:
568 for failure
in failures
:
569 print(f
"\033[95m{failure.test_case.file_path}\033[0m")
570 print(failure
.output
)
573 record_results(failures
, out_extension
)
575 dump_failures(failures
)
576 fnames
= [failure
.test_case
.file_path
for failure
in failures
]
577 print("To review the failures, use the following command: ")
579 first_test_file
= os
.path
.realpath(failures
[0].test_case
.file_path
)
580 out_dir
: str # for Pyre
581 (exp_dir
, out_dir
) = get_exp_out_dirs(first_test_file
)
583 # Get a full path to 'review.sh' so this command be run
584 # regardless of your current directory.
585 review_script
= os
.path
.join(
586 os
.path
.dirname(os
.path
.realpath(__file__
)), "review.sh"
588 if not os
.path
.isfile(review_script
):
589 review_script
= "./hphp/hack/test/review.sh"
591 def fname_map_var(f
: str) -> str:
592 return "hphp/hack/" + os
.path
.relpath(f
, out_dir
)
595 if out_extension
!= DEFAULT_OUT_EXT
:
596 env_vars
.append("OUT_EXT=%s" % out_extension
)
597 if expect_extension
!= DEFAULT_EXP_EXT
:
598 env_vars
.append("EXP_EXT=%s" % expect_extension
)
599 if fallback_expect_extension
is not None:
600 env_vars
.append("FALLBACK_EXP_EXT=%s " % fallback_expect_extension
)
601 if verify_pessimisation
!= VerifyPessimisationOptions
.no
:
602 env_vars
.append("VERIFY_PESSIMISATION=true")
604 env_vars
.append("UPDATE=never")
606 env_vars
.extend(["SOURCE_ROOT=%s" % exp_dir
, "OUTPUT_ROOT=%s" % out_dir
])
613 " ".join(map(fname_map_var
, fnames
)),
617 # If more than 75% of files have changed, we're probably doing
618 # a transformation to all the .exp files.
619 if len(fnames
) >= (0.75 * total
):
621 "\nJust want to update all the %s files? Use UPDATE=always with the above command."
626 def dump_failures(failures
: List
[Result
]) -> None:
628 expected
= f
.test_case
.expected
630 diff
= difflib
.ndiff(expected
.splitlines(True), actual
.splitlines(True))
631 print("Details for the failed test %s:" % f
.test_case
.file_path
)
632 print("\n>>>>> Expected output >>>>>>\n")
634 print("\n===== Actual output ======\n")
636 print("\n<<<<< End Actual output <<<<<<<\n")
637 print("\n>>>>> Diff >>>>>>>\n")
639 print("\n<<<<< End Diff <<<<<<<\n")
642 def get_hh_flags(test_dir
: str) -> List
[str]:
643 path
= os
.path
.join(test_dir
, "HH_FLAGS")
644 if not os
.path
.isfile(path
):
646 print("No HH_FLAGS file found")
648 with
open(path
) as f
:
649 return shlex
.split(f
.read())
652 def files_with_ext(files
: List
[str], ext
: str) -> List
[str]:
654 Returns the set of filenames in :files that end in :ext
656 filtered_files
: List
[str] = []
658 prefix
, suffix
= os
.path
.splitext(file)
660 filtered_files
.append(prefix
)
661 return filtered_files
665 root
: str, disabled_ext
: str, test_ext
: str, include_directories
: bool
667 if os
.path
.isfile(root
):
668 if root
.endswith(test_ext
):
672 elif os
.path
.isdir(root
):
673 result
: List
[str] = []
674 if include_directories
and root
.endswith(test_ext
):
676 children
= os
.listdir(root
)
677 disabled
= files_with_ext(children
, disabled_ext
)
678 for child
in children
:
679 if child
!= "disabled" and child
not in disabled
:
682 os
.path
.join(root
, child
),
689 elif os
.path
.islink(root
):
690 # Some editors create broken symlinks as part of their locking scheme,
694 raise Exception("Could not find test file or directory at %s" % root
)
697 def get_content_(file_path
: str, ext
: str) -> str:
698 with
open(file_path
+ ext
, "r") as fexp
:
703 file_path
: str, ext
: str = "", fallback_ext
: Optional
[str] = None
706 return get_content_(file_path
, ext
)
707 except FileNotFoundError
:
708 if fallback_ext
is not None:
710 return get_content_(file_path
, fallback_ext
)
711 except FileNotFoundError
:
719 expected_extension
: str,
720 fallback_expect_extension
: Optional
[str],
724 default_expect_regex
: Optional
[str],
726 ignore_error_text
: bool,
729 normalize_paths
: bool,
730 verify_pessimisation
: VerifyPessimisationOptions
,
731 mode_flag
: List
[str],
732 get_flags
: Callable
[[str], List
[str]],
733 check_expected_included_in_actual
: bool,
734 timeout
: Optional
[float] = None,
735 only_compare_error_lines
: bool = False,
736 filter_glog_failures
: bool = False,
739 # for each file, create a test case
743 expected
=get_content(file, expected_extension
, fallback_expect_extension
),
744 input=get_content(file) if use_stdin
else None,
749 results
= run_batch_tests(
752 default_expect_regex
,
760 verify_pessimisation
,
761 check_expected_included_in_actual
,
762 only_compare_error_lines
,
765 results
= run_test_program(
768 default_expect_regex
,
775 verify_pessimisation
,
776 check_expected_included_in_actual
=check_expected_included_in_actual
,
778 filter_glog_failures
=filter_glog_failures
,
781 failures
= [result
for result
in results
if result
.is_failure
]
783 num_results
= len(results
)
786 "All tests in the suite passed! "
787 "The number of tests that ran: %d\n" % num_results
790 print("The number of tests that failed: %d/%d\n" % (len(failures
), num_results
))
795 args
.expect_extension
,
796 args
.fallback_expect_extension
,
797 verify_pessimisation
=verify_pessimisation
,
798 only_compare_error_lines
=only_compare_error_lines
,
800 sys
.exit(1) # this exit code fails the suite and lets Buck know
805 def run_idempotence_tests(
806 results
: List
[Result
],
807 expected_extension
: str,
810 default_expect_regex
: Optional
[str],
811 mode_flag
: List
[str],
812 get_flags
: Callable
[[str], List
[str]],
813 normalize_paths
: bool,
814 check_expected_included_in_actual
: bool,
816 idempotence_test_cases
= [
818 file_path
=result
.test_case
.file_path
,
819 expected
=result
.test_case
.expected
,
822 for result
in results
825 idempotence_results
= run_test_program(
826 idempotence_test_cases
,
828 default_expect_regex
,
835 VerifyPessimisationOptions
.no
,
836 check_expected_included_in_actual
=check_expected_included_in_actual
,
839 num_idempotence_results
= len(idempotence_results
)
841 idempotence_failures
= [
842 result
for result
in idempotence_results
if result
.is_failure
845 if idempotence_failures
== []:
847 "All idempotence tests in the suite passed! The number of "
848 "idempotence tests that ran: %d\n" % num_idempotence_results
852 "The number of idempotence tests that failed: %d/%d\n"
853 % (len(idempotence_failures
), num_idempotence_results
)
856 num_idempotence_results
,
857 idempotence_failures
,
858 out_extension
+ out_extension
, # e.g., *.out.out
863 sys
.exit(1) # this exit code fails the suite and lets Buck know
866 def get_flags_cache(args_flags
: List
[str]) -> Callable
[[str], List
[str]]:
867 flags_cache
: Dict
[str, List
[str]] = {}
869 def get_flags(test_dir
: str) -> List
[str]:
870 if test_dir
not in flags_cache
:
871 flags_cache
[test_dir
] = get_hh_flags(test_dir
)
872 flags
= flags_cache
[test_dir
]
873 if args_flags
is not None:
874 flags
= flags
+ args_flags
880 def get_flags_dummy(args_flags
: List
[str]) -> Callable
[[str], List
[str]]:
881 def get_flags(_
: str) -> List
[str]:
888 global max_workers
, dump_on_failure
, verbose
, args
889 parser
= argparse
.ArgumentParser()
890 parser
.add_argument("test_path", help="A file or a directory. ")
891 parser
.add_argument("--program", type=os
.path
.abspath
)
892 parser
.add_argument("--out-extension", type=str, default
=DEFAULT_OUT_EXT
)
893 parser
.add_argument("--expect-extension", type=str, default
=DEFAULT_EXP_EXT
)
894 parser
.add_argument("--fallback-expect-extension", type=str)
895 parser
.add_argument("--default-expect-regex", type=str)
896 parser
.add_argument("--in-extension", type=str, default
=".php")
897 parser
.add_argument("--include-directories", action
="store_true")
898 parser
.add_argument("--disabled-extension", type=str, default
=".no_typecheck")
899 parser
.add_argument("--verbose", action
="store_true")
903 help="Verify that the output passed to the program "
904 "as input results in the same output.",
906 parser
.add_argument("--max-workers", type=int, default
="48")
910 help="On test failure, show the content of " "the files and a diff",
912 parser
.add_argument("--mode-flag", type=str)
913 parser
.add_argument("--flags", nargs
=argparse
.REMAINDER
)
915 "--stdin", action
="store_true", help="Pass test input file via stdin"
920 help="Do not include stderr output in the output file",
923 "--batch", action
="store_true", help="Run tests in batches to the test program"
926 "--ignore-error-text",
928 help="Do not compare error text when verifying output",
931 "--only-compare-error-lines",
933 help="Does not care about exact expected error message, "
934 "but only compare the error line numbers.",
937 "--check-expected-included-in-actual",
939 help="Check that the set of lines in the expected file is included in the set"
940 " of lines in the output",
945 help="Timeout in seconds for each test, in non-batch mode.",
950 help="Set the FORCE_ERROR_COLOR environment variable, "
951 "which causes the test output to retain terminal escape codes.",
954 "--no-hh-flags", action
="store_true", help="Do not read HH_FLAGS files"
957 "--verify-pessimisation",
958 type=VerifyPessimisationOptions
,
959 choices
=list(VerifyPessimisationOptions
),
960 default
=VerifyPessimisationOptions
.no
,
961 help="Experimental test suite for hh_pessimisation",
966 help="Match on normalized file paths in error messages",
969 "--filter-glog-failures",
971 help='Filters out glog messages of the "COULD NOT CREATE A LOGGINGFILE" form.',
974 "%s looks for a file named HH_FLAGS in the same directory"
975 " as the test files it is executing. If found, the "
976 "contents will be passed as arguments to "
977 "<program> in addition to any arguments "
978 "specified by --flags" % parser
.prog
980 args
= parser
.parse_args()
982 max_workers
= args
.max_workers
983 verbose
= args
.verbose
985 dump_on_failure
= args
.diff
986 if os
.getenv("SANDCASTLE") is not None:
987 dump_on_failure
= True
989 if not os
.path
.isfile(args
.program
):
990 raise Exception("Could not find program at %s" % args
.program
)
992 # 'args.test_path' is a path relative to the current working
993 # directory. buck1 runs this test from fbsource/fbocde, buck2 runs
995 if os
.path
.basename(os
.getcwd()) != "fbsource":
997 # If running under buck1 then we are in fbcode, if running
998 # under dune then some ancestor directory of fbcode. These two
999 # cases are handled by the logic of this script and
1000 # 'review.sh' and there are no adjustments to make.
1004 # The buck2 case has us running in fbsource. This puts us in
1008 files
: List
[str] = list_test_files(
1010 args
.disabled_extension
,
1012 args
.include_directories
,
1016 raise Exception("Could not find any files to test in " + args
.test_path
)
1018 mode_flag
: List
[str] = [] if args
.mode_flag
is None else [args
.mode_flag
]
1019 get_flags
: Callable
[[str], List
[str]] = (
1020 get_flags_dummy(args
.flags
) if args
.no_hh_flags
else get_flags_cache(args
.flags
)
1023 results
: List
[Result
] = run_tests(
1025 args
.expect_extension
,
1026 args
.fallback_expect_extension
,
1030 args
.default_expect_regex
,
1032 args
.ignore_error_text
,
1035 args
.normalize_paths
,
1036 args
.verify_pessimisation
,
1039 check_expected_included_in_actual
=args
.check_expected_included_in_actual
,
1040 timeout
=args
.timeout
,
1041 only_compare_error_lines
=args
.only_compare_error_lines
,
1042 filter_glog_failures
=args
.filter_glog_failures
,
1045 # Doesn't make sense to check failures for idempotence
1046 successes
: List
[Result
] = [result
for result
in results
if not result
.is_failure
]
1048 if args
.idempotence
and successes
:
1049 run_idempotence_tests(
1051 args
.expect_extension
,
1054 args
.default_expect_regex
,
1057 args
.normalize_paths
,
1058 check_expected_included_in_actual
=args
.check_expected_included_in_actual
,
1062 args
: argparse
.Namespace
1065 if __name__
== "__main__":