PageRenderTime 58ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/betse_test/func/fixture/clier.py

https://gitlab.com/betse/betse
Python | 186 lines | 56 code | 8 blank | 122 comment | 4 complexity | 8f9b2ec40211613d90190f8133c1f4e8 MD5 | raw file
  1. #!/usr/bin/env python3
  2. # --------------------( LICENSE )--------------------
  3. # Copyright 2014-2022 by Alexis Pietak & Cecil Curry.
  4. # See "LICENSE" for further details.
  5. '''
  6. Fixtures and fixture classes efficiently exercising a single subcommand of the
  7. BETSE CLI in the active Python interpreter.
  8. '''
  9. #FIXME: We've added a new "--dist-dir" option to our "freeze_*" family of
  10. #setuptools subcommands. Now, let's add support for fixtures running these
  11. #subcommands. Is there any means of programmatically running setuptools
  12. #subcommands in the current Python process? We suspect not, sadly.
  13. #FIXME: Add seemless support for exercising all CLI-specific functional tests
  14. #leveraging the "betse_cli" fixture against a frozen rather than unfrozen
  15. #version of BETSE's CLI. Do *NOT* parametrize this fixture to unconditionally
  16. #run each such test against both frozen and unfrozen versions of BETSE's CLI, as
  17. #doing so would substantially increase space and time complexity with little to
  18. #no tangible gain. Instead:
  19. #
  20. #* Define a new "--is-frozen" command-line option specific to our setuptools
  21. # "test" subcommand, defaulting to disabled.
  22. #* When this option is *NOT* passed:
  23. # * One and only one PyInstaller test should be run -- say, test_pyi_try().
  24. # This test is intended only to provide a coarse-grained sanity check of
  25. # whether or not:
  26. # * BETSE is actually freezable under PyInstaller.
  27. # * The resulting executable is successfully simulatable with the default
  28. # simulation configuration via "betse try".
  29. # * Consequently, this test should (in order):
  30. # 1. Freeze BETSE in one-dir mode (which requires no additional compression
  31. # and decompression steps and hence is slightly more time efficient for
  32. # our simple purposes) into a temporary subdirectory.
  33. # 2. Run "betse try" against this frozen executable.
  34. # * All other CLI tests should be run as is against the unfrozen version of
  35. # BETSE importable by the active Python interpreter.
  36. #* When this option is passed:
  37. # * The aforementioned test_pyi_try() test should *NOT* be run, as doing so
  38. # would be wholly redundant.
  39. # * BETSE should be frozen in one-dir mode, as discussed above.
  40. # * All other CLI tests should be run against this frozen executable rather
  41. # than the unfrozen, importable version of BETSE.
  42. #
  43. #Hence, the "--is-frozen" CLI option serves as a high-level switch fundamentally
  44. #changing testing behaviour. The intention, of course, is that this option would
  45. #only be passed immediately before producing an official new frozen version of
  46. #BETSE. This provides a sanity check on frozen executable behaviour otherwise
  47. #difficult (if not infeasible) to do manually.
  48. #FIXME: This is great. We'd only like to make one improvement to the
  49. #aforementioned PyInstaller testing support: marks. Use them. Explicit is
  50. #typically better than implicit. In particular:
  51. #
  52. #* Define a new "betse_test.mark.able" submodule.
  53. #* Define a new "@freezable" mark decorator in this submodule.
  54. #* Decorate all tests suitable for running under a frozen copy of BETSE with
  55. # this decorator. This should probably *ONLY* include:
  56. # * All functional CLI tests, excluding "test_pyi_try". Everything else (e.g.,
  57. # unit tests) are *NOT* suitable for running frozen.
  58. #FIXME: Urgh. That's great, but it's still not quite enough. Why? Simulation
  59. #configurations. We currently create them by importing BETSE packages, which
  60. #clearly won't work for frozen commands. Instead, if running frozen, we'll need
  61. #to just call "betse config". Simple, if tedious.
  62. # ....................{ IMPORTS }....................
  63. from betse.util.os.command import cmdexit
  64. from betse.util.type.types import type_check
  65. from pytest import fixture
  66. # ....................{ CONSTANTS }....................
  67. _CLI_OPTIONS_MANDATORY = (
  68. '--verbose',
  69. '--log-level=none',
  70. '--matplotlib-backend=agg',
  71. )
  72. '''
  73. Tuple of all failure-friendly command-line options unconditionally passed to
  74. all invocations of the BETSE CLI by functional tests.
  75. To improve debuggability for failing tests, these options are unconditionally
  76. passed by :class:`CLITester` instances created by the :func:`betse_cli`
  77. fixture:
  78. * ``--verbose``, logging low-level debugging messages to stdout, which
  79. ``py.test`` captures for all tests and displays for all failing tests.
  80. * ``--log-level=none``, redirecting all log messages to either stdout or stderr
  81. but *not* a logfile. While ``py.test`` can be configured to capture logfile
  82. messages, doing so sanely is complicated by the fact that ``py.test`` already
  83. captures both stdout and stderr by default. There is no advantage to
  84. recapturing logfile messages already logged to either stdout or stderr. So,
  85. tests avoid doing so entirely.
  86. * ``--matplotlib-backend=agg``, enabling the default non-interactive matplotlib
  87. backend *guaranteed* to be usable on all platforms. By default, matplotlib
  88. enables an interactive backend (e.g., ``Qt5Agg``) inhibiting sane test
  89. automation.
  90. '''
  91. # ....................{ CLASSES }....................
  92. class CLITester(object):
  93. '''
  94. BETSE CLI test runner, efficiently testing a single subcommand of the
  95. official BETSE CLI (i.e., ``betse``) in the active Python interpreter.
  96. Simple functional fixtures (e.g., ``betse_cli``) often return instances of
  97. this class to other fixtures and tests exercising a single facet of the
  98. BETSE CLI.
  99. Command Execution
  100. ----------
  101. For both efficiency and reliably, this runner does *not* actually fork this
  102. subcommand as a separate process. Doing so would introduce installation
  103. complications, portability concerns, and non-reproduceable edge-cases
  104. (e.g., conflicting versions of the same ``betse`` command in the current
  105. ``${PATH}``). Instead, this runner:
  106. * Imports the :mod:`betse.__main__` module implementing the BETSE CLI.
  107. * Passes this module's :func:`betse.__main__.main` function the passed
  108. arguments extended by the mandatory arguments defined by the
  109. :data:`_CLI_OPTIONS_MANDATORY` tuple global.
  110. '''
  111. # ..................{ RUNNERS }..................
  112. @type_check
  113. def run(self, *args: str) -> None:
  114. '''
  115. Run the BETSE CLI with the passed positional string arguments, extended
  116. by the mandatory positional string arguments defined by the
  117. :data:`_CLI_OPTIONS_MANDATORY` tuple global.
  118. Parameters
  119. ----------
  120. args : Tuple[str]
  121. Tuple of zero or more arguments to be passed to this entry point,
  122. corresponding exactly to the set of command-line arguments accepted
  123. by the external command for the BETSE CLI (i.e., ``betse``).
  124. See Also
  125. ----------
  126. ``betse --help``
  127. Further details on arguments accepted by the BETSE CLI.
  128. '''
  129. # Defer heavyweight imports to their point of use.
  130. from betse.__main__ import main
  131. from betse.util.app.meta import appmetaone
  132. # Deinitialize this application and hence the previously initialized
  133. # application metadata singleton if any. If this is *NOT* done, the
  134. # call to the main() function below will raise an exception on
  135. # attempting to reinstantiate this singleton.
  136. appmetaone.deinit()
  137. # Prefixed this argument list by failure-friendly options.
  138. args_evolved = _CLI_OPTIONS_MANDATORY + args
  139. # print('BETSE arg list: {}'.format(arg_list))
  140. # Run the BETSE CLI subcommand corresponding to these arguments,
  141. # capturing the exit status of that subcommand for testing.
  142. exit_status = main(args_evolved)
  143. # If this exit status signifies failure, fail the current test.
  144. assert cmdexit.is_success(exit_status), (
  145. 'BETSE CLI failed with exit status {} '
  146. 'given arguments: {}'.format(exit_status, args_evolved))
  147. # ....................{ FIXTURES }....................
  148. # Test-scope fixture creating and returning a new object for each unique test.
  149. @fixture
  150. def betse_cli() -> CLITester:
  151. '''
  152. Fixture returning a test-specific object suitable for running arbitrary
  153. BETSE CLI subcommands required by the current fixture or test.
  154. Returns
  155. ----------
  156. CLITester
  157. Object running arbitrary BETSE CLI subcommands.
  158. '''
  159. # Print a newline, ensuring that the first line of stderr or stdout printed
  160. # by the BETSE CLI is visually demarcated from prior py.test output.
  161. print()
  162. # Create and return a new BETSE CLI test runner.
  163. return CLITester()