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
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
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:
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')
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,
98 The TestCommon module also provides the following variables
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"
125 from collections
import UserList
126 from typing
import Callable
128 from TestCmd
import *
129 from TestCmd
import __all__
145 # Variables that describe the prefixes and suffixes on this system.
146 if sys
.platform
== 'win32':
147 if sysconfig
.get_platform() == "mingw":
152 shobj_suffix
= '.obj'
156 # TODO: for mingw, is this .lib or .a?
160 elif sys
.platform
== 'cygwin':
169 elif sys
.platform
.find('irix') != -1:
178 elif sys
.platform
.find('darwin') != -1:
186 dll_suffix
= '.dylib'
187 elif sys
.platform
.find('sunos') != -1:
190 shobj_suffix
= '.pic.o'
207 return isinstance(e
, (list, UserList
))
210 return isinstance(e
, tuple)
213 return (not hasattr(e
, "strip") and
214 hasattr(e
, "__getitem__") or
215 hasattr(e
, "__iter__"))
218 mode
= os
.stat(f
).st_mode
219 return mode
& stat
.S_IWUSR
221 def separate_files(flist
):
225 if os
.path
.exists(f
):
229 return existing
, missing
231 def contains(seq
, subseq
, find
: Callable |
None = None) -> bool:
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.
244 return seq
.index(subseq
)
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:
256 return _status(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
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
280 super().__init
__(**kw
)
281 os
.chdir(self
.workdir
)
283 def options_arguments(
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?
292 if arguments
is None:
295 # If not list, then split into lists
296 # this way we're not losing arguments specified with
298 if isinstance(options
, str):
299 options
= options
.split()
300 if isinstance(arguments
, str):
301 arguments
= arguments
.split()
302 arguments
= options
+ 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
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
)]
318 print("Missing files: `%s'" % "', `".join(missing
))
320 print("Unwritable files: `%s'" % "', `".join(unwritable
))
321 self
.fail_test(missing
+ unwritable
)
328 find
: Callable |
None = None,
330 """Ensures specified file contains the required text.
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
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 '))
354 print(self
.banner(f
'{file} contents '))
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.
370 output
= '\n'.join(output
)
372 if not contains(output
, input, find
):
375 print(f
'Missing expected input from {title}:')
377 print(self
.banner(f
"{title} "))
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.
393 output
= '\n'.join(output
)
395 missing
= [line
for line
in lines
if not contains(output
, line
, find
)]
399 sys
.stdout
.write(f
"Missing expected lines from {title}:\n")
401 sys
.stdout
.write(f
" {line!r}\n")
402 sys
.stdout
.write(f
"{self.banner(f'{title} ')}\n")
403 sys
.stdout
.write(output
)
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.
414 output
= '\n'.join(output
)
418 count
= output
.count(line
)
425 sys
.stdout
.write(f
"Unexpected number of lines from {title}:\n")
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
)
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.
444 if contains(output
, line
, find
):
449 sys
.stdout
.write(f
"Missing any expected line from {title}:\n")
451 sys
.stdout
.write(f
" {line!r}\n")
452 sys
.stdout
.write(f
"{self.banner(f'{title} ')}\n")
453 sys
.stdout
.write(output
)
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)
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()
471 exp
= [ e
.rstrip('\n') for e
in expect
]
473 exp
= expect
.splitlines()
474 if sorted(out
) == sorted(exp
):
475 # early out for exact match
479 i
= find_index(out
, line
, find
)
485 if not missing
and not out
:
486 # all lines were matched
492 sys
.stdout
.write(f
"Missing expected lines from {title}:\n")
494 sys
.stdout
.write(f
" {line!r}\n")
495 sys
.stdout
.write(f
"{self.banner(f'Missing {title} ')}\n")
497 sys
.stdout
.write(f
"Extra unexpected lines from {title}:\n")
499 sys
.stdout
.write(f
" {line!r}\n")
500 sys
.stdout
.write(f
"{self.banner(f'Extra {title} ')}\n")
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
)]
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.
530 if is_List(x
) or is_Tuple(x
):
531 xpath
= os
.path
.join(*x
)
533 xpath
= is_Sequence(x
) and os
.path
.join(x
) or x
536 missing
.append(xpath
)
537 print("Missing one of: `%s'" % "', `".join(missing
))
538 self
.fail_test(bool(missing
), message
=message
)
545 match
: Callable |
None = 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
554 file_contents
= self
.read(file, mode
, newline
)
559 not match(to_str(file_contents
), to_str(expect
)),
562 except KeyboardInterrupt:
565 print(f
"Unexpected contents of `{file}'")
566 self
.diff(expect
, file_contents
, 'contents ')
574 match
: Callable |
None = 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
583 file_contents
= self
.read(file, mode
, newline
)
584 golden_file_contents
= self
.read(golden_file
, mode
, newline
)
591 not match(to_str(file_contents
), to_str(golden_file_contents
)),
594 except KeyboardInterrupt:
597 print("Unexpected contents of `%s'" % file)
598 self
.diff(golden_file_contents
, file_contents
, 'contents ')
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 '))
610 print(self
.banner(f
'{file} contents '))
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.
627 if contains(output
, line
, find
):
628 unexpected
.append(line
)
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
)
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
)]
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.
665 if is_List(x
) or is_Tuple(x
):
666 xpath
= os
.path
.join(*x
)
668 xpath
= is_Sequence(x
) and os
.path
.join(x
) or x
670 existing
.append(xpath
)
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}'")
684 fsize
= os
.path
.getsize(file)
689 print(f
"File is empty: `{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
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)]
703 print("Missing files: `%s'" % "', `".join(missing
))
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
):
717 expect
= f
" (expected {str(status)})"
718 print(f
"{self.program} returned {_status(self)}{expect}")
719 print(self
.banner('STDOUT '))
721 print(self
.banner('STDERR '))
724 if (expected_stdout
is not None
725 and not match(actual_stdout
, expected_stdout
)):
726 self
.diff(expected_stdout
, actual_stdout
, 'STDOUT ')
728 print(self
.banner('STDERR '))
731 if (expected_stderr
is not None
732 and not match(actual_stderr
, expected_stderr
)):
733 print(self
.banner('STDOUT '))
735 self
.diff(expected_stderr
, actual_stderr
, 'STDERR ')
738 def start(self
, program
= None,
742 universal_newlines
= None,
745 Starts a program or script for the test environment, handling
748 arguments
= self
.options_arguments(options
, arguments
)
750 return super().start(program
, interpreter
, arguments
,
751 universal_newlines
, **kw
)
752 except KeyboardInterrupt:
754 except Exception as e
:
755 print(self
.banner('STDOUT '))
760 print(self
.banner('STDERR '))
765 cmd_args
= self
.command_args(program
, interpreter
, arguments
)
766 sys
.stderr
.write(f
'Exception trying to execute: {cmd_args}\n')
773 stdout
: str |
None = None,
774 stderr
: str |
None = '',
775 status
: int |
None = 0,
778 """Finish and wait for the process being run.
780 The *popen* argument describes a ``Popen`` object controlling
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.
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
)
805 stdout
: str |
None = None,
806 stderr
: str |
None = '',
807 status
: int |
None = 0,
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.
818 options: Extra options that get prepended to the beginning
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
)
833 match
= kw
.pop('match')
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:
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.
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.
867 sys
.stdout
.write(message
)
868 if not message
.endswith('\n'):
869 sys
.stdout
.write('\n')
871 pass_skips
= os
.environ
.get('TESTCOMMON_PASS_SKIPS')
872 if pass_skips
in [None, 0, '0']:
874 self
.no_result(skip
=2)
876 self
.no_result(skip
=1)
878 # We're under the development directory for this change,
879 # so this is an Aegis invocation; pass the test (exit 0).
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)}")
890 for v
, e
in zip(v_split
, e_split
):
891 # print("%s:%s"%(v,e))
893 print(f
"\n[{v}]\n[{e}]")
895 return f
"Expected:\n{expect}\nGot:\n{value}"
900 # indent-tabs-mode:nil
902 # vim: set expandtab tabstop=4 shiftwidth=4: