Modernize stat usage
[scons.git] / testing / framework / TestCommon.py
blob5038f1a66517ca53043452a01f57328663be8811
1 # Copyright 2000-2010 Steven Knight
3 # This module is free software, and you may redistribute it and/or modify
4 # it under the same terms as Python itself, so long as this copyright message
5 # and disclaimer are retained in their original form.
7 # IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
8 # SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
9 # THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
10 # DAMAGE.
12 # THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
13 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
14 # PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
15 # AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
18 # Python License: https://docs.python.org/3/license.html#psf-license
20 """
21 A testing framework for commands and scripts with commonly useful error handling
23 The TestCommon module provides a simple, high-level interface for writing
24 tests of executable commands and scripts, especially commands and scripts
25 that interact with the file system. All methods throw exceptions and
26 exit on failure, with useful error messages. This makes a number of
27 explicit checks unnecessary, making the test scripts themselves simpler
28 to write and easier to read.
30 The TestCommon class is a subclass of the TestCmd class. In essence,
31 TestCommon is a wrapper that handles common TestCmd error conditions in
32 useful ways. You can use TestCommon directly, or subclass it for your
33 program and add additional (or override) methods to tailor it to your
34 program's specific needs. Alternatively, the TestCommon class serves
35 as a useful example of how to define your own TestCmd subclass.
37 As a subclass of TestCmd, TestCommon provides access to all of the
38 variables and methods from the TestCmd module. Consequently, you can
39 use any variable or method documented in the TestCmd module without
40 having to explicitly import TestCmd.
42 A TestCommon environment object is created via the usual invocation:
44 import TestCommon
45 test = TestCommon.TestCommon()
47 You can use all of the TestCmd keyword arguments when instantiating a
48 TestCommon object; see the TestCmd documentation for details.
50 Here is an overview of the methods and keyword arguments that are
51 provided by the TestCommon class:
53 test.must_be_writable('file1', ['file2', ...])
55 test.must_contain('file', 'required text\n')
57 test.must_contain_all(output, input, ['title', find])
59 test.must_contain_all_lines(output, lines, ['title', find])
61 test.must_contain_single_instance_of(output, lines, ['title'])
63 test.must_contain_any_line(output, lines, ['title', find])
65 test.must_contain_exactly_lines(output, lines, ['title', find])
67 test.must_exist('file1', ['file2', ...], ['message'])
69 test.must_exist_one_of(files, ['message'])
71 test.must_match('file', "expected contents\n")
73 test.must_match_file(file, golden_file, ['message', 'newline'])
75 test.must_not_be_writable('file1', ['file2', ...])
77 test.must_not_contain('file', 'banned text\n')
79 test.must_not_contain_any_line(output, lines, ['title', find])
81 test.must_not_contain_lines(lines, output, ['title', find]):
83 test.must_not_exist('file1', ['file2', ...])
85 test.must_not_exist_any_of(files)
87 test.must_not_be_empty('file')
89 test.run(
90 options="options to be prepended to arguments",
91 arguments="string or list of arguments",
92 stdout="expected standard output from the program",
93 stderr="expected error output from the program",
94 status=expected_status,
95 match=match_function,
98 The TestCommon module also provides the following variables
100 TestCommon.python
101 TestCommon._python_
102 TestCommon.exe_suffix
103 TestCommon.obj_suffix
104 TestCommon.shobj_prefix
105 TestCommon.shobj_suffix
106 TestCommon.lib_prefix
107 TestCommon.lib_suffix
108 TestCommon.dll_prefix
109 TestCommon.dll_suffix
113 from __future__ import annotations
115 __author__ = "Steven Knight <knight at baldmt dot com>"
116 __revision__ = "TestCommon.py 1.3.D001 2010/06/03 12:58:27 knight"
117 __version__ = "1.3"
119 import glob
120 import os
121 import stat
122 import sys
123 import sysconfig
125 from collections import UserList
126 from typing import Callable
128 from TestCmd import *
129 from TestCmd import __all__
131 __all__.extend(
133 'TestCommon',
134 'exe_suffix',
135 'obj_suffix',
136 'shobj_prefix',
137 'shobj_suffix',
138 'lib_prefix',
139 'lib_suffix',
140 'dll_prefix',
141 'dll_suffix',
145 # Variables that describe the prefixes and suffixes on this system.
146 if sys.platform == 'win32':
147 if sysconfig.get_platform() == "mingw":
148 obj_suffix = '.o'
149 shobj_suffix = '.o'
150 else:
151 obj_suffix = '.obj'
152 shobj_suffix = '.obj'
153 exe_suffix = '.exe'
154 shobj_prefix = ''
155 lib_prefix = ''
156 # TODO: for mingw, is this .lib or .a?
157 lib_suffix = '.lib'
158 dll_prefix = ''
159 dll_suffix = '.dll'
160 elif sys.platform == 'cygwin':
161 exe_suffix = '.exe'
162 obj_suffix = '.o'
163 shobj_suffix = '.os'
164 shobj_prefix = ''
165 lib_prefix = 'lib'
166 lib_suffix = '.a'
167 dll_prefix = 'cyg'
168 dll_suffix = '.dll'
169 elif sys.platform.find('irix') != -1:
170 exe_suffix = ''
171 obj_suffix = '.o'
172 shobj_suffix = '.o'
173 shobj_prefix = ''
174 lib_prefix = 'lib'
175 lib_suffix = '.a'
176 dll_prefix = 'lib'
177 dll_suffix = '.so'
178 elif sys.platform.find('darwin') != -1:
179 exe_suffix = ''
180 obj_suffix = '.o'
181 shobj_suffix = '.os'
182 shobj_prefix = ''
183 lib_prefix = 'lib'
184 lib_suffix = '.a'
185 dll_prefix = 'lib'
186 dll_suffix = '.dylib'
187 elif sys.platform.find('sunos') != -1:
188 exe_suffix = ''
189 obj_suffix = '.o'
190 shobj_suffix = '.pic.o'
191 shobj_prefix = ''
192 lib_prefix = 'lib'
193 lib_suffix = '.a'
194 dll_prefix = 'lib'
195 dll_suffix = '.so'
196 else:
197 exe_suffix = ''
198 obj_suffix = '.o'
199 shobj_suffix = '.os'
200 shobj_prefix = ''
201 lib_prefix = 'lib'
202 lib_suffix = '.a'
203 dll_prefix = 'lib'
204 dll_suffix = '.so'
206 def is_List(e):
207 return isinstance(e, (list, UserList))
209 def is_Tuple(e):
210 return isinstance(e, tuple)
212 def is_Sequence(e):
213 return (not hasattr(e, "strip") and
214 hasattr(e, "__getitem__") or
215 hasattr(e, "__iter__"))
217 def is_writable(f):
218 mode = os.stat(f).st_mode
219 return mode & stat.S_IWUSR
221 def separate_files(flist):
222 existing = []
223 missing = []
224 for f in flist:
225 if os.path.exists(f):
226 existing.append(f)
227 else:
228 missing.append(f)
229 return existing, missing
231 def contains(seq, subseq, find: Callable | None = None) -> bool:
232 if find is None:
233 return subseq in seq
234 else:
235 f = find(seq, subseq)
236 return f not in (None, -1) and f is not False
238 def find_index(seq, subseq, find: Callable | None = None) -> int | None:
239 # Returns either an index of the subseq within the seq, or None.
240 # Accepts a function find(seq, subseq), which returns an integer on success
241 # and either: None, False, or -1, on failure.
242 if find is None:
243 try:
244 return seq.index(subseq)
245 except ValueError:
246 return None
247 else:
248 i = find(seq, subseq)
249 return None if (i in (None, -1) or i is False) else i
252 if os.name == 'posix':
253 def _failed(self, status: int = 0):
254 if self.status is None or status is None:
255 return None
256 return _status(self) != status
257 def _status(self):
258 return self.status
259 elif os.name == 'nt':
260 def _failed(self, status: int = 0):
261 return not (self.status is None or status is None) and \
262 self.status != status
263 def _status(self):
264 return self.status
266 class TestCommon(TestCmd):
268 # Additional methods from the Perl Test::Cmd::Common module
269 # that we may wish to add in the future:
271 # $test->subdir('subdir', ...);
273 # $test->copy('src_file', 'dst_file');
275 def __init__(self, **kw) -> None:
276 """Initialize a new TestCommon instance. This involves just
277 calling the base class initialization, and then changing directory
278 to the workdir.
280 super().__init__(**kw)
281 os.chdir(self.workdir)
283 def options_arguments(
284 self,
285 options: str | list[str],
286 arguments: str | list[str],
288 """Merges the "options" keyword argument with the arguments."""
289 # TODO: this *doesn't* split unless both are non-empty strings.
290 # Did we want to always split strings?
291 if options:
292 if arguments is None:
293 return options
295 # If not list, then split into lists
296 # this way we're not losing arguments specified with
297 # Spaces in quotes.
298 if isinstance(options, str):
299 options = options.split()
300 if isinstance(arguments, str):
301 arguments = arguments.split()
302 arguments = options + arguments
304 return arguments
306 def must_be_writable(self, *files) -> None:
307 """Ensure that the specified file(s) exist and are writable.
309 An individual file can be specified as a list of directory names,
310 in which case the pathname will be constructed by concatenating
311 them. Exits FAILED if any of the files does not exist or is
312 not writable.
314 flist = [is_List(x) and os.path.join(*x) or x for x in files]
315 existing, missing = separate_files(flist)
316 unwritable = [x for x in existing if not is_writable(x)]
317 if missing:
318 print("Missing files: `%s'" % "', `".join(missing))
319 if unwritable:
320 print("Unwritable files: `%s'" % "', `".join(unwritable))
321 self.fail_test(missing + unwritable)
323 def must_contain(
324 self,
325 file: str,
326 required: str,
327 mode: str = 'rb',
328 find: Callable | None = None,
329 ) -> None:
330 """Ensures specified file contains the required text.
332 Args:
333 file: name of file to search in.
334 required (string): text to search for. For the default
335 find function, type must match the return type from
336 reading the file; current implementation will convert.
337 mode: file open mode.
338 find: optional custom search routine. Must take the
339 form ``find(output, line)``, returning non-negative integer on success
340 and None, False, or -1, on failure.
342 Calling test exits FAILED if search result is false
344 if 'b' in mode:
345 # Reading a file in binary mode returns a bytes object.
346 # We cannot search for a string in a bytes obj so convert.
347 required = to_bytes(required)
348 file_contents = self.read(file, mode)
350 if not contains(file_contents, required, find):
351 print(f"File `{file}' does not contain required string.")
352 print(self.banner('Required string '))
353 print(required)
354 print(self.banner(f'{file} contents '))
355 print(file_contents)
356 self.fail_test()
358 def must_contain_all(self, output, input, title: str = "", find: Callable | None = None)-> None:
359 """Ensures that the specified output string (first argument)
360 contains all of the specified input as a block (second argument).
362 An optional third argument can be used to describe the type
363 of output being searched, and only shows up in failure output.
365 An optional fourth argument can be used to supply a different
366 function, of the form "find(output, line)", to use when searching
367 for lines in the output.
369 if is_List(output):
370 output = '\n'.join(output)
372 if not contains(output, input, find):
373 if not title:
374 title = 'output'
375 print(f'Missing expected input from {title}:')
376 print(input)
377 print(self.banner(f"{title} "))
378 print(output)
379 self.fail_test()
381 def must_contain_all_lines(self, output, lines, title: str = "", find: Callable | None = None) -> None:
382 """Ensures that the specified output string (first argument)
383 contains all of the specified lines (second argument).
385 An optional third argument can be used to describe the type
386 of output being searched, and only shows up in failure output.
388 An optional fourth argument can be used to supply a different
389 function, of the form "find(output, line)", to use when searching
390 for lines in the output.
392 if is_List(output):
393 output = '\n'.join(output)
395 missing = [line for line in lines if not contains(output, line, find)]
396 if missing:
397 if not title:
398 title = 'output'
399 sys.stdout.write(f"Missing expected lines from {title}:\n")
400 for line in missing:
401 sys.stdout.write(f" {line!r}\n")
402 sys.stdout.write(f"{self.banner(f'{title} ')}\n")
403 sys.stdout.write(output)
404 self.fail_test()
406 def must_contain_single_instance_of(self, output, lines, title: str = "") -> None:
407 """Ensures that the specified output string (first argument)
408 contains one instance of the specified lines (second argument).
410 An optional third argument can be used to describe the type
411 of output being searched, and only shows up in failure output.
413 if is_List(output):
414 output = '\n'.join(output)
416 counts = {}
417 for line in lines:
418 count = output.count(line)
419 if count != 1:
420 counts[line] = count
422 if counts:
423 if not title:
424 title = 'output'
425 sys.stdout.write(f"Unexpected number of lines from {title}:\n")
426 for line in counts:
427 sys.stdout.write(f" {line!r}: found {str(counts[line])}\n")
428 sys.stdout.write(f"{self.banner(f'{title} ')}\n")
429 sys.stdout.write(output)
430 self.fail_test()
432 def must_contain_any_line(self, output, lines, title: str = "", find: Callable | None = None) -> None:
433 """Ensures that the specified output string (first argument)
434 contains at least one of the specified lines (second argument).
436 An optional third argument can be used to describe the type
437 of output being searched, and only shows up in failure output.
439 An optional fourth argument can be used to supply a different
440 function, of the form "find(output, line)", to use when searching
441 for lines in the output.
443 for line in lines:
444 if contains(output, line, find):
445 return
447 if not title:
448 title = 'output'
449 sys.stdout.write(f"Missing any expected line from {title}:\n")
450 for line in lines:
451 sys.stdout.write(f" {line!r}\n")
452 sys.stdout.write(f"{self.banner(f'{title} ')}\n")
453 sys.stdout.write(output)
454 self.fail_test()
456 def must_contain_exactly_lines(self, output, expect, title: str = "", find: Callable | None = None) -> None:
457 """Ensures that the specified output string (first argument)
458 contains all of the lines in the expected string (second argument)
459 with none left over.
461 An optional third argument can be used to describe the type
462 of output being searched, and only shows up in failure output.
464 An optional fourth argument can be used to supply a different
465 function, of the form "find(output, line)", to use when searching
466 for lines in the output. The function must return the index
467 of the found line in the output, or None if the line is not found.
469 out = output.splitlines()
470 if is_List(expect):
471 exp = [ e.rstrip('\n') for e in expect ]
472 else:
473 exp = expect.splitlines()
474 if sorted(out) == sorted(exp):
475 # early out for exact match
476 return
477 missing = []
478 for line in exp:
479 i = find_index(out, line, find)
480 if i is None:
481 missing.append(line)
482 else:
483 out.pop(i)
485 if not missing and not out:
486 # all lines were matched
487 return
489 if not title:
490 title = 'output'
491 if missing:
492 sys.stdout.write(f"Missing expected lines from {title}:\n")
493 for line in missing:
494 sys.stdout.write(f" {line!r}\n")
495 sys.stdout.write(f"{self.banner(f'Missing {title} ')}\n")
496 if out:
497 sys.stdout.write(f"Extra unexpected lines from {title}:\n")
498 for line in out:
499 sys.stdout.write(f" {line!r}\n")
500 sys.stdout.write(f"{self.banner(f'Extra {title} ')}\n")
501 sys.stdout.flush()
502 self.fail_test()
504 def must_contain_lines(self, lines, output, title: str = "", find: Callable | None = None) -> None:
505 # Deprecated; retain for backwards compatibility.
506 self.must_contain_all_lines(output, lines, title, find)
508 def must_exist(self, *files, message: str = "") -> None:
509 """Ensures that the specified file(s) must exist. An individual
510 file be specified as a list of directory names, in which case the
511 pathname will be constructed by concatenating them. Exits FAILED
512 if any of the files does not exist.
514 flist = [is_List(x) and os.path.join(*x) or x for x in files]
515 missing = [x for x in flist if not os.path.exists(x) and not os.path.islink(x)]
516 if missing:
517 print("Missing files: `%s'" % "', `".join(missing))
518 self.fail_test(bool(missing), message=message)
520 def must_exist_one_of(self, files, message: str = "") -> None:
521 """Ensures that at least one of the specified file(s) exists.
522 The filenames can be given as a list, where each entry may be
523 a single path string, or a tuple of folder names and the final
524 filename that get concatenated.
525 Supports wildcard names like 'foo-1.2.3-*.rpm'.
526 Exits FAILED if none of the files exists.
528 missing = []
529 for x in files:
530 if is_List(x) or is_Tuple(x):
531 xpath = os.path.join(*x)
532 else:
533 xpath = is_Sequence(x) and os.path.join(x) or x
534 if glob.glob(xpath):
535 return
536 missing.append(xpath)
537 print("Missing one of: `%s'" % "', `".join(missing))
538 self.fail_test(bool(missing), message=message)
540 def must_match(
541 self,
542 file,
543 expect,
544 mode: str = 'rb',
545 match: Callable | None = None,
546 message: str = "",
547 newline=None,
549 """Matches the contents of the specified file (first argument)
550 against the expected contents (second argument). The expected
551 contents are a list of lines or a string which will be split
552 on newlines.
554 file_contents = self.read(file, mode, newline)
555 if not match:
556 match = self.match
557 try:
558 self.fail_test(
559 not match(to_str(file_contents), to_str(expect)),
560 message=message,
562 except KeyboardInterrupt:
563 raise
564 except:
565 print(f"Unexpected contents of `{file}'")
566 self.diff(expect, file_contents, 'contents ')
567 raise
569 def must_match_file(
570 self,
571 file,
572 golden_file,
573 mode: str = 'rb',
574 match: Callable | None = None,
575 message: str = "",
576 newline=None,
577 ) -> None:
578 """Matches the contents of the specified file (first argument)
579 against the expected contents (second argument). The expected
580 contents are a list of lines or a string which will be split
581 on newlines.
583 file_contents = self.read(file, mode, newline)
584 golden_file_contents = self.read(golden_file, mode, newline)
586 if not match:
587 match = self.match
589 try:
590 self.fail_test(
591 not match(to_str(file_contents), to_str(golden_file_contents)),
592 message=message,
594 except KeyboardInterrupt:
595 raise
596 except:
597 print("Unexpected contents of `%s'" % file)
598 self.diff(golden_file_contents, file_contents, 'contents ')
599 raise
601 def must_not_contain(self, file, banned, mode: str = 'rb', find = None) -> None:
602 """Ensures that the specified file doesn't contain the banned text.
604 file_contents = self.read(file, mode)
606 if contains(file_contents, banned, find):
607 print(f"File `{file}' contains banned string.")
608 print(self.banner('Banned string '))
609 print(banned)
610 print(self.banner(f'{file} contents '))
611 print(file_contents)
612 self.fail_test()
614 def must_not_contain_any_line(self, output, lines, title: str = "", find: Callable | None = None) -> None:
615 """Ensures that the specified output string (first argument)
616 does not contain any of the specified lines (second argument).
618 An optional third argument can be used to describe the type
619 of output being searched, and only shows up in failure output.
621 An optional fourth argument can be used to supply a different
622 function, of the form "find(output, line)", to use when searching
623 for lines in the output.
625 unexpected = []
626 for line in lines:
627 if contains(output, line, find):
628 unexpected.append(line)
630 if unexpected:
631 if not title:
632 title = 'output'
633 sys.stdout.write(f"Unexpected lines in {title}:\n")
634 for line in unexpected:
635 sys.stdout.write(f" {line!r}\n")
636 sys.stdout.write(f"{self.banner(f'{title} ')}\n")
637 sys.stdout.write(output)
638 self.fail_test()
640 def must_not_contain_lines(self, lines, output, title: str = "", find: Callable | None = None) -> None:
641 self.must_not_contain_any_line(output, lines, title, find)
643 def must_not_exist(self, *files) -> None:
644 """Ensures that the specified file(s) must not exist.
645 An individual file be specified as a list of directory names, in
646 which case the pathname will be constructed by concatenating them.
647 Exits FAILED if any of the files exists.
649 flist = [is_List(x) and os.path.join(*x) or x for x in files]
650 existing = [x for x in flist if os.path.exists(x) or os.path.islink(x)]
651 if existing:
652 print("Unexpected files exist: `%s'" % "', `".join(existing))
653 self.fail_test(bool(existing))
655 def must_not_exist_any_of(self, files) -> None:
656 """Ensures that none of the specified file(s) exists.
657 The filenames can be given as a list, where each entry may be
658 a single path string, or a tuple of folder names and the final
659 filename that get concatenated.
660 Supports wildcard names like 'foo-1.2.3-*.rpm'.
661 Exits FAILED if any of the files exists.
663 existing = []
664 for x in files:
665 if is_List(x) or is_Tuple(x):
666 xpath = os.path.join(*x)
667 else:
668 xpath = is_Sequence(x) and os.path.join(x) or x
669 if glob.glob(xpath):
670 existing.append(xpath)
671 if existing:
672 print("Unexpected files exist: `%s'" % "', `".join(existing))
673 self.fail_test(bool(existing))
675 def must_not_be_empty(self, file) -> None:
676 """Ensures that the specified file exists, and that it is not empty.
677 Exits FAILED if the file doesn't exist or is empty.
679 if not (os.path.exists(file) or os.path.islink(file)):
680 print(f"File doesn't exist: `{file}'")
681 self.fail_test(file)
683 try:
684 fsize = os.path.getsize(file)
685 except OSError:
686 fsize = 0
688 if fsize == 0:
689 print(f"File is empty: `{file}'")
690 self.fail_test(file)
692 def must_not_be_writable(self, *files) -> None:
693 """Ensures that the specified file(s) exist and are not writable.
694 An individual file can be specified as a list of directory names,
695 in which case the pathname will be constructed by concatenating
696 them. Exits FAILED if any of the files does not exist or is
697 writable.
699 flist = [is_List(x) and os.path.join(*x) or x for x in files]
700 existing, missing = separate_files(flist)
701 writable = [file for file in existing if is_writable(file)]
702 if missing:
703 print("Missing files: `%s'" % "', `".join(missing))
704 if writable:
705 print("Writable files: `%s'" % "', `".join(writable))
706 self.fail_test(missing + writable)
708 def _complete(self, actual_stdout, expected_stdout,
709 actual_stderr, expected_stderr, status, match) -> None:
711 Post-processes running a subcommand, checking for failure
712 status and displaying output appropriately.
714 if _failed(self, status):
715 expect = ''
716 if status != 0:
717 expect = f" (expected {str(status)})"
718 print(f"{self.program} returned {_status(self)}{expect}")
719 print(self.banner('STDOUT '))
720 print(actual_stdout)
721 print(self.banner('STDERR '))
722 print(actual_stderr)
723 self.fail_test()
724 if (expected_stdout is not None
725 and not match(actual_stdout, expected_stdout)):
726 self.diff(expected_stdout, actual_stdout, 'STDOUT ')
727 if actual_stderr:
728 print(self.banner('STDERR '))
729 print(actual_stderr)
730 self.fail_test()
731 if (expected_stderr is not None
732 and not match(actual_stderr, expected_stderr)):
733 print(self.banner('STDOUT '))
734 print(actual_stdout)
735 self.diff(expected_stderr, actual_stderr, 'STDERR ')
736 self.fail_test()
738 def start(self, program = None,
739 interpreter = None,
740 options = None,
741 arguments = None,
742 universal_newlines = None,
743 **kw):
745 Starts a program or script for the test environment, handling
746 any exceptions.
748 arguments = self.options_arguments(options, arguments)
749 try:
750 return super().start(program, interpreter, arguments,
751 universal_newlines, **kw)
752 except KeyboardInterrupt:
753 raise
754 except Exception as e:
755 print(self.banner('STDOUT '))
756 try:
757 print(self.stdout())
758 except IndexError:
759 pass
760 print(self.banner('STDERR '))
761 try:
762 print(self.stderr())
763 except IndexError:
764 pass
765 cmd_args = self.command_args(program, interpreter, arguments)
766 sys.stderr.write(f'Exception trying to execute: {cmd_args}\n')
767 raise e
770 def finish(
771 self,
772 popen,
773 stdout: str | None = None,
774 stderr: str | None = '',
775 status: int | None = 0,
776 **kw,
777 ) -> None:
778 """Finish and wait for the process being run.
780 The *popen* argument describes a ``Popen`` object controlling
781 the process.
783 The default behavior is to expect a successful exit, to not test
784 standard output, and to expect error output to be empty.
786 Only arguments extending :meth:`TestCmd.finish` are shown.
788 Args:
789 stdout: The expected standard output from the command.
790 A value of ``None`` means don't test standard output.
791 stderr: The expected error output from the command.
792 A value of ``None`` means don't test error output.
793 status: The expected exit status from the command.
794 A value of ``None`` means don't test exit status.
796 super().finish(popen, **kw)
797 match = kw.get('match', self.match)
798 self._complete(self.stdout(), stdout, self.stderr(), stderr, status, match)
801 def run(
802 self,
803 options=None,
804 arguments=None,
805 stdout: str | None = None,
806 stderr: str | None = '',
807 status: int | None = 0,
808 **kw,
809 ) -> None:
810 """Runs the program under test, checking that the test succeeded.
812 The default behavior is to expect a successful exit, not test
813 standard output, and expects error output to be empty.
815 Only arguments extending :meth:`TestCmd.run` are shown.
817 Args:
818 options: Extra options that get prepended to the beginning
819 of the arguments.
820 stdout: The expected standard output from the command.
821 A value of ``None`` means don't test standard output.
822 stderr: The expected error output from the command.
823 A value of ``None`` means don't test error output.
824 status: The expected exit status from the command.
825 A value of ``None`` means don't test exit status.
827 By default, this expects a successful exit (status = 0), does
828 not test standard output (stdout = None), and expects that error
829 output is empty (stderr = "").
831 kw['arguments'] = self.options_arguments(options, arguments)
832 try:
833 match = kw.pop('match')
834 except KeyError:
835 match = self.match
836 super().run(**kw)
837 self._complete(self.stdout(), stdout,
838 self.stderr(), stderr, status, match)
840 def skip_test(self, message: str="Skipping test.\n", from_fw: bool=False) -> None:
841 """Skips a test.
843 Proper test-skipping behavior is dependent on the external
844 TESTCOMMON_PASS_SKIPS environment variable. If set, we treat
845 the skip as a PASS (exit 0), and otherwise treat it as NO RESULT.
846 In either case, we print the specified message as an indication
847 that the substance of the test was skipped.
849 The use case for treating the skip as a PASS was an old system
850 that the SCons project has not used for a long time, and that
851 code path could eventually be dropped.
853 When reporting a NO RESULT, we normally skip the top line of the
854 traceback, as from no_result()'s point of view, that is this
855 function, and the user is likely to only be interested in the
856 test that called us. If from_fw is True, the skip was initiated
857 indirectly, coming from some function in the framework
858 (test_for_tool, skip_if_msvc, etc.), in this case we want to
859 skip an additional line to eliminate that function as well.
861 Args:
862 message: text to include in the skip message. Callers
863 should normally provide a reason, to improve on the default.
864 from_fw: if true, skip an extra line of traceback.
866 if message:
867 sys.stdout.write(message)
868 if not message.endswith('\n'):
869 sys.stdout.write('\n')
870 sys.stdout.flush()
871 pass_skips = os.environ.get('TESTCOMMON_PASS_SKIPS')
872 if pass_skips in [None, 0, '0']:
873 if from_fw:
874 self.no_result(skip=2)
875 else:
876 self.no_result(skip=1)
877 else:
878 # We're under the development directory for this change,
879 # so this is an Aegis invocation; pass the test (exit 0).
880 self.pass_test()
882 @staticmethod
883 def detailed_diff(value, expect) -> str:
884 v_split = value.split('\n')
885 e_split = expect.split('\n')
886 if len(v_split) != len(e_split):
887 print(f"different number of lines:{len(v_split)} {len(e_split)}")
889 # breakpoint()
890 for v, e in zip(v_split, e_split):
891 # print("%s:%s"%(v,e))
892 if v != e:
893 print(f"\n[{v}]\n[{e}]")
895 return f"Expected:\n{expect}\nGot:\n{value}"
898 # Local Variables:
899 # tab-width:4
900 # indent-tabs-mode:nil
901 # End:
902 # vim: set expandtab tabstop=4 shiftwidth=4: