1 # -*- coding: utf-8; -*-
4 # Part of Gajja, a Python test double library.
6 # Copyright © 2015–2016 Ben Finney <ben+python@benfinney.id.au>
8 # This is free software: you may copy, modify, and/or distribute this work
9 # under the terms of the GNU General Public License as published by the
10 # Free Software Foundation; version 3 of that license or any later version.
11 # No warranty expressed or implied. See the file ‘LICENSE.GPL-3’ for details.
13 """ Gajja: Fake objects for real tests.
15 The `gajja` library provides a system of Python test double classes
16 for specific system objects:
22 The Korean word 가짜 (*gajja*; IPA ˈkaːt͡ɕ̤a) means “fake thing”.
26 from __future__
import (absolute_import
, unicode_literals
)
30 if sys
.version_info
>= (3, 3):
33 import unittest
.mock
as mock
34 from io
import StringIO
as StringIO
36 import collections
.abc
as collections_abc
37 elif sys
.version_info
>= (3, 0):
38 raise RuntimeError("Python 3 earlier than 3.3 is not supported.")
39 elif sys
.version_info
>= (2, 7):
40 # Python 2 standard library.
41 import __builtin__
as builtins
42 # Third-party backport of Python 3 unittest improvements.
43 import unittest2
as unittest
44 # Third-party mock library.
46 # Python 2 standard library.
47 from StringIO
import StringIO
as BaseStringIO
48 import ConfigParser
as configparser
49 import collections
as collections_abc
51 raise RuntimeError("Python earlier than 2.7 is not supported.")
72 # The ‘pwd’ module is not available on platforms other than Unix.
75 __package__
= str("gajja")
76 __import__(__package__
)
85 # Alias for Python 3 types.
90 def make_unique_slug(testcase
):
91 """ Make a unique slug for the test case. """
92 text
= base64
.b64encode(
93 testcase
.getUniqueString().encode('utf-8')
102 # We don't yet have the StringIO we want. Create it.
104 class StringIO(BaseStringIO
, object):
105 """ StringIO with a context manager. """
110 def __exit__(self
, *args
):
124 def patch_stdout(testcase
):
125 """ Patch `sys.stdout` for the specified test case. """
126 patcher
= mock
.patch
.object(
127 sys
, "stdout", wraps
=StringIO())
129 testcase
.addCleanup(patcher
.stop
)
132 def patch_stderr(testcase
):
133 """ Patch `sys.stderr` for the specified test case. """
134 patcher
= mock
.patch
.object(
135 sys
, "stderr", wraps
=StringIO())
137 testcase
.addCleanup(patcher
.stop
)
140 def patch_signal_signal(testcase
):
141 """ Patch `signal.signal` for the specified test case. """
142 func_patcher
= mock
.patch
.object(signal
, "signal", autospec
=True)
144 testcase
.addCleanup(func_patcher
.stop
)
147 class FakeSystemExit(Exception):
148 """ Fake double for `SystemExit` exception. """
151 EXIT_STATUS_SUCCESS
= 0
152 EXIT_STATUS_FAILURE
= 1
153 EXIT_STATUS_COMMAND_NOT_FOUND
= 127
156 def patch_sys_exit(testcase
):
157 """ Patch `sys.exit` for the specified test case. """
158 func_patcher
= mock
.patch
.object(
159 sys
, "exit", autospec
=True,
160 side_effect
=FakeSystemExit())
162 testcase
.addCleanup(func_patcher
.stop
)
165 def patch_sys_argv(testcase
):
166 """ Patch the `sys.argv` sequence for the test case. """
167 if not hasattr(testcase
, 'progname'):
168 testcase
.progname
= make_unique_slug(testcase
)
169 if not hasattr(testcase
, 'sys_argv'):
170 testcase
.sys_argv
= [testcase
.progname
]
171 patcher
= mock
.patch
.object(
173 new
=list(testcase
.sys_argv
))
175 testcase
.addCleanup(patcher
.stop
)
178 def patch_system_interfaces(testcase
):
179 """ Patch system interfaces that are disruptive to the test runner. """
180 patch_stdout(testcase
)
181 patch_stderr(testcase
)
182 patch_sys_exit(testcase
)
183 patch_sys_argv(testcase
)
186 def patch_time_time(testcase
, values
=None):
187 """ Patch the `time.time` function for the specified test case.
189 :param testcase: The `TestCase` instance for binding to the patch.
190 :param values: An iterable to provide return values.
195 values
= itertools
.count()
197 def generator_fake_time():
201 func_patcher
= mock
.patch
.object(time
, "time", autospec
=True)
203 testcase
.addCleanup(func_patcher
.stop
)
205 time
.time
.side_effect
= generator_fake_time()
208 def patch_os_environ(testcase
):
209 """ Patch the `os.environ` mapping. """
210 if not hasattr(testcase
, 'os_environ'):
211 testcase
.os_environ
= {}
212 patcher
= mock
.patch
.object(os
, "environ", new
=testcase
.os_environ
)
214 testcase
.addCleanup(patcher
.stop
)
217 def patch_os_getpid(testcase
):
218 """ Patch `os.getpid` for the specified test case. """
219 func_patcher
= mock
.patch
.object(os
, "getpid", autospec
=True)
221 testcase
.addCleanup(func_patcher
.stop
)
224 def patch_os_getuid(testcase
):
225 """ Patch the `os.getuid` function. """
226 if not hasattr(testcase
, 'os_getuid_return_value'):
227 testcase
.os_getuid_return_value
= testcase
.getUniqueInteger()
228 func_patcher
= mock
.patch
.object(
229 os
, "getuid", autospec
=True,
230 return_value
=testcase
.os_getuid_return_value
)
232 testcase
.addCleanup(func_patcher
.stop
)
235 PasswdEntry
= collections
.namedtuple(
237 "pw_name pw_passwd pw_uid pw_gid pw_gecos pw_dir pw_shell")
240 def patch_pwd_getpwuid(testcase
):
241 """ Patch the `pwd.getpwuid` function. """
242 if not hasattr(testcase
, 'pwd_getpwuid_return_value'):
243 testcase
.pwd_getpwuid_return_value
= PasswdEntry(
244 pw_name
=make_unique_slug(testcase
),
245 pw_passwd
=make_unique_slug(testcase
),
246 pw_uid
=testcase
.getUniqueInteger(),
247 pw_gid
=testcase
.getUniqueInteger(),
248 pw_gecos
=testcase
.getUniqueString(),
249 pw_dir
=tempfile
.mktemp(),
250 pw_shell
=tempfile
.mktemp())
251 if not isinstance(testcase
.pwd_getpwuid_return_value
, pwd
.struct_passwd
):
252 pwent
= pwd
.struct_passwd(testcase
.pwd_getpwuid_return_value
)
254 pwent
= testcase
.pwd_getpwuid_return_value
255 func_patcher
= mock
.patch
.object(
256 pwd
, "getpwuid", autospec
=True,
259 testcase
.addCleanup(func_patcher
.stop
)
261 if pwd
is NotImplemented:
262 # The ‘pwd’ module is not available on some platforms.
263 del patch_pwd_getpwuid
266 def patch_os_path_exists(testcase
):
267 """ Patch `os.path.exists` behaviour for this test case.
269 When the patched function is called, the registry of
270 `FileDouble` instances for this test case will be used to get
271 the instance for the path specified.
274 orig_os_path_exists
= os
.path
.exists
276 def fake_os_path_exists(path
):
277 registry
= FileDouble
.get_registry_for_testcase(testcase
)
279 file_double
= registry
[path
]
280 result
= file_double
.os_path_exists_scenario
.call_hook()
282 result
= orig_os_path_exists(path
)
285 func_patcher
= mock
.patch
.object(
286 os
.path
, "exists", autospec
=True,
287 side_effect
=fake_os_path_exists
)
289 testcase
.addCleanup(func_patcher
.stop
)
292 def patch_os_access(testcase
):
293 """ Patch `os.access` behaviour for this test case.
295 When the patched function is called, the registry of
296 `FileDouble` instances for this test case will be used to get
297 the instance for the path specified.
300 orig_os_access
= os
.access
302 def fake_os_access(path
, mode
):
303 registry
= FileDouble
.get_registry_for_testcase(testcase
)
305 file_double
= registry
[path
]
306 result
= file_double
.os_access_scenario
.call_hook(mode
)
308 result
= orig_os_access(path
, mode
)
311 func_patcher
= mock
.patch
.object(
312 os
, "access", autospec
=True,
313 side_effect
=fake_os_access
)
315 testcase
.addCleanup(func_patcher
.stop
)
318 StatResult
= collections
.namedtuple(
321 'st_ino', 'st_dev', 'st_nlink',
324 'st_atime', 'st_mtime', 'st_ctime',
328 def patch_os_stat(testcase
):
329 """ Patch `os.stat` behaviour for this test case.
331 When the patched function is called, the registry of
332 `FileDouble` instances for this test case will be used to get
333 the instance for the path specified.
336 orig_os_stat
= os
.stat
338 def fake_os_stat(path
):
339 registry
= FileDouble
.get_registry_for_testcase(testcase
)
341 file_double
= registry
[path
]
342 result
= file_double
.os_stat_scenario
.call_hook()
344 result
= orig_os_stat(path
)
347 func_patcher
= mock
.patch
.object(
348 os
, "stat", autospec
=True,
349 side_effect
=fake_os_stat
)
351 testcase
.addCleanup(func_patcher
.stop
)
354 def patch_os_lstat(testcase
):
355 """ Patch `os.lstat` behaviour for this test case.
357 When the patched function is called, the registry of
358 `FileDouble` instances for this test case will be used to get
359 the instance for the path specified.
362 orig_os_lstat
= os
.lstat
364 def fake_os_lstat(path
):
365 registry
= FileDouble
.get_registry_for_testcase(testcase
)
367 file_double
= registry
[path
]
368 result
= file_double
.os_lstat_scenario
.call_hook()
370 result
= orig_os_lstat(path
)
373 func_patcher
= mock
.patch
.object(
374 os
, "lstat", autospec
=True,
375 side_effect
=fake_os_lstat
)
377 testcase
.addCleanup(func_patcher
.stop
)
380 def patch_builtins_open(testcase
):
381 """ Patch `builtins.open` behaviour for this test case.
383 :param testcase: The `TestCase` instance for binding to the patch.
386 When the patched function is called, the registry of
387 `FileDouble` instances for this test case will be used to get
388 the instance for the path specified.
391 orig_open
= builtins
.open
393 def fake_open(path
, mode
='rt', buffering
=-1):
394 registry
= FileDouble
.get_registry_for_testcase(testcase
)
396 file_double
= registry
[path
]
397 result
= file_double
.builtins_open_scenario
.call_hook(
400 result
= orig_open(path
, mode
, buffering
)
403 mock_open
= mock
.mock_open()
404 mock_open
.side_effect
= fake_open
406 func_patcher
= mock
.patch
.object(builtins
, "open", new
=mock_open
)
408 testcase
.addCleanup(func_patcher
.stop
)
411 def patch_os_unlink(testcase
):
412 """ Patch `os.unlink` behaviour for this test case.
414 When the patched function is called, the registry of
415 `FileDouble` instances for this test case will be used to get
416 the instance for the path specified.
419 orig_os_unlink
= os
.unlink
421 def fake_os_unlink(path
):
422 registry
= FileDouble
.get_registry_for_testcase(testcase
)
424 file_double
= registry
[path
]
425 result
= file_double
.os_unlink_scenario
.call_hook()
427 result
= orig_os_unlink(path
)
430 func_patcher
= mock
.patch
.object(
431 os
, "unlink", autospec
=True,
432 side_effect
=fake_os_unlink
)
434 testcase
.addCleanup(func_patcher
.stop
)
437 def patch_os_rmdir(testcase
):
438 """ Patch `os.rmdir` behaviour for this test case.
440 When the patched function is called, the registry of
441 `FileDouble` instances for this test case will be used to get
442 the instance for the path specified.
445 orig_os_rmdir
= os
.rmdir
447 def fake_os_rmdir(path
):
448 registry
= FileDouble
.get_registry_for_testcase(testcase
)
450 file_double
= registry
[path
]
451 result
= file_double
.os_rmdir_scenario
.call_hook()
453 result
= orig_os_rmdir(path
)
456 func_patcher
= mock
.patch
.object(
457 os
, "rmdir", autospec
=True,
458 side_effect
=fake_os_rmdir
)
460 testcase
.addCleanup(func_patcher
.stop
)
463 def patch_shutil_rmtree(testcase
):
464 """ Patch `shutil.rmtree` behaviour for this test case.
466 When the patched function is called, the registry of
467 `FileDouble` instances for this test case will be used to get
468 the instance for the path specified.
471 orig_shutil_rmtree
= os
.rmdir
473 def fake_shutil_rmtree(path
, ignore_errors
=False, onerror
=None):
474 registry
= FileDouble
.get_registry_for_testcase(testcase
)
476 file_double
= registry
[path
]
477 result
= file_double
.shutil_rmtree_scenario
.call_hook()
479 result
= orig_shutil_rmtree(path
)
482 func_patcher
= mock
.patch
.object(
483 shutil
, "rmtree", autospec
=True,
484 side_effect
=fake_shutil_rmtree
)
486 testcase
.addCleanup(func_patcher
.stop
)
489 def patch_tempfile_mkdtemp(testcase
):
490 """ Patch the `tempfile.mkdtemp` function for this test case. """
491 if not hasattr(testcase
, 'tempfile_mkdtemp_file_double'):
492 testcase
.tempfile_mkdtemp_file_double
= FileDouble(tempfile
.mktemp())
494 double
= testcase
.tempfile_mkdtemp_file_double
495 double
.set_os_unlink_scenario('okay')
496 double
.set_os_rmdir_scenario('okay')
497 double
.register_for_testcase(testcase
)
499 func_patcher
= mock
.patch
.object(tempfile
, "mkdtemp", autospec
=True)
501 testcase
.addCleanup(func_patcher
.stop
)
503 tempfile
.mkdtemp
.return_value
= testcase
.tempfile_mkdtemp_file_double
.path
511 # Python 2 uses IOError.
512 def _ensure_ioerror_args(init_args
, init_kwargs
, errno_value
):
513 result_kwargs
= init_kwargs
514 result_errno
= errno_value
515 result_strerror
= os
.strerror(errno_value
)
516 result_filename
= None
517 if len(init_args
) >= 3:
518 result_errno
= init_args
[0]
519 result_filename
= init_args
[2]
520 if 'errno' in init_kwargs
:
521 result_errno
= init_kwargs
['errno']
522 del result_kwargs
['errno']
523 if 'filename' in init_kwargs
:
524 result_filename
= init_kwargs
['filename']
525 del result_kwargs
['filename']
526 if len(init_args
) >= 2:
527 result_strerror
= init_args
[1]
528 if 'strerror' in init_kwargs
:
529 result_strerror
= init_kwargs
['strerror']
530 del result_kwargs
['strerror']
531 result_args
= (result_errno
, result_strerror
, result_filename
)
532 return (result_args
, result_kwargs
)
534 class FileNotFoundError(IOError):
535 def __init__(self
, *args
, **kwargs
):
536 (args
, kwargs
) = _ensure_ioerror_args(
537 args
, kwargs
, errno_value
=errno
.ENOENT
)
538 super(FileNotFoundError
, self
).__init
__(*args
, **kwargs
)
540 class FileExistsError(IOError):
541 def __init__(self
, *args
, **kwargs
):
542 (args
, kwargs
) = _ensure_ioerror_args(
543 args
, kwargs
, errno_value
=errno
.EEXIST
)
544 super(FileExistsError
, self
).__init
__(*args
, **kwargs
)
546 class PermissionError(IOError):
547 def __init__(self
, *args
, **kwargs
):
548 (args
, kwargs
) = _ensure_ioerror_args(
549 args
, kwargs
, errno_value
=errno
.EPERM
)
550 super(PermissionError
, self
).__init
__(*args
, **kwargs
)
553 def make_fake_file_scenarios(path
=None):
554 """ Make a collection of scenarios for testing with fake files.
556 :path: The filesystem path of the fake file. If not specified,
557 a valid random path will be generated.
558 :return: A collection of scenarios for tests involving input files.
560 The collection is a mapping from scenario name to a dictionary of
566 file_path
= tempfile
.mktemp()
570 fake_file_empty
= StringIO()
571 fake_file_minimal
= StringIO("Lorem ipsum.")
572 fake_file_large
= StringIO("\n".join(
574 for __
in range(1000)))
576 default_scenario_params
= {
577 'open_scenario_name': 'okay',
578 'file_double_params': dict(
579 path
=file_path
, fake_file
=fake_file_minimal
),
585 'open_scenario_name': 'nonexist',
588 'open_scenario_name': 'exist_error',
590 'error-read-denied': {
591 'open_scenario_name': 'read_denied',
594 'file_double_params': dict(
595 path
=file_path
, fake_file
=fake_file_empty
),
598 'file_double_params': dict(
599 path
=file_path
, fake_file
=fake_file_empty
),
602 'file_double_params': dict(
603 path
=file_path
, fake_file
=fake_file_minimal
),
606 'file_double_params': dict(
607 path
=file_path
, fake_file
=fake_file_large
),
611 for (name
, scenario
) in scenarios
.items():
612 params
= default_scenario_params
.copy()
613 params
.update(scenario
)
614 scenario
.update(params
)
615 scenario
['file_double'] = FileDouble(**scenario
['file_double_params'])
616 scenario
['file_double'].set_open_scenario(params
['open_scenario_name'])
617 scenario
['fake_file_scenario_name'] = name
622 def get_file_doubles_from_fake_file_scenarios(scenarios
):
623 """ Get the `FileDouble` instances from fake file scenarios.
625 :param scenarios: Collection of fake file scenarios.
626 :return: Collection of `FileDouble` instances.
630 scenario
['file_double']
631 for scenario
in scenarios
632 if scenario
['file_double'] is not None)
637 def setup_file_double_behaviour(testcase
, doubles
=None):
638 """ Set up file double instances and behaviour.
640 :param testcase: The `TestCase` instance to modify.
641 :param doubles: Collection of `FileDouble` instances.
644 If `doubles` is ``None``, a default collection will be made
645 from the result of `make_fake_file_scenarios` result.
649 scenarios
= make_fake_file_scenarios()
650 doubles
= get_file_doubles_from_fake_file_scenarios(
653 for file_double
in doubles
:
654 file_double
.register_for_testcase(testcase
)
656 patch_builtins_open(testcase
)
659 def setup_fake_file_fixtures(testcase
):
660 """ Set up fixtures for fake file doubles.
662 :param testcase: The `TestCase` instance to modify.
666 scenarios
= make_fake_file_scenarios()
667 testcase
.fake_file_scenarios
= scenarios
669 file_doubles
= get_file_doubles_from_fake_file_scenarios(
671 setup_file_double_behaviour(testcase
, file_doubles
)
674 def set_fake_file_scenario(testcase
, name
):
675 """ Set the named fake file scenario for the test case. """
676 scenario
= testcase
.fake_file_scenarios
[name
]
677 testcase
.fake_file_scenario
= scenario
678 testcase
.file_double
= scenario
['file_double']
679 testcase
.file_double
.register_for_testcase(testcase
)
682 class TestDoubleFunctionScenario
:
683 """ Scenario for fake behaviour of a specific function. """
685 def __init__(self
, scenario_name
, double
):
686 self
.scenario_name
= scenario_name
689 self
.call_hook
= getattr(
690 self
, "_hook_{name}".format(name
=self
.scenario_name
))
694 "<{class_name} instance: {id}"
696 " call_hook name: {hook_name!r}"
697 " double: {double!r}"
699 class_name
=self
.__class
__.__name
__, id=id(self
),
700 name
=self
.scenario_name
, double
=self
.double
,
701 hook_name
=self
.call_hook
.__name
__)
704 def __eq__(self
, other
):
706 if not self
.scenario_name
== other
.scenario_name
:
708 if not self
.double
== other
.double
:
710 if not self
.call_hook
.__name
__ == other
.call_hook
.__name
__:
714 def __ne__(self
, other
):
715 result
= not self
.__eq
__(other
)
719 class os_path_exists_scenario(TestDoubleFunctionScenario
):
720 """ Scenario for `os.path.exists` behaviour. """
722 def _hook_exist(self
):
725 def _hook_not_exist(self
):
729 class os_access_scenario(TestDoubleFunctionScenario
):
730 """ Scenario for `os.access` behaviour. """
732 def _hook_okay(self
, mode
):
735 def _hook_not_exist(self
, mode
):
738 def _hook_read_only(self
, mode
):
739 if mode
& (os
.W_OK | os
.X_OK
):
745 def _hook_denied(self
, mode
):
746 if mode
& (os
.R_OK | os
.W_OK | os
.X_OK
):
753 class os_stat_scenario(TestDoubleFunctionScenario
):
754 """ Scenario for `os.stat` behaviour. """
756 def _hook_okay(self
):
757 return self
.double
.stat_result
759 def _hook_notfound_error(self
):
760 raise FileNotFoundError(
762 "No such file or directory: {path!r}".format(
763 path
=self
.double
.path
))
765 def _hook_denied_error(self
):
766 raise PermissionError(
771 class os_lstat_scenario(os_stat_scenario
):
772 """ Scenario for `os.lstat` behaviour. """
775 class os_unlink_scenario(TestDoubleFunctionScenario
):
776 """ Scenario for `os.unlink` behaviour. """
778 def _hook_okay(self
):
781 def _hook_nonexist(self
):
782 error
= FileNotFoundError(
784 "No such file or directory: {path!r}".format(
785 path
=self
.double
.path
))
788 def _hook_denied(self
):
789 error
= PermissionError(
795 class os_rmdir_scenario(TestDoubleFunctionScenario
):
796 """ Scenario for `os.rmdir` behaviour. """
798 def _hook_okay(self
):
801 def _hook_nonexist(self
):
802 error
= FileNotFoundError(
804 "No such file or directory: {path!r}".format(
805 path
=self
.double
.path
))
808 def _hook_denied(self
):
809 error
= PermissionError(
815 class shutil_rmtree_scenario(TestDoubleFunctionScenario
):
816 """ Scenario for `shutil.rmtree` behaviour. """
818 def _hook_okay(self
):
821 def _hook_nonexist(self
):
822 error
= FileNotFoundError(
824 "No such file or directory: {path!r}".format(
825 path
=self
.double
.path
))
828 def _hook_denied(self
):
829 error
= PermissionError(
835 class builtins_open_scenario(TestDoubleFunctionScenario
):
836 """ Scenario for `builtins.open` behaviour. """
838 def _hook_okay(self
, mode
, buffering
):
839 result
= self
.double
.fake_file
842 def _hook_nonexist(self
, mode
, buffering
):
843 if mode
.startswith('r'):
844 error
= FileNotFoundError(
846 "No such file or directory: {path!r}".format(
847 path
=self
.double
.path
))
849 result
= self
.double
.fake_file
852 def _hook_exist_error(self
, mode
, buffering
):
853 if mode
.startswith('w') or mode
.startswith('a'):
854 error
= FileExistsError(
856 "File already exists: {path!r}".format(
857 path
=self
.double
.path
))
859 result
= self
.double
.fake_file
862 def _hook_read_denied(self
, mode
, buffering
):
863 if mode
.startswith('r'):
864 error
= PermissionError(
866 "Read denied on {path!r}".format(
867 path
=self
.double
.path
))
869 result
= self
.double
.fake_file
872 def _hook_write_denied(self
, mode
, buffering
):
873 if mode
.startswith('w') or mode
.startswith('a'):
874 error
= PermissionError(
876 "Write denied on {path!r}".format(
877 path
=self
.double
.path
))
879 result
= self
.double
.fake_file
883 class TestDoubleWithRegistry
:
884 """ Abstract base class for a test double with a test case registry. """
886 registry_class
= NotImplemented
887 registries
= NotImplemented
889 function_scenario_params_by_class
= NotImplemented
891 def __new__(cls
, *args
, **kwargs
):
892 superclass
= super(TestDoubleWithRegistry
, cls
)
893 if superclass
.__new
__ is object.__new
__:
894 # The ‘object’ implementation complains about extra arguments.
895 instance
= superclass
.__new
__(cls
)
897 instance
= superclass
.__new
__(cls
, *args
, **kwargs
)
898 instance
.make_set_scenario_methods()
902 def __init__(self
, *args
, **kwargs
):
903 super(TestDoubleWithRegistry
, self
).__init
__(*args
, **kwargs
)
904 self
._set
_method
_per
_scenario
()
906 def _make_set_scenario_method(self
, scenario_class
, params
):
907 def method(self
, name
):
908 scenario
= scenario_class(name
, double
=self
)
909 setattr(self
, scenario_class
.__name
__, scenario
)
911 """ Set the scenario for `{name}` behaviour. """
912 ).format(name
=scenario_class
.__name
__)
913 method
.__name
__ = str(params
['set_scenario_method_name'])
916 def make_set_scenario_methods(self
):
917 """ Make `set_<scenario_class_name>` methods on this class. """
918 for (function_scenario_class
, function_scenario_params
) in (
919 self
.function_scenario_params_by_class
.items()):
920 method
= self
._make
_set
_scenario
_method
(
921 function_scenario_class
, function_scenario_params
)
922 setattr(self
.__class
__, method
.__name
__, method
)
923 function_scenario_params
['set_scenario_method'] = method
925 def _set_method_per_scenario(self
):
926 """ Set the method to be called for each scenario. """
927 for function_scenario_params
in (
928 self
.function_scenario_params_by_class
.values()):
929 function_scenario_params
['set_scenario_method'](
930 self
, function_scenario_params
['default_scenario_name'])
933 def get_registry_for_testcase(cls
, testcase
):
934 """ Get the FileDouble registry for the specified test case. """
935 # Key in a dict must be hashable.
936 key
= (testcase
.__class
__, id(testcase
))
937 registry
= cls
.registries
.setdefault(key
, cls
.registry_class())
940 def get_registry_key(self
):
941 """ Get the registry key for this double. """
942 raise NotImplementedError
944 def register_for_testcase(self
, testcase
):
945 """ Add this instance to registry for the specified testcase. """
946 registry
= self
.get_registry_for_testcase(testcase
)
947 key
= self
.get_registry_key()
949 unregister_func
= functools
.partial(
950 self
.unregister_for_testcase
, testcase
)
951 testcase
.addCleanup(unregister_func
)
953 def unregister_for_testcase(self
, testcase
):
954 """ Remove this instance from registry for the specified testcase. """
955 registry
= self
.get_registry_for_testcase(testcase
)
956 key
= self
.get_registry_key()
961 def copy_fake_file(fake_file
):
962 """ Make a copy of the StringIO instance. """
963 fake_file_type
= StringIO
965 if fake_file
is not None:
966 fake_file_type
= type(fake_file
)
967 content
= fake_file
.getvalue()
968 assert issubclass(fake_file_type
, object)
969 result
= fake_file_type(content
)
970 if hasattr(fake_file
, 'encoding'):
971 if not hasattr(result
, 'encoding'):
972 result
.encoding
= fake_file
.encoding
976 class FileDouble(TestDoubleWithRegistry
):
977 """ A testing double for a file. """
979 registry_class
= dict
982 function_scenario_params_by_class
= {
983 os_path_exists_scenario
: {
984 'default_scenario_name': 'not_exist',
985 'set_scenario_method_name': 'set_os_path_exists_scenario',
987 os_access_scenario
: {
988 'default_scenario_name': 'okay',
989 'set_scenario_method_name': 'set_os_access_scenario',
992 'default_scenario_name': 'okay',
993 'set_scenario_method_name': 'set_os_stat_scenario',
996 'default_scenario_name': 'okay',
997 'set_scenario_method_name': 'set_os_lstat_scenario',
999 builtins_open_scenario
: {
1000 'default_scenario_name': 'okay',
1001 'set_scenario_method_name': 'set_open_scenario',
1003 os_unlink_scenario
: {
1004 'default_scenario_name': 'okay',
1005 'set_scenario_method_name': 'set_os_unlink_scenario',
1007 os_rmdir_scenario
: {
1008 'default_scenario_name': 'okay',
1009 'set_scenario_method_name': 'set_os_rmdir_scenario',
1011 shutil_rmtree_scenario
: {
1012 'default_scenario_name': 'okay',
1013 'set_scenario_method_name': 'set_shutil_rmtree_scenario',
1017 def __init__(self
, path
=None, fake_file
=None, *args
, **kwargs
):
1019 self
.fake_file
= copy_fake_file(fake_file
)
1020 self
.fake_file
.name
= path
1022 self
._set
_stat
_result
()
1024 super(FileDouble
, self
).__init
__(*args
, **kwargs
)
1026 def _set_stat_result(self
):
1027 """ Set the `os.stat` result for this file. """
1028 size
= len(self
.fake_file
.getvalue())
1029 self
.stat_result
= StatResult(
1031 st_ino
=None, st_dev
=None, st_nlink
=None,
1034 st_atime
=None, st_mtime
=None, st_ctime
=None,
1038 text
= "FileDouble(path={path!r}, fake_file={fake_file!r})".format(
1039 path
=self
.path
, fake_file
=self
.fake_file
)
1042 def get_registry_key(self
):
1043 """ Get the registry key for this double. """
1048 class os_popen_scenario(TestDoubleFunctionScenario
):
1049 """ Scenario for `os.popen` behaviour. """
1051 stream_name_by_mode
= {
1056 def _hook_success(self
, argv
, mode
, buffering
):
1057 stream_name
= self
.stream_name_by_mode
[mode
]
1058 stream_double
= getattr(
1059 self
.double
, stream_name
+ '_double')
1060 result
= stream_double
.fake_file
1063 def _hook_failure(self
, argv
, mode
, buffering
):
1067 def _hook_not_found(self
, argv
, mode
, buffering
):
1072 class os_waitpid_scenario(TestDoubleFunctionScenario
):
1073 """ Scenario for `os.waitpid` behaviour. """
1075 def _hook_success(self
, pid
, options
):
1076 result
= (pid
, EXIT_STATUS_SUCCESS
)
1079 def _hook_failure(self
, pid
, options
):
1080 result
= (pid
, EXIT_STATUS_FAILURE
)
1083 def _hook_not_found(self
, pid
, options
):
1084 error
= OSError(errno
.ECHILD
)
1088 class os_system_scenario(TestDoubleFunctionScenario
):
1089 """ Scenario for `os.system` behaviour. """
1091 def _hook_success(self
, command
):
1092 result
= EXIT_STATUS_SUCCESS
1095 def _hook_failure(self
, command
):
1096 result
= EXIT_STATUS_FAILURE
1099 def _hook_not_found(self
, command
):
1100 result
= EXIT_STATUS_COMMAND_NOT_FOUND
1104 class os_spawnv_scenario(TestDoubleFunctionScenario
):
1105 """ Scenario for `os.spawnv` behaviour. """
1107 def _hook_success(self
, mode
, file, args
):
1108 result
= EXIT_STATUS_SUCCESS
1111 def _hook_failure(self
, mode
, file, args
):
1112 result
= EXIT_STATUS_FAILURE
1115 def _hook_not_found(self
, mode
, file, args
):
1116 result
= EXIT_STATUS_COMMAND_NOT_FOUND
1125 """ A testing double for `subprocess.Popen`. """
1127 def __init__(self
, args
, *posargs
, **kwargs
):
1132 self
.returncode
= None
1134 if kwargs
.get('shell', False):
1135 self
.argv
= shlex
.split(args
)
1137 # The paramter is already a sequence of command-line arguments.
1140 def set_streams(self
, subprocess_double
, popen_kwargs
):
1141 """ Set the streams on the `PopenDouble`.
1143 :param subprocess_double: The `SubprocessDouble` from
1144 which to get existing stream doubles.
1145 :param popen_kwargs: The keyword arguments to the
1146 `subprocess.Popen` call.
1150 for stream_name
in (
1151 name
for name
in ['stdin', 'stdout', 'stderr']
1152 if name
in popen_kwargs
):
1153 stream_spec
= popen_kwargs
[stream_name
]
1154 if stream_spec
is subprocess
.PIPE
:
1155 stream_double
= getattr(
1157 "{name}_double".format(name
=stream_name
))
1158 stream_file
= stream_double
.fake_file
1159 elif stream_spec
is subprocess
.STDOUT
:
1160 stream_file
= subprocess_double
.stdout_double
.fake_file
1162 stream_file
= stream_spec
1163 setattr(self
, stream_name
, stream_file
)
1166 """ Wait for subprocess to terminate. """
1167 return self
.returncode
1170 class subprocess_popen_scenario(TestDoubleFunctionScenario
):
1171 """ Scenario for `subprocess.Popen` behaviour. """
1173 def _hook_success(self
, testcase
, args
, *posargs
, **kwargs
):
1174 double
= self
.double
.popen_double
1175 double
.set_streams(self
.double
, kwargs
)
1179 def patch_subprocess_popen(testcase
):
1180 """ Patch `subprocess.Popen` constructor for this test case.
1182 :param testcase: The `TestCase` instance to modify.
1185 When the patched function is called, the registry of
1186 `SubprocessDouble` instances for this test case will be used
1187 to get the instance for the program path specified.
1190 orig_subprocess_popen
= subprocess
.Popen
1192 def fake_subprocess_popen(args
, *posargs
, **kwargs
):
1193 if kwargs
.get('shell', False):
1194 argv
= shlex
.split(args
)
1197 registry
= SubprocessDouble
.get_registry_for_testcase(testcase
)
1198 if argv
in registry
:
1199 subprocess_double
= registry
[argv
]
1200 result
= subprocess_double
.subprocess_popen_scenario
.call_hook(
1201 testcase
, args
, *posargs
, **kwargs
)
1203 result
= orig_subprocess_popen(args
, *posargs
, **kwargs
)
1206 func_patcher
= mock
.patch
.object(
1207 subprocess
, "Popen", autospec
=True,
1208 side_effect
=fake_subprocess_popen
)
1209 func_patcher
.start()
1210 testcase
.addCleanup(func_patcher
.stop
)
1213 class subprocess_call_scenario(TestDoubleFunctionScenario
):
1214 """ Scenario for `subprocess.call` behaviour. """
1216 def _hook_success(self
, command
):
1217 result
= EXIT_STATUS_SUCCESS
1220 def _hook_failure(self
, command
):
1221 result
= EXIT_STATUS_FAILURE
1224 def _hook_not_found(self
, command
):
1225 result
= EXIT_STATUS_COMMAND_NOT_FOUND
1229 def patch_subprocess_call(testcase
):
1230 """ Patch `subprocess.call` function for this test case.
1232 :param testcase: The `TestCase` instance to modify.
1235 When the patched function is called, the registry of
1236 `SubprocessDouble` instances for this test case will be used
1237 to get the instance for the program path specified.
1240 orig_subprocess_call
= subprocess
.call
1242 def fake_subprocess_call(command
, *posargs
, **kwargs
):
1243 if kwargs
.get('shell', False):
1244 command_argv
= shlex
.split(command
)
1246 command_argv
= command
1247 registry
= SubprocessDouble
.get_registry_for_testcase(testcase
)
1248 if command_argv
in registry
:
1249 subprocess_double
= registry
[command_argv
]
1250 result
= subprocess_double
.subprocess_call_scenario
.call_hook(
1253 result
= orig_subprocess_call(command
, *posargs
, **kwargs
)
1256 func_patcher
= mock
.patch
.object(
1257 subprocess
, "call", autospec
=True,
1258 side_effect
=fake_subprocess_call
)
1259 func_patcher
.start()
1260 testcase
.addCleanup(func_patcher
.stop
)
1263 class subprocess_check_call_scenario(TestDoubleFunctionScenario
):
1264 """ Scenario for `subprocess.check_call` behaviour. """
1266 def _hook_success(self
, command
):
1269 def _hook_failure(self
, command
):
1270 result
= EXIT_STATUS_FAILURE
1271 error
= subprocess
.CalledProcessError(result
, command
)
1274 def _hook_not_found(self
, command
):
1275 result
= EXIT_STATUS_COMMAND_NOT_FOUND
1276 error
= subprocess
.CalledProcessError(result
, command
)
1280 def patch_subprocess_check_call(testcase
):
1281 """ Patch `subprocess.check_call` function for this test case.
1283 :param testcase: The `TestCase` instance to modify.
1286 When the patched function is called, the registry of
1287 `SubprocessDouble` instances for this test case will be used
1288 to get the instance for the program path specified.
1291 orig_subprocess_check_call
= subprocess
.check_call
1293 def fake_subprocess_check_call(command
, *posargs
, **kwargs
):
1294 if kwargs
.get('shell', False):
1295 command_argv
= shlex
.split(command
)
1297 command_argv
= command
1298 registry
= SubprocessDouble
.get_registry_for_testcase(testcase
)
1299 if command_argv
in registry
:
1300 subprocess_double
= registry
[command_argv
]
1301 scenario
= subprocess_double
.subprocess_check_call_scenario
1302 result
= scenario
.call_hook(command
)
1304 result
= orig_subprocess_check_call(command
, *posargs
, **kwargs
)
1307 func_patcher
= mock
.patch
.object(
1308 subprocess
, "check_call", autospec
=True,
1309 side_effect
=fake_subprocess_check_call
)
1310 func_patcher
.start()
1311 testcase
.addCleanup(func_patcher
.stop
)
1314 class SubprocessDoubleRegistry(collections_abc
.MutableMapping
):
1315 """ Registry of `SubprocessDouble` instances by `argv`. """
1317 def __init__(self
, *args
, **kwargs
):
1320 if isinstance(args
[0], collections_abc
.Mapping
):
1321 items
= args
[0].items()
1322 if isinstance(args
[0], collections_abc
.Iterable
):
1324 self
._mapping
= dict(items
)
1327 text
= "<{class_name} object: {mapping}>".format(
1328 class_name
=self
.__class
__.__name
__, mapping
=self
._mapping
)
1331 def _match_argv(self
, argv
):
1332 """ Match the specified `argv` with our registered keys. """
1334 if not isinstance(argv
, collections_abc
.Sequence
):
1336 candidates
= iter(self
._mapping
)
1337 while match
is None:
1339 candidate
= next(candidates
)
1340 except StopIteration:
1343 if candidate
== argv
:
1346 word_iter
= enumerate(candidate
)
1347 while found
is None:
1349 (word_index
, candidate_word
) = next(word_iter
)
1350 except StopIteration:
1352 if candidate_word
is ARG_MORE
:
1353 # Candiate matches any remaining words. We have a match.
1355 elif word_index
> len(argv
):
1356 # Candidate is too long for the specified argv.
1358 elif candidate_word
is ARG_ANY
:
1359 # Candidate matches any word at this position.
1361 elif candidate_word
== argv
[word_index
]:
1362 # Candidate matches the word at this position.
1365 # This candidate does not match.
1368 # Reached the end of the candidate without a mismatch.
1374 def __getitem__(self
, key
):
1375 match
= self
._match
_argv
(key
)
1378 result
= self
._mapping
[match
]
1381 def __setitem__(self
, key
, value
):
1384 self
._mapping
[key
] = value
1386 def __delitem__(self
, key
):
1387 match
= self
._match
_argv
(key
)
1388 if match
is not None:
1389 del self
._mapping
[match
]
1392 return self
._mapping
.__iter
__()
1395 return self
._mapping
.__len
__()
1398 class SubprocessDouble(TestDoubleWithRegistry
):
1399 """ A testing double for a subprocess. """
1401 registry_class
= SubprocessDoubleRegistry
1404 double_by_pid
= weakref
.WeakValueDictionary()
1406 function_scenario_params_by_class
= {
1407 subprocess_popen_scenario
: {
1408 'default_scenario_name': 'success',
1409 'set_scenario_method_name': 'set_subprocess_popen_scenario',
1411 subprocess_call_scenario
: {
1412 'default_scenario_name': 'success',
1413 'set_scenario_method_name': 'set_subprocess_call_scenario',
1415 subprocess_check_call_scenario
: {
1416 'default_scenario_name': 'success',
1417 'set_scenario_method_name':
1418 'set_subprocess_check_call_scenario',
1420 os_popen_scenario
: {
1421 'default_scenario_name': 'success',
1422 'set_scenario_method_name': 'set_os_popen_scenario',
1424 os_waitpid_scenario
: {
1425 'default_scenario_name': 'success',
1426 'set_scenario_method_name': 'set_os_waitpid_scenario',
1428 os_system_scenario
: {
1429 'default_scenario_name': 'success',
1430 'set_scenario_method_name': 'set_os_system_scenario',
1432 os_spawnv_scenario
: {
1433 'default_scenario_name': 'success',
1434 'set_scenario_method_name': 'set_os_spawnv_scenario',
1438 def __init__(self
, path
=None, argv
=None, *args
, **kwargs
):
1440 path
= tempfile
.mktemp()
1444 command_name
= os
.path
.basename(path
)
1445 argv
= [command_name
]
1448 self
.pid
= self
._make
_pid
()
1449 self
._register
_by
_pid
()
1451 self
.set_popen_double()
1453 stream_class
= SubprocessDouble
.stream_class
1454 for stream_name
in ['stdin', 'stdout', 'stderr']:
1455 fake_file
= stream_class()
1456 file_double
= FileDouble(fake_file
=fake_file
)
1457 stream_double_name
= '{name}_double'.format(name
=stream_name
)
1458 setattr(self
, stream_double_name
, file_double
)
1460 super(SubprocessDouble
, self
).__init
__(*args
, **kwargs
)
1462 def set_popen_double(self
):
1463 """ Set the `PopenDouble` for this instance. """
1464 double
= PopenDouble(self
.argv
)
1465 double
.pid
= self
.pid
1467 self
.popen_double
= double
1471 "<SubprocessDouble instance: {id}"
1474 " stdin_double: {stdin_double!r}"
1475 " stdout_double: {stdout_double!r}"
1476 " stderr_double: {stderr_double!r}"
1479 path
=self
.path
, argv
=self
.argv
,
1480 stdin_double
=self
.stdin_double
,
1481 stdout_double
=self
.stdout_double
,
1482 stderr_double
=self
.stderr_double
)
1487 """ Make a unique PID for a subprocess. """
1488 for pid
in itertools
.count(1):
1491 def _register_by_pid(self
):
1492 """ Register this subprocess by its PID. """
1493 self
.__class
__.double_by_pid
[self
.pid
] = self
1495 def get_registry_key(self
):
1496 """ Get the registry key for this double. """
1497 result
= tuple(self
.argv
)
1500 stream_class
= io
.BytesIO
1501 stream_encoding
= "utf-8"
1503 def set_stdin_content(self
, text
, bytes_encoding
=stream_encoding
):
1504 """ Set the content of the `stdin` stream for this double. """
1505 content
= text
.encode(bytes_encoding
)
1506 fake_file
= self
.stream_class(content
)
1507 self
.stdin_double
.fake_file
= fake_file
1509 def set_stdout_content(self
, text
, bytes_encoding
=stream_encoding
):
1510 """ Set the content of the `stdout` stream for this double. """
1511 content
= text
.encode(bytes_encoding
)
1512 fake_file
= self
.stream_class(content
)
1513 self
.stdout_double
.fake_file
= fake_file
1515 def set_stderr_content(self
, text
, bytes_encoding
=stream_encoding
):
1516 """ Set the content of the `stderr` stream for this double. """
1517 content
= text
.encode(bytes_encoding
)
1518 fake_file
= self
.stream_class(content
)
1519 self
.stderr_double
.fake_file
= fake_file
1522 def make_fake_subprocess_scenarios(path
=None):
1523 """ Make a collection of scenarios for testing with fake files.
1525 :path: The filesystem path of the fake program. If not specified,
1526 a valid random path will be generated.
1527 :return: A collection of scenarios for tests involving subprocesses.
1529 The collection is a mapping from scenario name to a dictionary of
1530 scenario attributes.
1534 file_path
= tempfile
.mktemp()
1538 default_scenario_params
= {
1539 'return_value': EXIT_STATUS_SUCCESS
,
1540 'program_path': file_path
,
1541 'argv_after_command_name': [],
1547 'return_value': EXIT_STATUS_COMMAND_NOT_FOUND
,
1551 for (name
, scenario
) in scenarios
.items():
1552 params
= default_scenario_params
.copy()
1553 params
.update(scenario
)
1554 scenario
.update(params
)
1555 program_path
= params
['program_path']
1556 program_name
= os
.path
.basename(params
['program_path'])
1557 argv
= [program_name
]
1558 argv
.extend(params
['argv_after_command_name'])
1559 subprocess_double_params
= dict(
1563 subprocess_double
= SubprocessDouble(**subprocess_double_params
)
1564 scenario
['subprocess_double'] = subprocess_double
1565 scenario
['fake_file_scenario_name'] = name
1570 def get_subprocess_doubles_from_fake_subprocess_scenarios(scenarios
):
1571 """ Get the `SubprocessDouble` instances from fake subprocess scenarios.
1573 :param scenarios: Collection of fake subprocess scenarios.
1574 :return: Collection of `SubprocessDouble` instances.
1578 scenario
['subprocess_double']
1579 for scenario
in scenarios
1580 if scenario
['subprocess_double'] is not None)
1585 def setup_subprocess_double_behaviour(testcase
, doubles
=None):
1586 """ Set up subprocess double instances and behaviour.
1588 :param testcase: The `TestCase` instance to modify.
1589 :param doubles: Collection of `SubprocessDouble` instances.
1592 If `doubles` is ``None``, a default collection will be made
1593 from the return value of `make_fake_subprocess_scenarios`.
1597 scenarios
= make_fake_subprocess_scenarios()
1598 doubles
= get_subprocess_doubles_from_fake_subprocess_scenarios(
1601 for double
in doubles
:
1602 double
.register_for_testcase(testcase
)
1605 def setup_fake_subprocess_fixtures(testcase
):
1606 """ Set up fixtures for fake subprocess doubles.
1608 :param testcase: The `TestCase` instance to modify.
1612 scenarios
= make_fake_subprocess_scenarios()
1613 testcase
.fake_subprocess_scenarios
= scenarios
1615 doubles
= get_subprocess_doubles_from_fake_subprocess_scenarios(
1617 setup_subprocess_double_behaviour(testcase
, doubles
)
1620 def patch_os_popen(testcase
):
1621 """ Patch `os.popen` behaviour for this test case.
1623 :param testcase: The `TestCase` instance to modify.
1626 When the patched function is called, the registry of
1627 `SubprocessDouble` instances for this test case will be used
1628 to get the instance for the program path specified.
1631 orig_os_popen
= os
.popen
1633 def fake_os_popen(cmd
, mode
='r', buffering
=-1):
1634 registry
= SubprocessDouble
.get_registry_for_testcase(testcase
)
1635 if isinstance(cmd
, basestring
):
1636 command_argv
= shlex
.split(cmd
)
1639 if command_argv
in registry
:
1640 subprocess_double
= registry
[command_argv
]
1641 result
= subprocess_double
.os_popen_scenario
.call_hook(
1642 command_argv
, mode
, buffering
)
1644 result
= orig_os_popen(cmd
, mode
, buffering
)
1647 func_patcher
= mock
.patch
.object(
1648 os
, "popen", autospec
=True,
1649 side_effect
=fake_os_popen
)
1650 func_patcher
.start()
1651 testcase
.addCleanup(func_patcher
.stop
)
1654 def patch_os_waitpid(testcase
):
1655 """ Patch `os.waitpid` behaviour for this test case.
1657 :param testcase: The `TestCase` instance to modify.
1660 When the patched function is called, the registry of
1661 `SubprocessDouble` instances for this test case will be used
1662 to get the instance for the program path specified.
1665 orig_os_waitpid
= os
.waitpid
1667 def fake_os_waitpid(pid
, options
):
1668 registry
= SubprocessDouble
.double_by_pid
1670 subprocess_double
= registry
[pid
]
1671 result
= subprocess_double
.os_waitpid_scenario
.call_hook(
1674 result
= orig_os_waitpid(pid
, options
)
1677 func_patcher
= mock
.patch
.object(
1678 os
, "waitpid", autospec
=True,
1679 side_effect
=fake_os_waitpid
)
1680 func_patcher
.start()
1681 testcase
.addCleanup(func_patcher
.stop
)
1684 def patch_os_system(testcase
):
1685 """ Patch `os.system` behaviour for this test case.
1687 :param testcase: The `TestCase` instance to modify.
1690 When the patched function is called, the registry of
1691 `SubprocessDouble` instances for this test case will be used
1692 to get the instance for the program path specified.
1695 orig_os_system
= os
.system
1697 def fake_os_system(command
):
1698 registry
= SubprocessDouble
.get_registry_for_testcase(testcase
)
1699 command_argv
= shlex
.split(command
)
1700 if command_argv
in registry
:
1701 subprocess_double
= registry
[command_argv
]
1702 result
= subprocess_double
.os_system_scenario
.call_hook(
1705 result
= orig_os_system(command
)
1708 func_patcher
= mock
.patch
.object(
1709 os
, "system", autospec
=True,
1710 side_effect
=fake_os_system
)
1711 func_patcher
.start()
1712 testcase
.addCleanup(func_patcher
.stop
)
1715 def patch_os_spawnv(testcase
):
1716 """ Patch `os.spawnv` behaviour for this test case.
1718 :param testcase: The `TestCase` instance to modify.
1721 When the patched function is called, the registry of
1722 `SubprocessDouble` instances for this test case will be used
1723 to get the instance for the program path specified.
1726 orig_os_spawnv
= os
.spawnv
1728 def fake_os_spawnv(mode
, file, args
):
1729 registry
= SubprocessDouble
.get_registry_for_testcase(testcase
)
1730 registry_key
= tuple(args
)
1731 if registry_key
in registry
:
1732 subprocess_double
= registry
[registry_key
]
1733 result
= subprocess_double
.os_spawnv_scenario
.call_hook(
1736 result
= orig_os_spawnv(mode
, file, args
)
1739 func_patcher
= mock
.patch
.object(
1740 os
, "spawnv", autospec
=True,
1741 side_effect
=fake_os_spawnv
)
1742 func_patcher
.start()
1743 testcase
.addCleanup(func_patcher
.stop
)
1750 # vim: fileencoding=utf-8 filetype=python :