pax_global_header00006660000000000000000000000064147060104530014512gustar00rootroot0000000000000052 comment=37cce29b78be8bde55abc2512ca318b760d3829d pytest-shell-utilities-1.9.7/000077500000000000000000000000001470601045300161765ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/.codecov.yml000066400000000000000000000115361470601045300204270ustar00rootroot00000000000000codecov: require_ci_to_pass: yes # Less spammy. Only notify on passing builds. notify: wait_for_ci: true # Should Codecov wait for all CI statuses to complete before sending theirs ignore: - ^*.py$ # python files at the repo root, ie, setup.py - docs/.* # ignore any code under doc/ coverage: round: up range: 70..100 precision: 2 status: project: # measuring the overall project coverage default: target: auto # will use the coverage from the base commit (pull request base or parent commit) coverage to compare against. base: auto # will use the pull request base if the commit is on a pull request. If not, the parent commit will be used. flags: - src - tests src: # declare a new status context "src" target: auto # will use the coverage from the base commit (pull request base or parent commit) coverage to compare against. base: auto # will use the pull request base if the commit is on a pull request. If not, the parent commit will be used. if_no_uploads: error # will post commit status of "error" if no coverage reports were uploaded # options: success, error, failure if_not_found: success # if parent is not found report status as success, error, or failure if_ci_failed: error # if ci fails report status as success, error, or failure flags: - src tests: # declare a new status context "tests" target: auto # auto while we get this going base: auto # will use the pull request base if the commit is on a pull request. If not, the parent commit will be used. if_no_uploads: error # will post commit status of "error" if no coverage reports were uploaded # options: success, error, failure if_not_found: success # if parent is not found report status as success, error, or failure if_ci_failed: error # if ci fails report status as success, error, or failure flags: - tests patch: # pull requests only: this commit status will measure the # entire pull requests Coverage Diff. Checking if the lines # adjusted are covered at least X%. default: target: 100% # Newly added lines must have 100% coverage if_no_uploads: error # will post commit status of "error" if no coverage reports were uploaded # options: success, error, failure if_not_found: success if_ci_failed: error flags: - src - tests changes: # if there are any unexpected changes in coverage default: if_no_uploads: error if_not_found: success if_ci_failed: error flags: - src - tests flags: src: paths: - src/ carryforward: false # https://docs.codecov.io/docs/carryforward-flags # We always test the full repo code. Carry-forward is False. tests: paths: - tests/ carryforward: false Linux: paths: - src/ - tests/ carryforward: false Windows: paths: - src/ - tests/ carryforward: false macOS: paths: - src/ - tests/ carryforward: false Py35: paths: - src/ - tests/ carryforward: false Py36: paths: - src/ - tests/ carryforward: false Py37: paths: - src/ - tests/ carryforward: false Py38: paths: - src/ - tests/ carryforward: false Py39: paths: - src/ - tests/ carryforward: false Py310: paths: - src/ - tests/ carryforward: false PyTest60: paths: - src/ - tests/ carryforward: false PyTest61: paths: - src/ - tests/ carryforward: false PyTest62: paths: - src/ - tests/ carryforward: false PyTest70: paths: - src/ - tests/ carryforward: false comment: layout: "reach, diff, flags, files" behavior: default # Comment posting behaviour # default: update, if exists. Otherwise post new. # once: update, if exists. Otherwise post new. Skip if deleted. # new: delete old and post new. # spammy: post new (do not delete old comments). pytest-shell-utilities-1.9.7/.coveragerc000066400000000000000000000023061470601045300203200ustar00rootroot00000000000000[run] branch = True cover_pylib = False parallel = True concurrency = multiprocessing relative_files = True omit = .nox/* setup.py noxfile.py plugins = ${COVERAGE_PLUGINS_LIST} [coverage_conditional_plugin] # Here we specify our pragma rules: rules = "sys_platform == 'win32'": is-windows "sys_platform == 'win32' and sys.version_info[:2] == (3, 7)": is-windows-ge-py37 "'bsd' in sys_platform": is-bsd "'bsd' in sys_platform and sys.version_info < (3, 9)": is-bsd-lt-py39 "sys_platform == "darwin" and sys.version_info >= (3, 8)": is-darwin-ge-py38 [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplemented raise NotImplementedError # Don't complain if non-runnable code isn't run: if 0: if False: if __name__ == .__main__.: if TYPE_CHECKING: omit = .nox/* setup.py noxfile.py ignore_errors = True [paths] source = src/pytestshellutils/ **/site-packages/pytestshellutils/ testsuite = tests/ pytest-shell-utilities-1.9.7/.github/000077500000000000000000000000001470601045300175365ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/.github/CODEOWNERS000066400000000000000000000006561470601045300211400ustar00rootroot00000000000000# pytest-shell-utilities CODE OWNERS # See GitHub's Docs About Code Owners # for more info about the CODEOWNERS file # This is a comment. # Each line is a file pattern followed by one or more owners. # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, # @and will be requested for # review when someone opens a pull request. # Team Core * @saltstack/team-core pytest-shell-utilities-1.9.7/.github/workflows/000077500000000000000000000000001470601045300215735ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/.github/workflows/release.yml000066400000000000000000000012611470601045300237360ustar00rootroot00000000000000name: Release on: release: types: [created] jobs: Publish: runs-on: ubuntu-latest environment: release permissions: id-token: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install Nox run: | python -m pip install nox - name: Build a binary wheel and a source tarball run: | nox -e build - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: print-hash: true verbose: true verify-metadata: true pytest-shell-utilities-1.9.7/.github/workflows/testing.yml000066400000000000000000000226761470601045300240100ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: Pre-Commit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: 3.9 - name: Set Cache Key run: echo "PY=$(python --version --version | sha256sum | cut -d' ' -f1)" >> "$GITHUB_ENV" - name: Install System Deps run: | sudo apt-get update sudo apt-get install -y libxml2 libxml2-dev libxslt-dev - uses: actions/cache@v4 with: path: ~/.cache/pre-commit key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} - uses: pre-commit/action@v1.0.1 Twine-Check: runs-on: ubuntu-latest needs: Pre-Commit steps: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install Nox run: | python -m pip install --upgrade pip pip install nox - name: Twine check run: | nox -e twine-check PyLint: runs-on: ubuntu-latest needs: Pre-Commit timeout-minutes: 10 steps: - uses: actions/checkout@v4 - name: Set up Python 3.9 For Nox uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install Nox run: | python -m pip install --upgrade pip pip install nox - name: Install Lint Requirements run: | nox --force-color -e lint --install-only - name: Build Docs env: SKIP_REQUIREMENTS_INSTALL: YES run: | nox --force-color -e lint Docs: runs-on: ubuntu-latest needs: Pre-Commit timeout-minutes: 10 steps: - uses: actions/checkout@v4 - name: Set up Python 3.9 For Nox uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install Nox run: | python -m pip install --upgrade pip pip install nox - name: Install Doc Requirements run: | nox --force-color -e docs --install-only - name: Build Docs env: SKIP_REQUIREMENTS_INSTALL: YES run: | nox --force-color -e docs Linux: runs-on: ubuntu-latest needs: Pre-Commit timeout-minutes: 15 strategy: fail-fast: false max-parallel: 15 matrix: python-version: - "3.8" - "3.9" - "3.10" - "3.11" pytest-version: - "7.4.0" - "8.0.0" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Nox run: | python -m pip install --upgrade pip pip install nox - name: Install Test Requirements env: PYTEST_VERSION_REQUIREMENT: pytest~=${{ matrix.pytest-version }} run: | nox --force-color -e tests-3 --install-only - name: Test env: SKIP_REQUIREMENTS_INSTALL: YES run: | nox --force-color -e tests-3 -- -vv tests/ - name: Upload Tests coverage to Codecov uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} directory: artifacts/ fail_ci_if_error: false files: coverage-tests.xml flags: tests,${{ runner.os }},Py${{ matrix.python-version}},PyTest${{ matrix.pytest-version }} name: tests-${{ runner.os }}-Py${{ matrix.python-version}}-PyTest${{ matrix.pytest-version }} verbose: true - name: Upload Project coverage to Codecov uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} directory: artifacts/ fail_ci_if_error: false files: coverage-project.xml flags: src,${{ runner.os }},Py${{ matrix.python-version}},PyTest${{ matrix.pytest-version }} name: src-${{ runner.os }}-Py${{ matrix.python-version}}-PyTest${{ matrix.pytest-version }} verbose: true - name: Upload Logs if: always() uses: actions/upload-artifact@v4 with: name: runtests-${{ runner.os }}-Py${{ matrix.python-version}}-PyTest${{ matrix.pytest-version }}.log path: artifacts/runtests-*.log Windows: runs-on: windows-latest needs: Pre-Commit timeout-minutes: 40 strategy: fail-fast: false max-parallel: 15 matrix: python-version: - "3.8" - "3.9" - "3.10" - "3.11" pytest-version: - "7.4.0" - "8.0.0" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Nox run: | python -m pip install --upgrade pip pip install nox - name: Install Test Requirements shell: bash env: PYTEST_VERSION_REQUIREMENT: pytest~=${{ matrix.pytest-version }} run: | export PATH="/C/Program Files (x86)/Windows Kits/10/bin/10.0.18362.0/x64;$PATH" nox --force-color -e tests-3 --install-only - name: Test shell: bash env: SKIP_REQUIREMENTS_INSTALL: YES run: | export PATH="/C/Program Files (x86)/Windows Kits/10/bin/10.0.18362.0/x64;$PATH" nox --force-color -e tests-3 -- -vv tests/ - name: Upload Tests coverage to Codecov uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} directory: artifacts/ fail_ci_if_error: false files: coverage-tests.xml flags: tests,${{ runner.os }},Py${{ matrix.python-version}},PyTest${{ matrix.pytest-version }} name: tests-${{ runner.os }}-Py${{ matrix.python-version}}-PyTest${{ matrix.pytest-version }} verbose: true - name: Upload Project coverage to Codecov uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} directory: artifacts/ fail_ci_if_error: false files: coverage-project.xml flags: src,${{ runner.os }},Py${{ matrix.python-version}},PyTest${{ matrix.pytest-version }} name: src-${{ runner.os }}-Py${{ matrix.python-version}}-PyTest${{ matrix.pytest-version }} verbose: true - name: Upload Logs if: always() uses: actions/upload-artifact@v4 with: name: runtests-${{ runner.os }}-Py${{ matrix.python-version}}-PyTest${{ matrix.pytest-version }}.log path: artifacts/runtests-*.log macOS: runs-on: macOS-latest needs: Pre-Commit timeout-minutes: 40 strategy: fail-fast: false max-parallel: 15 matrix: python-version: - "3.8" - "3.9" - "3.10" - "3.11" pytest-version: - "7.4.0" - "8.0.0" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Nox run: | python -m pip install --upgrade pip pip install nox - name: Install Test Requirements env: PYTEST_VERSION_REQUIREMENT: pytest~=${{ matrix.pytest-version }} run: | nox --force-color -e tests-3 --install-only - name: Test env: SKIP_REQUIREMENTS_INSTALL: YES run: | nox --force-color -e tests-3 -- -vv tests/ - name: Upload Tests coverage to Codecov uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} directory: artifacts/ fail_ci_if_error: false files: coverage-tests.xml flags: tests,${{ runner.os }},Py${{ matrix.python-version}},PyTest${{ matrix.pytest-version }} name: tests-${{ runner.os }}-Py${{ matrix.python-version}}-PyTest${{ matrix.pytest-version }} verbose: true - name: Upload Project coverage to Codecov uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} directory: artifacts/ fail_ci_if_error: false files: coverage-project.xml flags: src,${{ runner.os }},Py${{ matrix.python-version}},PyTest${{ matrix.pytest-version }} name: src-${{ runner.os }}-Py${{ matrix.python-version}}-PyTest${{ matrix.pytest-version }} verbose: true - name: Upload Logs if: always() uses: actions/upload-artifact@v4 with: name: runtests-${{ runner.os }}-Py${{ matrix.python-version}}-PyTest${{ matrix.pytest-version }}.log path: artifacts/runtests-*.log Build: runs-on: ubuntu-latest environment: testing permissions: id-token: write needs: - Docs - PyLint - Twine-Check - Linux - Windows - macOS steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install Nox run: | python -m pip install nox - name: Build a binary wheel and a source tarball run: | nox -e build - name: Publish distribution 📦 to Test PyPI if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@release/v1 with: repository_url: https://test.pypi.org/legacy/ print-hash: true skip-existing: true verbose: true verify-metadata: true pytest-shell-utilities-1.9.7/.gitignore000066400000000000000000000037051470601045300201730ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.data dmypy.data # Pyre type checker .pyre/ # Local vim directory .vim/ # Ignore the setuptools_scm auto-generated version module src/pytestshellutils/version.py # Ignore CI generated artifacts artifacts/ # neovim backup files *~ pytest-shell-utilities-1.9.7/.pre-commit-config.yaml000066400000000000000000000120751470601045300224640ustar00rootroot00000000000000--- minimum_pre_commit_version: 3.6.0 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: check-merge-conflict # Check for files that contain merge conflict strings. - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - id: mixed-line-ending # Replaces or checks mixed line ending. args: [--fix=lf] - id: end-of-file-fixer - id: fix-encoding-pragma args: [--remove] - id: check-yaml - id: debug-statements language_version: python3 # ----- Local Hooks -----------------------------------------------------------------------------------------------> - repo: local hooks: - id: sort-pylint-spelling-words name: Sort PyLint Spelling Words File entry: python .pre-commit-hooks/sort-pylint-spelling-words.py language: system files: ^\.pylint-spelling-words$ - id: check-changelog-entries name: Check Changelog Entries entry: python .pre-commit-hooks/check-changelog-entries.py language: system - id: check-copyright-headers name: Check python modules for appropriate copyright headers files: ^.*\.py$ entry: python .pre-commit-hooks/copyright-headers.py language: system # <---- Local Hooks ------------------------------------------------------------------------------------------------ # ----- Formatting ------------------------------------------------------------------------------------------------> - repo: https://github.com/saltstack/pre-commit-remove-import-headers rev: 1.1.0 hooks: - id: remove-import-headers - repo: https://github.com/asottile/pyupgrade rev: v3.15.1 hooks: - id: pyupgrade name: Rewrite Code to be Py3.8+ args: [ --py38-plus ] files: ^(src/.*\.py)$ exclude: ^src/pytestshellutils/(__init__|version)\.py$ - repo: https://github.com/asottile/reorder_python_imports rev: v3.13.0 hooks: - id: reorder-python-imports args: - --py37-plus - --application-directories=.:src exclude: ^src/pytestshellutils/version\.py$ - repo: https://github.com/psf/black rev: 24.2.0 hooks: - id: black args: [-l 100] exclude: ^src/pytestshellutils/version\.py$ - repo: https://github.com/asottile/blacken-docs rev: 1.16.0 hooks: - id: blacken-docs args: [--skip-errors] files: ^(.*\.rst|docs/.*\.rst|src/pytestshellutils/.*\.py)$ additional_dependencies: [black==24.2.0] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.11.1 hooks: - id: mypy alias: mypy-tools name: Run mypy against tools files: ^tools/.*\.py$ #args: [--strict] additional_dependencies: - attrs - rich - types-attrs - types-requests - repo: https://github.com/s0undt3ch/python-tools-scripts rev: "0.20.5" hooks: - id: tools alias: actionlint name: Lint GitHub Actions Workflows files: "^.github/workflows/" types: - yaml args: - pre-commit - actionlint additional_dependencies: - packaging==23.0 # <---- Formatting ------------------------------------------------------------------------------------------------- # ----- Security --------------------------------------------------------------------------------------------------> - repo: https://github.com/PyCQA/bandit rev: "1.7.7" hooks: - id: bandit alias: bandit-salt name: Run bandit against the code base args: [--silent, -lll, --skip, B701] files: ^(?!tests/).*\.py$ exclude: ^src/pytestshellutils/version\.py$ - repo: https://github.com/PyCQA/bandit rev: "1.7.7" hooks: - id: bandit alias: bandit-tests name: Run bandit against the test suite args: [--silent, -lll, --skip, B701] files: ^tests/.* # <---- Security --------------------------------------------------------------------------------------------------- # ----- Code Analysis ---------------------------------------------------------------------------------------------> - repo: https://github.com/pycqa/flake8 rev: '7.1.1' hooks: - id: flake8 exclude: ^(src/pytestshellutils/version\.py|\.pre-commit-hooks/.*\.py)$ additional_dependencies: - flake8-mypy-fork - pydocstyle>=4.0.0 - flake8-docstrings - flake8-rst - flake8-typing-imports - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.11.1 hooks: - id: mypy files: ^((src|tests)/.*\.py)$ exclude: ^(src/pytestshellutils/(utils/(socket|time)\.py))$ args: [--strict] additional_dependencies: - attrs - types-attrs - types-setuptools - pydantic - pytest # <---- Code Analysis ---------------------------------------------------------------------------------------------- pytest-shell-utilities-1.9.7/.pre-commit-hooks/000077500000000000000000000000001470601045300214515ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/.pre-commit-hooks/check-changelog-entries.py000077500000000000000000000110161470601045300264760ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # # pylint: disable=invalid-name,missing-module-docstring,missing-function-docstring import argparse import pathlib import re import sys CODE_ROOT = pathlib.Path(__file__).resolve().parent.parent CHANGELOG_ENTRIES_PATH = CODE_ROOT / "changelog" CHANGELOG_LIKE_RE = re.compile(r"([\d]+)\.([a-z]+)(\.rst)?$") CHANGELOG_EXTENSIONS = ( "breaking", "deprecation", "feature", "improvement", "bugfix", "doc", "trivial", ) CHANGELOG_ENTRY_REREX = r"^[\d]+\.({})\.rst$".format("|".join(CHANGELOG_EXTENSIONS)) CHANGELOG_ENTRY_RE = re.compile(CHANGELOG_ENTRY_REREX) def check_changelog_entries(files): exitcode = 0 for entry in files: path = pathlib.Path(entry).resolve() # Is it under changelog/ try: path.relative_to(CHANGELOG_ENTRIES_PATH) if path.name in (".gitkeep", ".gitignore", "_template.rst", __name__): # These files should be ignored continue # Is it named properly if not CHANGELOG_ENTRY_RE.match(path.name): # Does it end in .rst if path.suffix != ".rst": exitcode = 1 print( "The changelog entry '{}' should have '.rst' as it's file extension".format( path.relative_to(CODE_ROOT), ), file=sys.stderr, flush=True, ) continue print( "The changelog entry '{}' should have one of the following extensions: {}.".format( path.relative_to(CODE_ROOT), ", ".join(repr(ext) for ext in CHANGELOG_EXTENSIONS), ), file=sys.stderr, flush=True, ) exitcode = 1 continue check_changelog_entry_contents(path) except ValueError: # Not under changelog/, carry on checking # Is it a changelog entry if CHANGELOG_ENTRY_RE.match(path.name): # So, this IS a changelog entry, but it's misplaced.... exitcode = 1 print( "The changelog entry '{}' should be placed under '{}/', not '{}'".format( path.relative_to(CODE_ROOT), CHANGELOG_ENTRIES_PATH.relative_to(CODE_ROOT), path.relative_to(CODE_ROOT).parent, ), file=sys.stderr, flush=True, ) continue elif CHANGELOG_LIKE_RE.match(path.name) and not CHANGELOG_ENTRY_RE.match(path.name): # Does it look like a changelog entry print( "The changelog entry '{}' should have one of the following extensions: {}.".format( path.relative_to(CODE_ROOT), ", ".join(repr(ext) for ext in CHANGELOG_EXTENSIONS), ), file=sys.stderr, flush=True, ) exitcode = 1 continue elif not CHANGELOG_LIKE_RE.match(path.name) and not CHANGELOG_ENTRY_RE.match(path.name): # Does not look like, and it's not a changelog entry continue # Does it end in .rst if path.suffix != ".rst": exitcode = 1 print( "The changelog entry '{}' should have '.rst' as it's file extension".format( path.relative_to(CODE_ROOT), ), file=sys.stderr, flush=True, ) return exitcode def check_changelog_entry_contents(entry): contents = entry.read_text().splitlines() if len(contents) > 1: # More than one line. # If the second line starts with '*' it's a bullet list and we need to add an # empty line before it. if contents[1].strip().startswith("*"): contents.insert(1, "") entry.write_text("{}\n".format("\n".join(contents))) def main(argv): parser = argparse.ArgumentParser(prog=__name__) parser.add_argument("files", nargs="+") options = parser.parse_args(argv) return check_changelog_entries(options.files) if __name__ == "__main__": sys.exit(main(sys.argv)) pytest-shell-utilities-1.9.7/.pre-commit-hooks/copyright-headers.py000066400000000000000000000067351470601045300254570ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # # pylint: disable=invalid-name,missing-module-docstring,missing-function-docstring import argparse import pathlib import re import sys from datetime import datetime CODE_ROOT = pathlib.Path(__file__).resolve().parent.parent SPDX_HEADER = "# SPDX-License-Identifier: Apache-2.0" COPYRIGHT_HEADER = "# Copyright {year} VMware, Inc." COPYRIGHT_REGEX = re.compile( r"# Copyright (?:(?P[0-9]{4})(?:-(?P[0-9]{4}))?) VMware, Inc\." ) SPDX_REGEX = re.compile(r"# SPDX-License-Identifier:.*") def check_copyright(files): for file in files: contents = file.read_text() if not contents.strip(): # Don't add headers to empty files continue original_contents = contents try: if not COPYRIGHT_REGEX.search(contents): contents = inject_copyright_header(contents) if contents != original_contents: print(f"Added the copyright header to {file}") else: contents = update_copyright_header(contents) if contents != original_contents: print(f"Updated the copyright header on {file}") if not SPDX_REGEX.search(contents): contents = inject_spdx_header(contents) if contents != original_contents: print(f"Added the SPDX header to {file}") finally: if not contents.endswith("\n"): contents += "\n" if original_contents != contents: file.write_text(contents) def inject_copyright_header(contents): lines = contents.splitlines() shebang_found = False for idx, line in enumerate(lines[:]): if idx == 0 and line.startswith("#!"): shebang_found = True continue if shebang_found and line.strip(): shebang_found = False lines.insert(idx, "") idx += 1 lines.insert(idx, COPYRIGHT_HEADER.format(year=datetime.today().year)) break return "\n".join(lines) def update_copyright_header(contents): lines = contents.splitlines() for idx, line in enumerate(lines[:]): match = COPYRIGHT_REGEX.match(line) if match: this_year = str(datetime.today().year) cur_year = match.group("cur_year") if cur_year and cur_year.strip() == this_year: return contents initial_year = match.group("start_year").strip() if initial_year == this_year: return contents lines[idx] = COPYRIGHT_HEADER.format(year=f"{initial_year}-{this_year}") break return "\n".join(lines) def inject_spdx_header(contents): lines = contents.splitlines() for idx, line in enumerate(lines[:]): if COPYRIGHT_REGEX.match(line): lines.insert(idx + 1, SPDX_HEADER) next_line = lines[idx + 2].strip() if next_line and not next_line.startswith('"""'): # If the next line is not empty, insert an empty comment lines.insert(idx + 2, "#") break return "\n".join(lines) def main(argv): parser = argparse.ArgumentParser(prog=__name__) parser.add_argument("files", nargs="+", type=pathlib.Path) options = parser.parse_args(argv) return check_copyright(options.files) if __name__ == "__main__": sys.exit(main(sys.argv)) pytest-shell-utilities-1.9.7/.pre-commit-hooks/sort-pylint-spelling-words.py000077500000000000000000000011031470601045300272740ustar00rootroot00000000000000#!/usr/bin/env python # Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # pylint: skip-file import pathlib REPO_ROOT = pathlib.Path(__name__).resolve().parent PYLINT_SPELLING_WORDS = REPO_ROOT / ".pylint-spelling-words" def sort(): in_contents = PYLINT_SPELLING_WORDS.read_text() out_contents = "" out_contents += "\n".join(sorted({line.lower() for line in in_contents.splitlines()})) out_contents += "\n" if in_contents != out_contents: PYLINT_SPELLING_WORDS.write_text(out_contents) if __name__ == "__main__": sort() pytest-shell-utilities-1.9.7/.pylint-spelling-words000066400000000000000000000012621470601045300224660ustar00rootroot00000000000000abc abspath aix argparse args ascii attr attrs autodoc basename basepath bool boolean changelog cli cmdline config conftest confvals css cwd darwin dns eg env environ exitcode favicon favicons fds freebsd getsockname https ico illumos ini intersphinx intl ips iterable json kwargs linkcheck linux localhost lru lsof macos mbcs mtime mypy namespace netbsd nox openbsd os osx pathlib pid png popen pragma prepend processresult psutil py pyproject pytest pytest's pytestshellutils pytestskipmarkers rc ret returncode rst rtype sdist shellresult sid sids sitecustomize sitevars smartos stacklevel stderr stdin stdout str subprocess sunos sys toml towncrier untyped uss utils virtualenvs vmware pytest-shell-utilities-1.9.7/.pylintrc000066400000000000000000000323571470601045300200550ustar00rootroot00000000000000[MASTER] # Specify a configuration file. #rcfile= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Profiled execution. profile=no # Add files or directories to the blacklist. They should be base names, not # paths. ignore=CVS,_version.py # Pickle collected data for later comparisons. persistent=yes # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= # Use multiple processes to speed up Pylint. jobs=1 # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code extension-pkg-whitelist= # Fileperms Lint Plugin Settings fileperms-default=0644 fileperms-ignore-paths=setup.py [MESSAGES CONTROL] # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED confidence= # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time. See also the "--disable" option for examples. #enable= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this # option multiple times (only on the command line, not in the configuration # file where it should appear only once).You can also use "--disable=all" to # disable everything first and then reenable specific checks. For example, if # you want to run only the similarities checker, you can use "--disable=all # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" #disable= disable=R, I0011, I0012, I0013, E1101, E1103, C0102, C0103, C0111, C0203, C0204, C0301, C0302, C0330, W0110, W0122, W0142, W0201, W0212, W0404, W0511, W0603, W0612, W0613, W0621, W0622, W0631, W0704, W1202, W1307, F0220, F0401, E8501, E8116, E8121, E8122, E8123, E8124, E8125, E8126, E8127, E8128, E8129, E8131, E8265, E8266, E8402, E8731, locally-disabled, repr-flag-used-in-string, un-indexed-curly-braces-error, un-indexed-curly-braces-warning, import-outside-toplevel, wrong-import-position, wrong-import-order, missing-whitespace-after-comma, consider-using-f-string # Disabled: # R* [refactoring suggestions & reports] # I0011 (locally-disabling) # I0012 (locally-enabling) # I0013 (file-ignored) # E1101 (no-member) [pylint isn't smart enough] # E1103 (maybe-no-member) # C0102 (blacklisted-name) [because it activates C0103 too] # C0103 (invalid-name) # C0111 (missing-docstring) # C0203 (bad-mcs-method-argument) # C0204 (bad-mcs-classmethod-argument) # C0301 (line-too-long) # C0302 (too-many-lines) # C0330 (bad-continuation) # W0110 (deprecated-lambda) # W0122 (exec-statement) # W0142 (star-args) # W0201 (attribute-defined-outside-init) [done in several places in the codebase] # W0212 (protected-access) # W0404 (reimported) [done intentionally for legit reasons] # W0511 (fixme) [several outstanding instances currently in the codebase] # W0603 (global-statement) # W0612 (unused-variable) [unused return values] # W0613 (unused-argument) # W0621 (redefined-outer-name) # W0622 (redefined-builtin) [many parameter names shadow builtins] # W0631 (undefined-loop-variable) [~3 instances, seem to be okay] # W0704 (pointless-except) [misnomer; "ignores the exception" rather than "pointless"] # F0220 (unresolved-interface) # F0401 (import-error) # W1202 (logging-format-interpolation) Use % formatting in logging functions but pass the % parameters as arguments # W1307 (invalid-format-index) Using invalid lookup key '%s' in format specifier "0['%s']" # # E8116 PEP8 E116: unexpected indentation (comment) # E812* All PEP8 E12* # E8265 PEP8 E265 - block comment should start with "# " # E8266 PEP8 E266 - too many leading '#' for block comment # E8501 PEP8 line too long # E8402 module level import not at top of file # E8731 do not assign a lambda expression, use a def # # E1322(repr-flag-used-in-string) [REPORTS] # Set the output format. Available formats are text, parseable, colorized, msvs # (visual studio) and html. You can also give a reporter class, eg # mypackage.mymodule.MyReporterClass. output-format=text # Put messages in a separate file for each module / package specified on the # command line instead of printing them on stdout. Reports (if any) will be # written in a file name "pylint_global.[txt|html]". files-output=no # Tells whether to display a full report or only the messages reports=no # Python expression which should return a note less than 10 (10 is the highest # note). You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Add a comment according to your evaluation note. This is used by the global # evaluation report (RP0004). comment=no # Template used to display messages. This is a python new-style format string # used to format the message information. See doc for all details #msg-template= [LOGGING] # Logging modules to check that the string format arguments are in logging # function parameter format logging-modules=logging [SPELLING] # Spelling dictionary name. Available dictionaries: none. To make it working # install python-enchant package. spelling-dict=en_US # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains private dictionary; one word per line. spelling-private-dict-file=.pylint-spelling-words # Tells whether to store unknown words to indicated private dictionary in # --spelling-private-dict-file option instead of raising a message. spelling-store-unknown-words=no [SIMILARITIES] # Minimum lines number of a similarity. min-similarity-lines=4 # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=no [VARIABLES] # Tells whether we should check for unused import in __init__ files. init-import=no # A regular expression matching the name of dummy variables (i.e. expectedly # not used). dummy-variables-rgx=_$|dummy # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins=__opts__,__virtual__,__salt_system_encoding__,__context__,__salt__ # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. callbacks=cb_,_cb [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME,XXX,TODO [BASIC] # List of builtins function names that should not be used, separated by a comma bad-functions=map,filter,input # Good variable names which should always be accepted, separated by a comma good-names=i,j,k,ex,Run,_,log,pytest_plugins,__opts__,__context__ # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= # Include a hint for the correct naming format with invalid-name include-naming-hint=no # Regular expression matching correct function names function-rgx=[a-z_][a-z0-9_]{2,60}$ # Naming hint for function names function-name-hint=[a-z_][a-z0-9_]{2,60}$ # Regular expression matching correct variable names variable-rgx=[a-z_][a-z0-9_]{2,60}$ # Naming hint for variable names variable-name-hint=[a-z_][a-z0-9_]{2,60}$ # Regular expression matching correct constant names const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ # Naming hint for constant names const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ # Regular expression matching correct attribute names attr-rgx=[a-z_][a-z0-9_]{2,60}$ # Naming hint for attribute names attr-name-hint=[a-z_][a-z0-9_]{2,60}$ # Regular expression matching correct argument names argument-rgx=[a-z_][a-z0-9_]{2,60}$ # Naming hint for argument names argument-name-hint=[a-z_][a-z0-9_]{2,60}$ # Regular expression matching correct class attribute names class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,60}|(__.*__))$ # Naming hint for class attribute names class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,60}|(__.*__))$ # Regular expression matching correct inline iteration names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ # Naming hint for inline iteration names inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ # Regular expression matching correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ # Naming hint for class names class-name-hint=[A-Z_][a-zA-Z0-9]+$ # Regular expression matching correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Naming hint for module names module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression matching correct method names method-rgx=[a-z_][a-z0-9_]{2,60}$ # Naming hint for method names method-name-hint=[a-z_][a-z0-9_]{2,60}$ # Regular expression which should only match function or class names that do # not require a docstring. no-docstring-rgx=__.*__ # Minimum line length for functions/classes that require docstrings, shorter # ones are exempt. docstring-min-length=-1 [FORMAT] # Maximum number of characters on a single line. max-line-length=120 # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no # List of optional constructs for which whitespace checking is disabled no-space-check=trailing-comma,dict-separator # Maximum number of lines in a module max-module-lines=1000 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' # Number of spaces of indent required inside a hanging or continued line. indent-after-paren=4 # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. expected-line-ending-format=LF [TYPECHECK] # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime # and thus existing member attributes cannot be deduced by static analysis ignored-modules= # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). ignored-classes= # When zope mode is activated, add a predefined set of Zope acquired attributes # to generated-members. zope=no # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E0201 when accessed. Python regular # expressions are accepted. generated-members= [IMPORTS] # Deprecated modules which should not be used, separated by a comma deprecated-modules=regsub,TERMIOS,Bastion,rexec # Create a graph of every (i.e. internal and external) dependencies in the # given file (report RP0402 must not be disabled) import-graph= # Create a graph of external dependencies in the given file (report RP0402 must # not be disabled) ext-import-graph= # Create a graph of internal dependencies in the given file (report RP0402 must # not be disabled) int-import-graph= [DESIGN] # Maximum number of arguments for function / method max-args=5 # Argument names that match this expression will be ignored. Default to name # with leading underscore ignored-argument-names=_.* # Maximum number of locals for function / method body max-locals=15 # Maximum number of return / yield for function / method body max-returns=6 # Maximum number of branch for function / method body max-branches=12 # Maximum number of statements in function / method body max-statements=50 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of attributes for a class (see R0902). max-attributes=7 # Minimum number of public methods for a class (see R0903). min-public-methods=2 # Maximum number of public methods for a class (see R0904). max-public-methods=20 [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. valid-metaclass-classmethod-first-arg=mcs # List of member names, which should be excluded from the protected access # warning. exclude-protected=_asdict,_fields,_replace,_source,_make [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to # "Exception" overgeneral-exceptions=Exception pytest-shell-utilities-1.9.7/.readthedocs.yaml000066400000000000000000000013461470601045300214310ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.10" # You can also specify other tool versions: # nodejs: "20" # rust: "1.70" # golang: "1.20" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # If using Sphinx, optionally build your docs in additional formats such as PDF ## formats: ## - pdf # Optionally declare the Python requirements required to build your docs python: install: - method: pip path: . extra_requirements: - docs pytest-shell-utilities-1.9.7/CHANGELOG.rst000066400000000000000000000214221470601045300202200ustar00rootroot00000000000000.. _changelog: ========= Changelog ========= Versions follow `Semantic Versioning `_ (`..`). Backward incompatible (breaking) changes will only be introduced in major versions with advance notice in the **Deprecations** section of releases. .. towncrier-draft-entries:: .. towncrier release notes start shell-utilities 1.9.0 (2024-02-23) ================================== Breaking Changes ---------------- - Drop support for Python older than 3.8 and Pytest older than 7.4.x (`#43 `_) Bug Fixes --------- - The printed output is now the result of `json.dumps` instead of `pprint.pformat` (`#42 `_) Trivial/Internal Changes ------------------------ - Several minor changes to the code base: * Update copyright headers * Update pre-commit hook versions (`#43 `_) shell-utilities 1.8.0 (2023-07-02) ================================== Breaking Changes ---------------- - Drop support for python versions older than 3.7 (`#38 `_) Improvements ------------ - Support Python 3.11 (`#40 `_) Bug Fixes --------- - Set minimal attrs version to 22.1.0 (`#28 `_) Trivial/Internal Changes ------------------------ - Update the GitHub actions versions and stop using `::set-output` (`#38 `_) - Several project internal changes * Start running tests against Py3.11 and Pytest `7.3.x` and `7.4.x` * Update copyright headers * Upgrade to `coverage==7.2.7` * Switch to `codecov/codecov-action` (`#39 `_) shell-utilities 1.7.0 (2022-09-23) ================================== Bug Fixes --------- - ``Subprocess.run()`` now accepts ``shell`` keyword argument like ``subprocess.Popen``. (`#32 `_) - The `Subprocess.run()` method can now override the `cwd` (`#33 `_) Trivial/Internal Changes ------------------------ - Update pre-commit hook versions (`#34 `_) shell-utilities 1.6.0 (2022-07-28) ================================== Improvements ------------ - The ``shell`` fixture is now ``session`` scoped (`#29 `_) shell-utilities 1.5.0 (2022-06-02) ================================== Improvements ------------ - The minimum python for the code base is now 3.7(we still provide support to Py3.5 and Py3.6 by providing a downgraded source, transparent to the user), and the project is now 100% typed, including the test suite. (`#26 `_) Improved Documentation ---------------------- - Improve and switch to google style docstrings (`#24 `_) shell-utilities 1.4.0 (2022-05-26) ================================== Improvements ------------ - ``Daemon.started()`` is now a context manager (`#22 `_) shell-utilities 1.3.0 (2022-05-26) ================================== Improvements ------------ - Support user provided callable functions to confirm that the daemon is up and running (`#20 `_) shell-utilities 1.2.1 (2022-05-23) ================================== Bug Fixes --------- - Account for ``ProcessLookupError`` when terminating the underlying process. (`#18 `_) shell-utilities 1.2.0 (2022-05-20) ================================== Improvements ------------ - Revert `"Skip test when the GLIBC race conditions are met, instead of failing." `_ It wasn't the right fix/workaround. The right fix can be seen in `the Salt repo `_ (`#16 `_) Trivial/Internal Changes ------------------------ - Remove the redundant `wheel` dependency from pyproject.toml. The setuptools backend takes care of adding it automatically via `setuptools.build_meta.get_requires_for_build_wheel()` since day one. The documentation has historically been wrong about listing it, and it has been fixed since. See https://github.com/pypa/setuptools/commit/f7d30a9529378cf69054b5176249e5457aaf640a (`#15 `_) shell-utilities 1.1.0 (2022-05-16) ================================== Improvements ------------ - Skip test when the GLIBC race conditions are met, instead of failing (`#13 `_) Trivial/Internal Changes ------------------------ - Update pre-commit hooks and test against PyTest 7.0.x and 7.1.x. (`#13 `_) shell-utilities 1.0.5 (2022-02-21) ================================== Bug Fixes --------- - Fix deprecation message telling to use the wrong property. (`#12 `_) shell-utilities 1.0.4 (2022-02-17) ================================== Improvements ------------ - State from which library the ``DeprecationWarning`` is coming from. (`#9 `_) Bug Fixes --------- - Handle ``None`` values for ``.stdout`` and ``.stderr`` on ``ProcessResult.__str__()`` (`#8 `_) shell-utilities 1.0.3 (2022-02-16) ================================== Bug Fixes --------- - Fixed issue with ``sdist`` recompression for reproducible packages not iterating though subdirectories contents. (`#7 `_) shell-utilities 1.0.2 (2022-02-05) ================================== Bug Fixes --------- - Set lower required python to `3.5.2` and avoid issues with `flake8-typing-imports`. (`#6 `_) shell-utilities 1.0.1 (2022-01-25) ================================== Bug Fixes --------- - Stop casting ``None`` to a string for ``ProcessResult.std{out,err}`` (`#4 `_) shell-utilities 1.0.0 (2022-01-25) ================================== No significant changes. shell-utilities 1.0.0rc7 (2022-01-25) ===================================== Trivial/Internal Changes ------------------------ - Improvements before final RC * Add ``ProcessResult.std{out,err}.matcher`` example * Also generate reproducible packages when uploading a release to pypi * The ``twine-check`` nox target now call's the ``build`` target (`#3 `_) shell-utilities 1.0.0rc6 (2022-01-24) ===================================== No significant changes. shell-utilities 1.0.0rc5 (2022-01-24) ===================================== Trivial/Internal Changes ------------------------ - Provide a way to create reproducible distribution packages. * Stop customizing the ``towncrier`` template. (`#1 `_) shell-utilities 1.0.0rc4 (2022-01-23) ===================================== * ``ProcessResult.stdout`` and ``ProcessResult.stderr`` are now instances of ``pytestshellutils.utils.processes.MatchString`` which provides a ``.matcher`` attribute that returns an instance of ``pytest.LineMatcher``. shell-utilities 1.0.0rc3 (2022-01-21) ===================================== * ``cwd`` and ``environ`` are now defined on ``BaseFactory`` * Add ``py.typed`` to state that the package is fully typed * Fix the ``stacklevel`` value to point to the actual caller of the ``warn_until`` function. * Fix the deprecated ``ProcessResult.json`` property. shell-utilities 1.0.0rc2 (2022-01-21) ===================================== * When passed a string, cast it to ``pathlib.Path`` before calling ``.resolve()`` * Extract ``BaseFactory`` from ``Factory``. It's required on `pytest-salt-factories`_ container implementation. shell-utilities 1.0.0rc1 (2022-01-21) ===================================== Pre-release of the first working version of the pytest plugin. .. _pytest-salt-factories: https://github.com/saltstack/pytest-salt-factories pytest-shell-utilities-1.9.7/CODE_OF_CONDUCT.md000066400000000000000000000122101470601045300207710ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in pytest-shell-utilities project and our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at oss-coc@@vmware.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. pytest-shell-utilities-1.9.7/CONTRIBUTING.md000066400000000000000000000046571470601045300204430ustar00rootroot00000000000000# Contributing to pytest-shell-utilities The pytest-shell-utilities project team welcomes contributions from the community. If you wish to contribute code and you have not signed our [Contributor License Agreement](https://cla.vmware.com/cla/1/preview), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ](https://cla.vmware.com/faq). ## Contribution Flow This is a rough outline of what a contributor's workflow looks like: - Create a topic branch from where you want to base your work - Make commits of logical units - Make sure your commit messages are in the proper format (see below) - Push your changes to a topic branch in your fork of the repository - Submit a pull request Example: ``` shell git remote add upstream https://github.com/saltstack/pytest-shell-utilities.git git checkout -b my-new-feature main git commit -a git push origin my-new-feature ``` ### Staying In Sync With Upstream When your branch gets out of sync with the pytest-shell-utilities/main branch, use the following to update: ``` shell git checkout my-new-feature git fetch -a git pull --rebase upstream main git push --force-with-lease origin my-new-feature ``` ### Updating pull requests If your PR fails to pass CI or needs changes based on code review, you'll most likely want to squash these changes into existing commits. If your pull request contains a single commit or your changes are related to the most recent commit, you can simply amend the commit. ``` shell git add . git commit --amend git push --force-with-lease origin my-new-feature ``` If you need to squash changes into an earlier commit, you can use: ``` shell git add . git commit --fixup git rebase -i --autosquash main git push --force-with-lease origin my-new-feature ``` Be sure to add a comment to the PR indicating your new changes are ready to review, as GitHub does not generate a notification when you git push. ### Code Style ### Formatting Commit Messages We follow the conventions on [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/). Be sure to include any related GitHub issue references in the commit message. See [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) for referencing issues and commits. ## Reporting Bugs and Creating Issues When opening a new issue, try to roughly follow the commit message format conventions above. pytest-shell-utilities-1.9.7/LICENSE000066400000000000000000000261351470601045300172120ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. pytest-shell-utilities-1.9.7/README.rst000066400000000000000000000066231470601045300176740ustar00rootroot00000000000000.. image:: https://img.shields.io/github/actions/workflow/status/saltstack/pytest-shell-utilities/testing.yml?style=plastic&branch=main :target: https://github.com/saltstack/pytest-shell-utilities/actions/workflows/testing.yml :alt: CI .. image:: https://readthedocs.org/projects/pytest-shell-utilities/badge/?style=plastic :target: https://pytest-shell-utilities.readthedocs.io :alt: Docs .. image:: https://img.shields.io/codecov/c/github/saltstack/pytest-shell-utilities?style=plastic&token=ctdrjPj4mc :target: https://codecov.io/gh/saltstack/pytest-shell-utilities :alt: Codecov .. image:: https://img.shields.io/pypi/pyversions/pytest-shell-utilities?style=plastic :target: https://pypi.org/project/pytest-shell-utilities :alt: Python Versions .. image:: https://img.shields.io/pypi/wheel/pytest-shell-utilities?style=plastic :target: https://pypi.org/project/pytest-shell-utilities :alt: Python Wheel .. image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=plastic :target: https://github.com/psf/black :alt: Code Style: black .. image:: https://img.shields.io/pypi/l/pytest-shell-utilities?style=plastic :alt: PyPI - License .. include-starts-here ============================== What is Pytest Shell Utilities ============================== "When in doubt, shell out" -- Thomas S. Hatch This pytest plugin was extracted from `pytest-salt-factories`_. If provides a basic fixture ``shell`` which basically uses ``subprocess.Popen`` to run commands against the running system on a shell while providing a nice assert'able return class. .. _pytest-salt-factories: https://github.com/saltstack/pytest-salt-factories Install ======= Installing ``pytest-shell-utilities`` is as simple as: .. code-block:: bash python -m pip install pytest-shell-utilities And, that's honestly it. Usage ===== Once installed, you can now use the ``shell`` fixture to run some commands and assert against the outcome. .. code-block:: python def test_assert_good_exitcode(shell): ret = shell.run("exit", "0") assert ret.returncode == 0 def test_assert_bad_exitcode(shell): ret = shell.run("exit", "1") assert ret.returncode == 1 If the command outputs parseable JSON, the ``shell`` fixture can attempt loading that output as JSON which allows for asserting against the JSON loaded object. .. code-block:: python def test_against_json_output(shell): d = {"a": "a", "b": "b"} ret = shell.run("echo", json.dumps(d)) assert ret.data == d Additionally, the return object's ``.stdout`` and ``.stderr`` can be line matched using `pytest.pytester.LineMatcher`_: .. code-block:: python MARY_HAD_A_LITTLE_LAMB = """\ Mary had a little lamb, Its fleece was white as snow; And everywhere that Mary went The lamb was sure to go. """ def test_matcher_attribute(shell): ret = shell.run("echo", MARY_HAD_A_LITTLE_LAMB) ret.stdout.matcher.fnmatch_lines_random( [ "*had a little*", "Its fleece was white*", "*Mary went", "The lamb was sure to go.", ] ) .. _pytest.pytester.LineMatcher: https://docs.pytest.org/en/stable/reference.html#pytest.pytester.LineMatcher .. include-ends-here Documentation ============= The full documentation can be seen `here `_. pytest-shell-utilities-1.9.7/changelog/000077500000000000000000000000001470601045300201255ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/changelog/.gitkeep000066400000000000000000000000001470601045300215440ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/changelog/47.improvement.rst000066400000000000000000000001041470601045300234500ustar00rootroot00000000000000Added support for psutil 6.0.0, connections becomes net_connections pytest-shell-utilities-1.9.7/changelog/53.improvement.rst000066400000000000000000000000621470601045300234500ustar00rootroot00000000000000Adjusted requirment for psutil to 5.8.0 or higher pytest-shell-utilities-1.9.7/changelog/54.improvement.rst000066400000000000000000000000541470601045300234520ustar00rootroot00000000000000Need psutil >= 6.0.0 to use net_connections pytest-shell-utilities-1.9.7/changelog/55.improvement.rst000066400000000000000000000001431470601045300234520ustar00rootroot00000000000000Revert requirements for psutil and allow for connections / net_connections based on psutil version pytest-shell-utilities-1.9.7/docs/000077500000000000000000000000001470601045300171265ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/docs/Makefile000066400000000000000000000011721470601045300205670ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) pytest-shell-utilities-1.9.7/docs/_static/000077500000000000000000000000001470601045300205545ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/docs/_static/.gitkeep000066400000000000000000000000001470601045300221730ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/docs/_static/css/000077500000000000000000000000001470601045300213445ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/docs/_static/css/inline-include.css000066400000000000000000000006511470601045300247570ustar00rootroot00000000000000.literal-block-wrapper, article div[class^="highlight-default"] { margin-top: 0.4em; } .literal-block-wrapper .code-block-caption { text-align: left; } .literal-block-wrapper .code-block-caption .caption-text { padding: 0.5em 0.7em; border: .1em solid var(--color-code-background); border-radius: 0.6rem 0.6rem 0 0; background-color: var(--color-code-background); font-family: var(--font-stack--monospace); } pytest-shell-utilities-1.9.7/docs/_static/img/000077500000000000000000000000001470601045300213305ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/docs/_static/img/SaltProject_Logomark_teal.png000066400000000000000000000241671470601045300271420ustar00rootroot00000000000000PNG  IHDRbZ pHYs&|&| IDATxmܶ.u/`vvre` (H#,%@`_p>Tf5c0g|?HWwMY!M@U}2pB=@@M?ȃ :;ȇ 2paw@bw /.pqALj ii PysAt@tH@wp wZ5( :į .1jPtە@tTwoCe ^j :DavHDF PtHwC_q0J@HT}wV w0܍\.pesAU}r{ ks6TݛBo :lK_:ldU{e@;lcUst^8~+jվ;+tXԪqAu]7VR0 wXZ59.F8 u9OqAU%{B0c9:,8 Q#qj\`pfVݛBo1\`~`FU]UNw0܍S|9p*tAw/빗<ԪgqA3Uk8gj^!p.Z[= \\S727`N:iU07`N:Gn|sHH$t8\+KqAT}:ŬaZs$3{V Xs='kԪkH<껗!/kpA59tx@woBkqA5I@?T}wV XOU{z\w{؂ :LZol~j؊ :U"߹(^wpJ-PV6gl59tV :%} :Ej.l@\a6#qE+tJ @\(FwC_l :%im@{V O0܍PpU}r{ :ks .d7!ކvTdKj@J|@vZ[=R@9t2ժ}U 5.䦵Q E.dC2trre@tP0 RwV ȁ :9hs u.$M tRa8 :ɪm= :IQ#q$G#tR @n\HJwC_l ȍ :0%dT}wV ȕOH0܍\pU}r{Ȗ :)hs w.D7!ޖܹ;j@t5ժ!>q JSڭpA V{( :љjվ Ptbte+@it2ժ] PظEFwp/l(GZ5t.Ģ^P2tbP:MU}V\-U}רU#qlB\J+ꪾ{BM'tИ:tVUݥZ5Lxo.i/<UT}r{.E\XZ5繠֔&V)<',fUs繠F88 :jվ.a\XʕN@`vSڅN@` GUw S8GZ5ӹ0V88 :ui& p:U}Vytzp&T}רU8G8Yw/C7^n8 :hsypބz :jL`>:GR|Qw9\8T1ZX :Q,tz gU}7> ʤwV `.<Ԫ\:t4ժ]:tz"T}7> d8~V `.uዉ_ Z5x`;.9\ Wzv sA(XwoB\ʦV :@ !8@SڭL{ ..jվ;@\\a8D@\req Q0 O V ~.ehsdN@\a8mŽ'䭵_4QdH@z\ iqALwC_ -.0@tT}wV M>q0܍p.t T}r{ Q.yP8tU}&#@\ҧV :@¦ZWv>$jUs<k/ AS7ȇ :@ /:@bZ {ȋs n|#qP7tt49@\Pȗ :@Z{țުUȟ?sQPDJ@Y\ pAPwoB NE@LwjwLx<.q erADw/CL.h\.P :@\ 'llU{ee;Z¿l.js :vԪ+t\=?j.tm`eUC/_y$`Ejx :Z!/ C\Ӛ5VP[j<' r;OqAXZ5^Ns/$te59pAXHwoBpt4f tT}wV c`fSڭp ts0V훙p,ty )\fV s 3n|Yp*IspA8_#p.t3U`..2? hU0? 'n|0IKpA8^+07t#T}: :qZ` :TR|pa/ts^Ns/t59KsAxBwoB4tU`:#ZW|VX :9krAT\X :< \~V <؂0n|yU .59[rAV x@Ѫ{B(}lO@Jז> ŪQ@,<IqAJ (NwoBtD(JwjO܁bLx% (Bw/빗 :P jDȞZ5Reb'Yj^2;VH :p@*\Ё,MjlTYR"ٙj.l@\Hda @j<dC)sArBwC_lThm @{V @\H$F9H^n@\ЁU .@{Bm\jl@rT@n|$Oy HTȑ :ֶȕ :jH9YЁMjl TvBM3t vp@ \ЁhU$.@̮lR@Z :+s"ѩn|PQQ@\Ёش9%rAQD.@LZT:ުUd>q67= wvJ`/P:t`SU߽^nh.\Ё U}&jЁMT}wV ~;V't` {~jUf;t`m\ЁըUǹkr=G*{e0SsA4t`Qj0.ҮL'm„y:$jp XDwp/L8`vjx.Z̪!/ qAa88̦Kjpr;^8ӹgt=r;C#y\ЁT}&Л"8Z5lU{ep>'jnj/|\ЁMjLp03t(j`.L 'n||D,8T#r\ЁgU}:Ť`9.!ԪtIU߽ !\,K@z+ЁGU}רUux$xZ5X :V8M7X :T`]:.ժ|a/\Ё_s؆ :_U߽^n ?U j .@t(T9|jne t(T .P+xPV :"#Ca{aAAԪ@\С,pqrABT}:ž N.P֮ ^:껷j n:"?U:.piG_~՟ waW#ȟqA@x]~ :"@aWc7Ps7^,:G9H@|ye .P(~շe c`:aV &l:looV :l/N۸ !f 6jtXgjtX_ctX׵Z5!:xiժ`wîv=%: {jtaW_%p*7G:Z5\:iժstwjՀpF0NY0'NӘ0'wV Q,B@ ]"t8OہpVYK0txaWߘ$6֪yXOkԪkqcZk>txܥka]l0jtۻaWߚ &~7֪yX۫U O_]}etpftZ%? ט%Bhժ[(ݝZ5 :kԪ1(gj@,tJV @Zb!P{s 6:%j= F@4wînl@i.m@IZO6H@$@tJ~շ J@cဨ QN@ w_]2;mH@>UR!3s :zV H@Z5I^|vMc@trrV H@.]ρ UL@ wjՀ @<ԍjlH@6@@ ecڍ 9Hս9T5r"aWU"K[r#V퓭H9%v9HX+Tժ9HaW_3m ȝ@>UJ ;s:1{V (@BUV (@>UJ#VZP"ܻIV (@,]zK@ {J&VM%9P<v-ؒZ5aWc:kZN@`+&ZO&\ wî5u iU0tִW0|viUx]'VttV 0:KjԪF@`)ժN@`)N@` j#07j'[a8 n{p9D:sk>&itriaWߚ$t5֪yL:jԪO@cZk8f"pîdz8#pwj%pVp38^tuW&0?cx`!:VC5&Cj%;jxZ5+@Zt$H@aW)@zt|j}I@^@t<|v]K@&Z5 s=Ȁwj .@tt5j!Z5ij /:@zժG@H˽Z5< ii= ' wîsL i;@t4j _:@\2'o o:@Z5@@[V :@Z~ n!iUd7zP >]}k/e2֪y@:@\j$밫L:@7si\XLXkjlA@?j؊ݰ]،ؒV9%@Z҇tdB@J b kZ :PK &:PV(9Ҽv(X81ժ+(aW_6Rm @ >U v:{sR kժm@jB@rY)\56 @Jt GjHFIܴ E:aW9Id @.Z @t $M@r~76 @t ucHQ@t ecZk@@Rvi{B@R5֪}=r!r= +:wî9r"k< @vt 5{jH@RuW6@t %{ W:kjL@R0> 9jȝN%5j(Z5J!1S@1t VcڍP ѽ94:aW7@it 6>mH:V탍P"ɥmP*aWjh:F}vuk N@V aWmD@N$[k< -ժt`m]}e;X[c7XӵZ5x'kiժt` wji:j XXi: t`Icڍ t`)p8XJa88,nՍt` jL#ss=Dx:0Vpp"^N@uW& 9M#V 'r=9ީUyB<tTZ5Z515ZO@qzcj`:paWBtP{!Z&Ct9cڍ)t)xJ3L'kZuc.M#k> G@z+?vtWc`:Z5؆uW&Gj-w= @V '@Ԫ@t([V @>UxP :ZE@U Ppr7j= P@t(X N:"&@Ʈ ^:oU0DN@5j ~:mUk'@. @>@tȗZ5Hyz7[tVpW /_]}ea8HV p]@tCV BCSIENDB`pytest-shell-utilities-1.9.7/docs/_static/img/SaltProject_altlogo_teal.png000066400000000000000000000225671470601045300270320ustar00rootroot00000000000000PNG  IHDRdNn pHYs yj IDATxqV7`d'; ,V`+D[ "W`DnKDjUs,#H<3;$%sϽ|+`       |!VUu"=󢪪MUU.88&'h~]O&iUUo7VCx۞b~.:5Y"p?#]}Wwg,.):SW7!q*B#[żjp_hO8Jxh.e9&8,z<;w +(Qױ*u<~;st͋zJc Y;*=cQ弼LJb%nx.LN7.x9Y̏UC@ XS^BIPsy^9 @n:;3YT{yL}zZ==$%йLiWܙ lry t*)PX:*:@b:Y /_;Up>xT^}TޅJ&n١tSQF[;C ~ξ&8 :4YsHL8ƴd) Hj0Ryhfq(T:}7*sPb,!VW~HD@Wf:x|,`*:@":}srzקt6^=HB@7b'W r?\_: 9cPEHB5d4*oz>ѫJ0o3o:>` ײ)qma2hf,窪qz9]$Q,"6:*r:Lp,%d1?I.Cg:_1"BA/owƺ\!쀀NztSga(2ӽU>tvݫVkN`tRI軣_}n !Ϡ4Y̏\`ˈ}CUt=L[5+g}bͣ~_fpSWk{$VF Tiju=voQۛ?@],'O݀ct2se?c/GtZ9Y JtaO9ZKm IHwSz/RG3 k[eYP$tbc87/V!i 4 {_"܏RT2 \X> dtWS6y[zCo7NS=? R}G{I%Pj=iQl"tscܱz9Y*sUtT[5`] W>d9]e=IS=0^:i j&Ti'QM7MVk#$Qz`So jzl$7vY(QEϲ ڪ8W飩xdؐy-K0.Ut쀀imrٿd1 '1֞=K_} @t281O❿_WU[uFO, tY螀^E뭫umW&%2YanZ**:@w~3Sۡ߮T":o'y/UU]_s?YvI]Ei@Gt&*Y]˞~_=M+_ כyb1IH/U3٧Q߅|Raw!|:\ҬC!I*:EkôXjߌmoW*uh|btM^ޥ0:;7IFa8.|ҳM 9eyYe8 >jr&?YJhև>d)ɾoʒ jna@Mgp_JXz絳D3Ktvp0Lǫ&y,9/P}t;-ٙb~&J@E˸wc&Çd1/N!Uk H@g޵ة<.o5gc QL0\:;,БiMyL5*'#eS65Ut\@"s+ˎf}pxMY6.l=&&8م3m`ED7{T Aə&?^/֘5DMgRjcJ}oQlY :k)Y Ў) Q}?~س)/|ڢz. OQEK@g]|<_H{Pa?h1P=x?Bt,6Q6YC4.Rq<{]? %ӈ#%2}@/rQU簋 0ۘ,%%w%3[aj *::O]8Sb~H( v>nxTV4'4|;1xcxDѫt7ڪ{{_tfִξ1@<ō#`#3be}d1x3]|}Q=W1`ts )aX}=_G[S  ^%\u mn|n޹UGIJ@փ,-u~nڨ䫦7b3+6b,;oUAsTсй9رvjm,{l;]u\`t"nY7 P]":֨۳Ź4q@A=U܋.;U=?;h֦R59v~m./ f9]oT>b_GMAJܥtfF!4MIf޼LJ1،/[M?QK}9-{:M~r:uMےT//Y+T1[L=I|6WUR~dx;Y YJ_m9%sKu(.7sVсqcm>z3oa}\f5D0Ɲ)pUt`th,6&ύgwC{,Dh;ׇv*_Bw~}@6zO$%XH|B= 7!dV Йi=_ +zcqi֞i BX$zg|4A{'G# 4QȎyFQ]PjbSDQan{f1[T?d8BaNLӣc j7:祊?:,Zh&ZPo1z*:;Ftvhw5G物쇀iYh9>Z=*: н[ QLy/=?R8mOuP=@* ;V*돗ّjhn<,Uϟ-4Y_$8NЭw8>Y[>\%}x=UdG%CNUc,^?Me{_ 罖-QEJ@|vNۏ +oopT{*~쀀ݱ1VnYrXm;H|Tv@@n|lb};A7uq9tgɪY*:n>mX~M}ϫpV9o3Z|3 5:pp:wks-P*ЮOXY>-B*^.;i\Nq&ah2T,Zse`1X-Sڪ]82ze{]U\9}R=4Ut|BkFTի*􎪪vV;"TOr}'zxtthP0v13)Q%zpUc.[?uR=?$zWcptޭo+B]C?Āݨ*:@7t^W+Y+uO|7ϗ#91hq>j1н/b;o]{hh)mLb J`a%Ph\Uu]N]%vl I̖?7Eu!Yxgy\ 0l:lhVB߿+^ov`V_p/6wx&kEV hOh;V hΝ3m:p@'thj@Wthl w\]С3m. m 4m 9&UvB@jW :V%wfc8`tr:;u^][ k:Uiv&_{!_Ng:U17:T[5`t/yI@:q}Oڪ茝9ΘV B@gJ[5iՉj@&:cy9@&:ctGmtVJ@gLδUtz%3'4j4#z@oLUUAUU?qVv#%LM0G+@N:CWڪ]XL]Qu7Pu45YK?0UϟWwa?>ニrm;pQ;Vf^ Oe{0J:CUڪ;Y ^[Ng>_7u_ KUBW~ŀC,ǓKk!`9 'SH4Vh-X7܄ݹκhsđ`9XP]ShKukvְFHinZNgmM7ZԱ$!w@ {Or*|lvp'jZM׫w5M?H5mef ޞX|ݠ [ؠMչ5v^uAhM|AA2xܼP[kvMںiTwcmYu4 'نa{]f]`/k-[@h3\%]wSY CW}țξk ]~c}wMQ9s8۬o|_:H4Jvb] n0̐e aRAPB9Zyc6:k*~W3bZ@,G<4{lK=h%vm0s,mݳ^[{j=e&!q1hi]6qƵCz+YNX^̔6#ӆ>b%ǿ)x0GkTk($sXBqq+wyqba tiY~-sOyA#_y%P{ /k rUӡ{:0xQ.d>~m0Ux`!6Zc1Դ6K<36y},ޯ۳x^k}s.n>Mu?h9DHx&kۤ%hDUcr#h֥{HW8sH숀r:+ӵ?5|ͯc]VVV7dx씿dg4~9C?%5BϛXtbp_;*6U8;>wt5xr>=ubp:Ps7mᨂDބ`?nY                                |]/tH@@tH@@tH@@tH@@tH@@tH@@tH@@}m<E3IENDB`pytest-shell-utilities-1.9.7/docs/all.rst000066400000000000000000000002441470601045300204300ustar00rootroot00000000000000.. _all the states/modules: Complete List of salt-analytics-framework ========================================= .. toctree:: :maxdepth: 2 ref/modules.rst pytest-shell-utilities-1.9.7/docs/changelog.rst000066400000000000000000000000361470601045300216060ustar00rootroot00000000000000.. include:: ../CHANGELOG.rst pytest-shell-utilities-1.9.7/docs/conf.py000066400000000000000000000140651470601045300204330ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import datetime import os import pathlib import sys try: from importlib_metadata import distribution except ImportError: from importlib.metadata import distribution try: DOCS_BASEPATH = pathlib.Path(__file__).resolve().parent except NameError: # sphinx-intl and six execute some code which will raise this NameError # assume we're in the doc/ directory DOCS_BASEPATH = pathlib.Path(".").resolve().parent REPO_ROOT = DOCS_BASEPATH.parent addtl_paths = ( os.path.join(os.pardir, "src"), # pytest-shell-utilities itself (for autodoc) "_ext", # custom Sphinx extensions ) for addtl_path in addtl_paths: sys.path.insert(0, os.path.abspath(os.path.join(DOCS_BASEPATH, addtl_path))) dist = distribution("pytest-shell-utilities") # -- Project information ----------------------------------------------------- this_year = datetime.datetime.today().year if this_year == 2021: copyright_year = 2021 else: copyright_year = f"2021 - {this_year}" project = dist.metadata["Summary"] author = dist.metadata["Author"] copyright = f"{copyright_year}, {author}" # pylint: disable=redefined-builtin # The full version, including alpha/beta/rc tags release = dist.version # Variables to pass into the docs from sitevars.rst for rst substitution with open("sitevars.rst", encoding="utf-8") as site_vars_file: site_vars = site_vars_file.read().splitlines() rst_prolog = """ {} """.format( "\n".join(site_vars[:]) ) # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.autosummary", "sphinx.ext.napoleon", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinx.ext.todo", "sphinx.ext.coverage", "sphinx_copybutton", "sphinxcontrib.spelling", "sphinxcontrib.towncrier", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [ "_build", "Thumbs.db", ".DS_Store", ".vscode", ".venv", ".git", ".gitlab-ci", ".gitignore", "sitevars.rst", ] autosummary_generate = True modindex_common_prefix = ["saf."] master_doc = "contents" # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "furo" html_title = project # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = "_static/img/SaltProject_altlogo_teal.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. Favicons can be up to at least 228x228. PNG # format is supported as well, not just .ico' html_favicon = "_static/img/SaltProject_Logomark_teal.png" # Sphinx Napoleon Config napoleon_google_docstring = True napoleon_numpy_docstring = False napoleon_include_init_with_doc = True napoleon_include_private_with_doc = False napoleon_include_special_with_doc = True napoleon_use_admonition_for_examples = True napoleon_use_admonition_for_notes = True napoleon_use_admonition_for_references = True napoleon_use_ivar = True napoleon_use_param = True napoleon_use_keyword = True napoleon_use_rtype = True napoleon_attr_annotations = True napoleon_preprocess_types = True # ----- Intersphinx Config ----------------------------------------------------------------------------------------> intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "pytest": ("https://docs.pytest.org/en/stable", None), } # <---- Intersphinx Config ----------------------------------------------------------------------------------------- # ----- Autodoc Config ----------------------------------------------------------------------------------------------> autodoc_default_options = {"member-order": "bysource"} autodoc_mock_imports = [] # <---- Autodoc Config ----------------------------------------------------------------------------------------------- # ----- Towncrier Draft Release -------------------------------------------------------------------------------------> # Options: draft/sphinx-version/sphinx-release towncrier_draft_autoversion_mode = "draft" towncrier_draft_include_empty = True towncrier_draft_working_directory = REPO_ROOT # Not yet supported: # towncrier_draft_config_path = 'pyproject.toml' # relative to cwd # <---- Towncrier Draft Release -------------------------------------------------------------------------------------- def setup(app): app.add_crossref_type( directivename="fixture", rolename="fixture", indextemplate="pair: %s; fixture", ) # Allow linking to pytest's confvals. app.add_object_type( "confval", "pytest-confval", objname="configuration value", indextemplate="pair: %s; configuration value", ) pytest-shell-utilities-1.9.7/docs/contents.rst000066400000000000000000000003361470601045300215170ustar00rootroot00000000000000.. _table-of-contents: ================= Table Of Contents ================= .. toctree:: :maxdepth: 3 ref/pytestshellutils changelog GitHub Repository pytest-shell-utilities-1.9.7/docs/index.rst000066400000000000000000000013201470601045300207630ustar00rootroot00000000000000:orphan: .. _about: .. include:: ../README.rst :start-after: include-starts-here :end-before: include-ends-here Documentation ============= Please see :ref:`Contents ` for full documentation, including installation and tutorials. Bugs/Requests ============= Please use the `GitHub issue tracker`_ to submit bugs or request features. Changelog ========= Consult the :ref:`Changelog ` page for fixes and enhancements of each version. .. _GitHub issue tracker: https://github.com/saltstack/pytest-system-statistics/issues .. toctree:: :maxdepth: 2 :caption: Contents: all.rst Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` pytest-shell-utilities-1.9.7/docs/make.bat000066400000000000000000000013701470601045300205340ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd pytest-shell-utilities-1.9.7/docs/ref/000077500000000000000000000000001470601045300177025ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/docs/ref/modules.rst000066400000000000000000000001251470601045300221020ustar00rootroot00000000000000pytestshellutils ================ .. toctree:: :maxdepth: 4 pytestshellutils pytest-shell-utilities-1.9.7/docs/ref/pytestshellutils.rst000066400000000000000000000020441470601045300240750ustar00rootroot00000000000000pytestshellutils package ======================== .. automodule:: pytestshellutils :members: :undoc-members: :show-inheritance: Subpackages ----------- .. toctree:: :maxdepth: 4 pytestshellutils.utils Submodules ---------- pytestshellutils.customtypes module ----------------------------------- .. automodule:: pytestshellutils.customtypes :members: :undoc-members: :show-inheritance: pytestshellutils.exceptions module ---------------------------------- .. automodule:: pytestshellutils.exceptions :members: :undoc-members: :show-inheritance: pytestshellutils.plugin module ------------------------------ .. automodule:: pytestshellutils.plugin :members: :undoc-members: :show-inheritance: pytestshellutils.shell module ----------------------------- .. automodule:: pytestshellutils.shell :members: :undoc-members: :show-inheritance: pytestshellutils.version module ------------------------------- .. automodule:: pytestshellutils.version :members: :undoc-members: :show-inheritance: pytest-shell-utilities-1.9.7/docs/ref/pytestshellutils.utils.rst000066400000000000000000000015631470601045300252410ustar00rootroot00000000000000pytestshellutils.utils package ============================== .. automodule:: pytestshellutils.utils :members: :undoc-members: :show-inheritance: Submodules ---------- pytestshellutils.utils.ports module ----------------------------------- .. automodule:: pytestshellutils.utils.ports :members: :undoc-members: :show-inheritance: pytestshellutils.utils.processes module --------------------------------------- .. automodule:: pytestshellutils.utils.processes :members: :undoc-members: :show-inheritance: pytestshellutils.utils.socket module ------------------------------------ .. automodule:: pytestshellutils.utils.socket :members: :undoc-members: :show-inheritance: pytestshellutils.utils.time module ---------------------------------- .. automodule:: pytestshellutils.utils.time :members: :undoc-members: :show-inheritance: pytest-shell-utilities-1.9.7/docs/sitevars.rst000066400000000000000000000000001470601045300215060ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/noxfile.py000066400000000000000000000447071470601045300202300ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # # pylint: disable=import-error,protected-access,line-too-long import datetime import gzip import json import os import pathlib import shutil import sys import tarfile import tempfile import nox from nox.command import CommandFailed COVERAGE_VERSION_REQUIREMENT = "coverage==7.2.7" PYTEST_VERSION_REQUIREMENT = os.environ.get("PYTEST_VERSION_REQUIREMENT") or None IS_WINDOWS = sys.platform.lower().startswith("win") IS_DARWIN = sys.platform.lower().startswith("darwin") if IS_WINDOWS: COVERAGE_FAIL_UNDER_PERCENT = 87 elif IS_DARWIN: COVERAGE_FAIL_UNDER_PERCENT = 87 else: COVERAGE_FAIL_UNDER_PERCENT = 87 # Be verbose when running under a CI context PIP_INSTALL_SILENT = (os.environ.get("CI") or os.environ.get("GITHUB_ACTIONS")) is None CI_RUN = PIP_INSTALL_SILENT is False SKIP_REQUIREMENTS_INSTALL = "SKIP_REQUIREMENTS_INSTALL" in os.environ EXTRA_REQUIREMENTS_INSTALL = os.environ.get("EXTRA_REQUIREMENTS_INSTALL") # Paths REPO_ROOT = pathlib.Path(__file__).resolve().parent # Change current directory to REPO_ROOT os.chdir(str(REPO_ROOT)) SITECUSTOMIZE_DIR = str(REPO_ROOT / "tests" / "support" / "coverage") ARTIFACTS_DIR = REPO_ROOT / "artifacts" # Make sure the artifacts directory exists ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True) RUNTESTS_LOGFILE = ARTIFACTS_DIR.relative_to(REPO_ROOT) / "runtests-{}.log".format( datetime.datetime.now().strftime("%Y%m%d%H%M%S.%f") ) COVERAGE_REPORT_DB = REPO_ROOT / ".coverage" COVERAGE_REPORT_PROJECT = ARTIFACTS_DIR.relative_to(REPO_ROOT) / "coverage-project.xml" COVERAGE_REPORT_TESTS = ARTIFACTS_DIR.relative_to(REPO_ROOT) / "coverage-tests.xml" JUNIT_REPORT = ARTIFACTS_DIR.relative_to(REPO_ROOT) / "junit-report.xml" # Nox options # Reuse existing virtualenvs nox.options.reuse_existing_virtualenvs = True # Don't fail on missing interpreters nox.options.error_on_missing_interpreters = False def pytest_version(session): try: return session._runner._pytest_version_info except AttributeError: session_pytest_version = session_run_always( session, "python", "-c", 'import sys, pkg_resources; sys.stdout.write("{}".format(pkg_resources.get_distribution("pytest").version))', silent=True, log=False, ) session._runner._pytest_version_info = tuple( int(part) for part in session_pytest_version.split(".") if part.isdigit() ) return session._runner._pytest_version_info def python_version(session): try: return session._runner._python_version_info except AttributeError: session_python_version = session_run_always( session, "python", "-c", 'import sys; sys.stdout.write("{}.{}".format(*sys.version_info));', silent=True, log=False, ) session._runner._python_version_info = tuple( int(part) for part in session_python_version.split(".") if part.isdigit() ) return session._runner._python_version_info def session_run_always(session, *command, **kwargs): try: # Guess we weren't the only ones wanting this # https://github.com/theacodes/nox/pull/331 return session.run_always(*command, **kwargs) except AttributeError: old_install_only_value = session._runner.global_config.install_only try: # Force install only to be false for the following chunk of code # For additional information as to why see: # https://github.com/theacodes/nox/pull/181 session._runner.global_config.install_only = False return session.run(*command, **kwargs) finally: session._runner.global_config.install_only = old_install_only_value @nox.session(python=("3", "3.7", "3.8", "3.9", "3.10", "3.11")) def tests(session): """ Run tests. """ env = {} install_arguments = ["--progress-bar=off"] if SKIP_REQUIREMENTS_INSTALL is False: env["COVERAGE_PLUGINS_LIST"] = "coverage_conditional_plugin" coverage_requirements = [COVERAGE_VERSION_REQUIREMENT, "coverage-conditional-plugin"] session.install(*install_arguments, "wheel", silent=PIP_INSTALL_SILENT) session.install( *install_arguments, *coverage_requirements, silent=PIP_INSTALL_SILENT, ) pytest_version_requirement = PYTEST_VERSION_REQUIREMENT if pytest_version_requirement: if not pytest_version_requirement.startswith("pytest"): if ( not pytest_version_requirement.startswith(("==", "~=")) and pytest_version_requirement.endswith(".0") and not pytest_version_requirement.startswith("~=") ): pytest_version_requirement = f"~={pytest_version_requirement}" pytest_version_requirement = f"pytest{pytest_version_requirement}" session.install(pytest_version_requirement, silent=PIP_INSTALL_SILENT) session.install(*install_arguments, "-e", ".[tests]", silent=PIP_INSTALL_SILENT) if EXTRA_REQUIREMENTS_INSTALL: session.log( "Installing the following extra requirements because the EXTRA_REQUIREMENTS_INSTALL " "environment variable was set: EXTRA_REQUIREMENTS_INSTALL='%s'", EXTRA_REQUIREMENTS_INSTALL, ) install_command = [req.strip() for req in EXTRA_REQUIREMENTS_INSTALL.split()] session.install(*install_arguments, *install_command, silent=PIP_INSTALL_SILENT) session.run("coverage", "erase") python_path_env_var = os.environ.get("PYTHONPATH") or None if python_path_env_var is None: python_path_env_var = SITECUSTOMIZE_DIR else: python_path_entries = python_path_env_var.split(os.pathsep) if SITECUSTOMIZE_DIR in python_path_entries: python_path_entries.remove(SITECUSTOMIZE_DIR) python_path_entries.insert(0, SITECUSTOMIZE_DIR) python_path_env_var = os.pathsep.join(python_path_entries) env.update( { # The updated python path so that sitecustomize is importable "PYTHONPATH": python_path_env_var, # The full path to the .coverage data file. Makes sure we always write # them to the same directory "COVERAGE_FILE": str(COVERAGE_REPORT_DB), # Instruct sub processes to also run under coverage "COVERAGE_PROCESS_START": str(REPO_ROOT / ".coveragerc"), } ) args = [ "--rootdir", str(REPO_ROOT), "--log-file={}".format(RUNTESTS_LOGFILE), "--log-file-level=debug", "--show-capture=no", "--junitxml={}".format(JUNIT_REPORT), "--showlocals", "--strict-markers", "-ra", "-s", ] if pytest_version(session) > (6, 2): args.append("--lsof") if session._runner.global_config.forcecolor: args.append("--color=yes") if not session.posargs: args.append("tests/") else: for arg in session.posargs: if arg.startswith("--color") and session._runner.global_config.forcecolor: args.remove("--color=yes") args.append(arg) try: session.run("python", "-bb", "-m", "coverage", "run", "-m", "pytest", *args, env=env) finally: # Always combine and generate the XML coverage report try: session.run("coverage", "combine") except CommandFailed: # Sometimes some of the coverage files are corrupt which would # trigger a CommandFailed exception pass # Generate report for project code coverage session.run( "coverage", "xml", "-o", str(COVERAGE_REPORT_PROJECT), "--omit=tests/*", "--include=src/pytestshellutils/*", ) # Generate report for tests code coverage session.run( "coverage", "xml", "-o", str(COVERAGE_REPORT_TESTS), "--omit=src/pytestshellutils/*", "--include=tests/*", ) try: cmdline = [ "coverage", "report", "--show-missing", "--include=src/pytestshellutils/*,tests/*", ] if pytest_version(session) >= (6, 2): cmdline.append("--fail-under={}".format(COVERAGE_FAIL_UNDER_PERCENT)) session.run(*cmdline) finally: if COVERAGE_REPORT_DB.exists(): shutil.copyfile(str(COVERAGE_REPORT_DB), str(ARTIFACTS_DIR / ".coverage")) def _lint(session, rcfile, flags, paths): session.install("--progress-bar=off", "-e", ".[lint]", silent=PIP_INSTALL_SILENT) session.run("pylint", "--version") pylint_report_path = os.environ.get("PYLINT_REPORT") cmd_args = ["pylint", "--rcfile={}".format(rcfile)] + list(flags) + list(paths) stdout = tempfile.TemporaryFile(mode="w+b") try: session.run(*cmd_args, stdout=stdout) finally: stdout.seek(0) contents = stdout.read() if contents: contents = contents.decode("utf-8") sys.stdout.write(contents) sys.stdout.flush() if pylint_report_path: # Write report with open(pylint_report_path, "w", encoding="utf-8") as wfh: wfh.write(contents) session.log("Report file written to %r", pylint_report_path) stdout.close() @nox.session(python="3") def lint(session): """ Run PyLint against Salt and it's test suite. Set PYLINT_REPORT to a path to capture output. """ session.notify("lint-code-{}".format(session.python)) session.notify("lint-tests-{}".format(session.python)) @nox.session(python="3", name="lint-code") def lint_code(session): """ Run PyLint against the code. Set PYLINT_REPORT to a path to capture output. """ flags = ["--disable=I"] if session.posargs: paths = session.posargs else: paths = ["setup.py", "noxfile.py", "src/pytestshellutils/"] _lint(session, ".pylintrc", flags, paths) @nox.session(python="3", name="lint-tests") def lint_tests(session): """ Run PyLint against Salt and it's test suite. Set PYLINT_REPORT to a path to capture output. """ flags = ["--disable=I"] if session.posargs: paths = session.posargs else: paths = ["tests/"] _lint(session, ".pylintrc", flags, paths) @nox.session(python="3") def docs(session): """ Build Docs. """ session.install("--progress-bar=off", "-e", ".[docs]", silent=PIP_INSTALL_SILENT) os.chdir("docs/") session.run("make", "clean", external=True) # session.run("make", "linkcheck", "SPHINXOPTS=-W", external=True) session.run("make", "coverage", "SPHINXOPTS=-W", external=True) docs_coverage_file = os.path.join("_build", "html", "python.txt") if os.path.exists(docs_coverage_file): with open(docs_coverage_file, encoding="utf-8") as rfh: contents = rfh.readlines()[2:] if contents: session.error("\n" + "".join(contents)) session.run("make", "html", "SPHINXOPTS=-W", external=True) os.chdir("..") @nox.session(name="docs-dev", python="3") def docs_dev(session): """ Build Docs. """ session.install("--progress-bar=off", "-e", ".[docs]", silent=PIP_INSTALL_SILENT) os.chdir("docs/") session.run("make", "html", "SPHINXOPTS=-W", external=True, env={"LOCAL_DEV_BUILD": "1"}) os.chdir("..") @nox.session(name="docs-crosslink-info", python="3") def docs_crosslink_info(session): """ Report intersphinx cross links information. """ session.install("--progress-bar=off", "-e", ".[docs]", silent=PIP_INSTALL_SILENT) os.chdir("docs/") intersphinx_mapping = json.loads( session.run( "python", "-c", "import json; import conf; print(json.dumps(conf.intersphinx_mapping))", silent=True, log=False, ) ) try: mapping_entry = intersphinx_mapping[session.posargs[0]] except IndexError: session.error( "You need to pass at least one argument whose value must be one of: {}".format( ", ".join(list(intersphinx_mapping)) ) ) except KeyError: session.error( "Only acceptable values for first argument are: {}".format( ", ".join(list(intersphinx_mapping)) ) ) session.run( "python", "-m", "sphinx.ext.intersphinx", mapping_entry[0].rstrip("/") + "/objects.inv" ) os.chdir("..") @nox.session(name="gen-api-docs", python="3") def gen_api_docs(session): """ Generate API Docs. """ session.install("--progress-bar=off", "-e", ".[docs]", silent=PIP_INSTALL_SILENT) shutil.rmtree("docs/ref", ignore_errors=True) session.run("sphinx-apidoc", "--module-first", "-o", "docs/ref/", "src/pytestshellutils/") @nox.session(name="twine-check", python="3") def twine_check(session): """ Run ``twine-check`` against the source distribution package. """ build(session) session.run("twine", "check", "dist/*") @nox.session(name="changelog", python="3") @nox.parametrize("draft", [False, True]) def changelog(session, draft): """ Generate changelog. """ session.install("--progress-bar=off", "-e", ".[changelog]", silent=PIP_INSTALL_SILENT) version = session.run( "python", "setup.py", "--version", silent=True, log=False, stderr=None, ).strip() town_cmd = ["towncrier", "build", "--version={}".format(version)] if draft: town_cmd.append("--draft") session.run(*town_cmd) @nox.session(name="release") def release(session): """ Create a release tag. """ if not session.posargs: session.error( "Forgot to pass the version to release? For example `nox -e release -- 1.1.0`" ) if len(session.posargs) > 1: session.error( "Only one argument is supported by the `release` nox session. " "For example `nox -e release -- 1.1.0`" ) version = session.posargs[0] try: session.log("Generating temporary %s tag", version) session.run("git", "tag", "-as", version, "-m", "Release {}".format(version), external=True) changelog(session, draft=False) except CommandFailed: session.error("Failed to generate the temporary tag") # session.notify("changelog(draft=False)") try: session.log("Generating the release changelog") session.run( "git", "commit", "-a", "-m", "Generate Changelog for version {}".format(version), external=True, ) except CommandFailed: session.error("Failed to generate the release changelog") try: session.log("Overwriting temporary %s tag", version) session.run( "git", "tag", "-fas", version, "-m", "Release {}".format(version), external=True ) except CommandFailed: session.error("Failed to overwrite the temporary tag") session.warn("Don't forget to push the newly created tag") class Recompress: """ Helper class to re-compress a ``.tag.gz`` file to make it reproducible. """ def __init__(self, mtime): self.mtime = int(mtime) def tar_reset(self, tarinfo): """ Reset user, group, mtime, and mode to create reproducible tar. """ tarinfo.uid = tarinfo.gid = 0 tarinfo.uname = tarinfo.gname = "root" tarinfo.mtime = self.mtime if tarinfo.type == tarfile.DIRTYPE: tarinfo.mode = 0o755 else: tarinfo.mode = 0o644 if tarinfo.pax_headers: raise ValueError(tarinfo.name, tarinfo.pax_headers) return tarinfo def recompress(self, targz): """ Re-compress the passed path. """ tempd = pathlib.Path(tempfile.mkdtemp()).resolve() d_src = tempd.joinpath("src") d_src.mkdir() d_tar = tempd.joinpath(targz.stem) d_targz = tempd.joinpath(targz.name) with tarfile.open(d_tar, "w|") as wfile: with tarfile.open(targz, "r:gz") as rfile: rfile.extractall(d_src) # nosec extracted_dir = next(pathlib.Path(d_src).iterdir()) for name in sorted(extracted_dir.rglob("*")): wfile.add( str(name), filter=self.tar_reset, recursive=False, arcname=str(name.relative_to(d_src)), ) with open(d_tar, "rb") as rfh: with gzip.GzipFile( fileobj=open(d_targz, "wb"), mode="wb", filename="", mtime=self.mtime ) as gz: while True: chunk = rfh.read(1024) if not chunk: break gz.write(chunk) targz.unlink() shutil.move(str(d_targz), str(targz)) @nox.session(python="3") def build(session): """ Build source and binary distributions based off the current commit author date UNIX timestamp. The reason being, reproducible packages. .. code-block: shell git show -s --format=%at HEAD """ shutil.rmtree("dist/", ignore_errors=True) session.install("--progress-bar=off", "-r", "requirements/build.txt", silent=PIP_INSTALL_SILENT) timestamp = session.run( "git", "show", "-s", "--format=%at", "HEAD", silent=True, log=False, stderr=None, ).strip() env = {"SOURCE_DATE_EPOCH": str(timestamp)} session.run( "python", "-m", "build", "--sdist", "--wheel", str(REPO_ROOT), env=env, ) # Recreate sdist to be reproducible recompress = Recompress(timestamp) for targz in REPO_ROOT.joinpath("dist").glob("*.tar.gz"): session.log("Re-compressing %s...", targz.relative_to(REPO_ROOT)) recompress.recompress(targz) sha256sum = shutil.which("sha256sum") if sha256sum: packages = [str(pkg.relative_to(REPO_ROOT)) for pkg in REPO_ROOT.joinpath("dist").iterdir()] session.run("sha256sum", *packages, external=True) session.run("python", "-m", "twine", "check", "dist/*") pytest-shell-utilities-1.9.7/pyproject.toml000066400000000000000000000023441470601045300211150ustar00rootroot00000000000000[build-system] requires = ["setuptools>=50.3.2", "setuptools-declarative-requirements", "setuptools_scm[toml]>=3.4"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "src/pytestshellutils/version.py" write_to_template = "# pylint: skip-file\n\n__version__ = \"{version}\"\n" [tool.towncrier] package = "pytestshellutils" filename = "CHANGELOG.rst" directory = "changelog/" issue_format = "`#{issue} `_" title_format = "shell-utilities {version} ({project_date})" [[tool.towncrier.type]] directory = "breaking" name = "Breaking Changes" showcontent = true [[tool.towncrier.type]] directory = "deprecation" name = "Deprecations" showcontent = true [[tool.towncrier.type]] directory = "feature" name = "Features" showcontent = true [[tool.towncrier.type]] directory = "improvement" name = "Improvements" showcontent = true [[tool.towncrier.type]] directory = "bugfix" name = "Bug Fixes" showcontent = true [[tool.towncrier.type]] directory = "doc" name = "Improved Documentation" showcontent = true [[tool.towncrier.type]] directory = "trivial" name = "Trivial/Internal Changes" showcontent = true pytest-shell-utilities-1.9.7/pytest.ini000066400000000000000000000006401470601045300202270ustar00rootroot00000000000000[pytest] log_file_level=debug log_date_format=%H:%M:%S log_cli_format=%(asctime)s,%(msecs)03.0f [%(name)-5s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)s)] %(message)s log_file_format=%(asctime)s,%(msecs)03d [%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s junit_family=xunit2 python_files=test_*.py python_classes=Test* python_functions=test_* testpaths=tests/ pytest-shell-utilities-1.9.7/requirements/000077500000000000000000000000001470601045300207215ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/requirements/base.txt000066400000000000000000000001271470601045300223740ustar00rootroot00000000000000pytest>=7.4.0 attrs>=22.1.0 psutil>=5.0.0 pytest-helpers-namespace pytest-skip-markers pytest-shell-utilities-1.9.7/requirements/build.txt000066400000000000000000000000231470601045300225540ustar00rootroot00000000000000twine build>=0.7.0 pytest-shell-utilities-1.9.7/requirements/changelog.txt000066400000000000000000000000251470601045300234060ustar00rootroot00000000000000towncrier==21.9.0rc1 pytest-shell-utilities-1.9.7/requirements/docs.txt000066400000000000000000000002451470601045300224130ustar00rootroot00000000000000-r base.txt -r tests.txt -r changelog.txt towncrier <= 23.11.0 furo sphinx sphinx-copybutton sphinx-prompt sphinxcontrib-spelling sphinxcontrib-towncrier >= 0.2.1a0 pytest-shell-utilities-1.9.7/requirements/lint.txt000066400000000000000000000003121470601045300224240ustar00rootroot00000000000000-r base.txt -r tests.txt pylint==2.12.2 pyenchant black; python_version >= '3.7' reorder-python-imports; python_version >= '3.7' flake8 >= 4.0.1 flake8-mypy-fork flake8-docstrings flake8-typing-imports pytest-shell-utilities-1.9.7/requirements/tests.txt000066400000000000000000000000601470601045300226200ustar00rootroot00000000000000-r base.txt pytest-subtests pytest-skip-markers pytest-shell-utilities-1.9.7/setup.cfg000066400000000000000000000101771470601045300200250ustar00rootroot00000000000000[metadata] name = pytest-shell-utilities description = Pytest plugin to simplify running shell commands against the system long_description = file: README.rst long_description_content_type = text/x-rst author = Pedro Algarvio author_email = pedro@algarvio.me url = https://github.com/saltstack/pytest-shell-utilities project_urls = Source=https://github.com/saltstack/pytest-shell-utilities Tracker=https://github.com/saltstack/pytest-shell-utilities/issues Documentation=https://pytest-shell-utilities.readthedocs.io license = Apache Software License 2.0 classifiers = Programming Language :: Python Programming Language :: Cython Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Typing :: Typed Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: Apache Software License platforms = unix, linux, osx, cygwin, win32 [options] zip_safe = False include_package_data = True package_dir = =src packages = find: python_requires = >= 3.8 setup_requires = setuptools>=50.3.2 setuptools_scm[toml]>=3.4 setuptools-declarative-requirements [options.packages.find] where = src exclude = tests* [requirements-files] install_requires = requirements/base.txt extras_require = docs = requirements/docs.txt lint = requirements/lint.txt tests = requirements/tests.txt changelog = requirements/changelog.txt [options.entry_points] pytest11= shell-utilities = pytestshellutils.plugin [bdist_wheel] universal = false [sdist] owner = root group = root [flake8] max-line-length = 120 exclude = # No need to traverse our git directory .git, # Nox virtualenvs are also not important .nox, # There's no value in checking cache directories __pycache__, # Don't check the auto generated version file src/saf/version.py, # Package build stuff build, dist, # The conf file is mostly autogenerated, ignore it docs/conf.py, # Also ignore setup.py, it's mostly a shim setup.py, # Ignore our custom pre-commit hooks .pre-commit-hooks per-file-ignores = # F401 imported but unused __init__.py: F401 # D100 Missing docstring in public module # D103 Missing docstring in public function noxfile.py: D100,D102,D103,D107,D212,E501 # F401 'socket.*' imported but unused # F403 'from socket import *' used; unable to detect undefined names src/pytestshellutils/utils/socket.py: F401,F403 # F401 'time.*' imported but unused # F403 'from time import *' used; unable to detect undefined names src/pytestshellutils/utils/time.py: F401,F403 # E501 line too long src/pytestshellutils/utils/processes.py: E501 # D100 Missing docstring in public module # D101 Missing docstring in public class # D102 Missing docstring in public method # D103 Missing docstring in public function tests/*.py: D100,D101,D102,D103 ignore = # D104 Missing docstring in public package D104, # D107 Missing docstring in __init__ - Class docstrings will cover __init__ docstrings D107, # D212 Multi-line docstring summary should start at the first line D212, # D200 One-line docstring should fit on one line with quotes D200, # W503 line break before binary operator W503 # Additional builtins builtins = # __salt__ dunder __salt__ # __opts__ dictionary __opts__ # The system encoding that Salt injects into the globals __salt_system_encoding__ # flake8-docstrings config docstring-convention = google [mypy] python_version = 3.8 mypy_path = src ignore_missing_imports = True no_implicit_optional = True show_error_codes = True strict_equality = True warn_redundant_casts = True warn_return_any = True warn_unused_configs = True warn_unused_ignores = True disallow_any_generics = True check_untyped_defs = True no_implicit_reexport = True disallow_untyped_calls = True strict = True [mypy-pytestshellutils.utils.socket] no_implicit_reexport = False [mypy-pytestshellutils.utils.time] no_implicit_reexport = False [mypy.tools] ignore_missing_imports = True pytest-shell-utilities-1.9.7/setup.py000066400000000000000000000002721470601045300177110ustar00rootroot00000000000000#!/usr/bin/env python # Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # import setuptools if __name__ == "__main__": setuptools.setup(use_scm_version=True) pytest-shell-utilities-1.9.7/src/000077500000000000000000000000001470601045300167655ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/src/pytestshellutils/000077500000000000000000000000001470601045300224265ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/src/pytestshellutils/__init__.py000066400000000000000000000035721470601045300245460ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # # type: ignore import pathlib import re import sys try: from .version import __version__ except ImportError: # pragma: no cover __version__ = "0.0.0.not-installed" try: from importlib.metadata import version, PackageNotFoundError try: __version__ = version("pytest-shell-utilities") except PackageNotFoundError: # package is not installed pass except ImportError: try: from importlib_metadata import version, PackageNotFoundError try: __version__ = version("pytest-shell-utilities") except PackageNotFoundError: # package is not installed pass except ImportError: try: from pkg_resources import get_distribution, DistributionNotFound try: __version__ = get_distribution("pytest-shell-utilities").version except DistributionNotFound: # package is not installed pass except ImportError: # pkg resources isn't even available?! pass # Define __version_info__ attribute VERSION_INFO_REGEX = re.compile( r"(?P[\d]+)\.(?P[\d]+)\.(?P[\d]+)" r"(?:\.dev(?P[\d]+)\+g(?P[a-z0-9]+)\.d(?P[\d]+))?" ) try: # pragma: no branch __version_info__ = tuple( int(p) if p.isdigit() else p for p in VERSION_INFO_REGEX.match(__version__).groups() if p ) except AttributeError: # pragma: no cover __version_info__ = (-1, -1, -1) finally: del VERSION_INFO_REGEX # Define some constants CODE_ROOT_DIR = pathlib.Path(__file__).resolve().parent IS_WINDOWS = sys.platform.startswith("win") IS_DARWIN = IS_OSX = sys.platform.startswith("darwin") pytest-shell-utilities-1.9.7/src/pytestshellutils/customtypes.py000066400000000000000000000036721470601045300254070ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 """ Custom Types. """ import copy import logging from typing import Any from typing import Callable from typing import Dict from typing import Protocol from typing import Tuple from typing import TYPE_CHECKING import attr from pytestshellutils.utils import format_callback_to_string if TYPE_CHECKING: from pytestshellutils.shell import Daemon log = logging.getLogger(__name__) class EnvironDict(Dict[str, str]): """ Environ dictionary type. """ def __str__(self) -> str: # pragma: no cover """ String representation of the class. """ return f"EnvironDict({super().__str__()})" class GenericCallback(Protocol): """ Generic callback function. """ def __call__(self, *args: Any, **kwargs: Any) -> None: # pragma: no cover """ Call the generic callback. """ ... class DaemonCallback(Protocol): """ Daemon callback function. """ def __call__(self, daemon: "Daemon") -> None: # pragma: no cover """ Call the daemon callback. """ ... @attr.s(kw_only=True, frozen=True) class Callback: """ Class which "stores" information of a callback. """ func: Callable[..., Any] = attr.ib() args: Tuple[Any, ...] = attr.ib(default=None) kwargs: Dict[str, Any] = attr.ib(default=None) def __str__(self) -> str: """ String representation of the class. """ return format_callback_to_string(self.func, self.args, self.kwargs) def __call__(self, *args: Any, **kwargs: Any) -> Any: """ Call the callback. """ _args = tuple(list(args) + list(self.args or ())) _kwargs = copy.deepcopy(self.kwargs) _kwargs.update(kwargs) log.debug("Running %s", format_callback_to_string(self.func, _args, _kwargs)) return self.func(*_args, **_kwargs) pytest-shell-utilities-1.9.7/src/pytestshellutils/exceptions.py000066400000000000000000000045571470601045300251740ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 """ Pytest Shell Utilities related exceptions. """ from typing import Optional from pytestshellutils.utils.processes import ProcessResult class ShellUtilsException(Exception): """ Base pytest shell utilities exception. """ class CallbackException(ShellUtilsException): """ Exception raised during a before/after start/stop daemon callback. """ class ProcessFailed(ShellUtilsException): """ Exception raised when a sub-process fails. Arguments: message: The exception message Keyword Arguments: process_result: The ``ProcessResult`` instance when the exception occurred """ def __init__(self, message: str, process_result: Optional[ProcessResult] = None) -> None: super().__init__() self.message = message self.process_result = process_result def __str__(self) -> str: """ Return a printable representation of the exception. """ message = self.message if self.process_result: if not message.endswith("\n"): message += "\n" message += str(self.process_result) return message class FactoryFailure(ProcessFailed): """ Exception raised when a sub-process fails on one of the factories. """ class FactoryNotStarted(FactoryFailure): """ Exception raised when a factory failed to start. Please look at :py:class:`~pytestshellutils.exceptions.FactoryFailure` for the supported keyword arguments documentation. """ class FactoryNotRunning(FactoryFailure): """ Exception raised when trying to use a factory's `.stopped` context manager and the factory is not running. Please look at :py:class:`~pytestshellutils.exceptions.FactoryFailure` for the supported keyword arguments documentation. """ class ProcessNotStarted(FactoryFailure): """ Exception raised when a process failed to start. Please look at :py:class:`~pytestshellutils.exceptions.FactoryFailure` for the supported keywords. arguments documentation. """ class FactoryTimeout(FactoryNotStarted): """ Exception raised when a process timed-out. Please look at :py:class:`~pytestshellutils.exceptions.FactoryFailure` for the supported keywords. arguments documentation. """ pytest-shell-utilities-1.9.7/src/pytestshellutils/plugin.py000066400000000000000000000011471470601045300243010ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # """ Pytest shell utilities plugin. """ import pytest from pytestshellutils.shell import Subprocess @pytest.fixture(scope="session") def shell() -> Subprocess: """ Shell fixture. Example: .. code-block:: python def test_assert_good_exitcode(shell): ret = shell.run("exit", "0") assert ret.returncode == 0 def test_assert_bad_exitcode(shell): ret = shell.run("exit", "1") assert ret.returncode == 1 """ return Subprocess() pytest-shell-utilities-1.9.7/src/pytestshellutils/py.typed000066400000000000000000000000001470601045300241130ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/src/pytestshellutils/shell.py000066400000000000000000001403441470601045300241150ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 """ Shelling class implementations. """ import atexit import contextlib import json import locale import logging import os import pathlib import shutil import subprocess import sys from tempfile import SpooledTemporaryFile from typing import Any from typing import Callable from typing import cast from typing import Dict from typing import Generator from typing import List from typing import Optional from typing import Tuple from typing import TYPE_CHECKING from typing import Union import attr import psutil from pytestskipmarkers.utils import platform from pytestshellutils.customtypes import Callback from pytestshellutils.customtypes import EnvironDict from pytestshellutils.exceptions import CallbackException from pytestshellutils.exceptions import FactoryNotRunning from pytestshellutils.exceptions import FactoryNotStarted from pytestshellutils.exceptions import FactoryTimeout from pytestshellutils.exceptions import ShellUtilsException from pytestshellutils.utils import format_callback_to_string from pytestshellutils.utils import ports from pytestshellutils.utils import resolved_pathlib_path from pytestshellutils.utils import time from pytestshellutils.utils.processes import ProcessResult from pytestshellutils.utils.processes import terminate_process from pytestshellutils.utils.processes import terminate_process_list if TYPE_CHECKING: from typing import Type from pytestsysstats.plugin import StatsProcesses log = logging.getLogger(__name__) @attr.s(slots=True, kw_only=True) class BaseFactory: """ Base factory class. Keyword Arguments: cwd: The path to the desired working directory environ: A dictionary of ``key``, ``value`` pairs to add to the environment. """ cwd: pathlib.Path = attr.ib(converter=resolved_pathlib_path) environ: EnvironDict = attr.ib(repr=False) @cwd.default def _default_cwd(self) -> pathlib.Path: """ Return the default cwd to use. """ return pathlib.Path.cwd() @environ.default def _default_environ(self) -> EnvironDict: """ Return the default ``os.environ`` to use. """ return cast(EnvironDict, os.environ.copy()) @attr.s(slots=True, kw_only=True) class SubprocessImpl: """ Subprocess interaction implementation. Arguments: factory: The factory instance, either :py:class:`~pytestshellutils.shell.Subprocess` or a sub-class of it. """ factory: "Union[Factory, Subprocess, ScriptSubprocess]" = attr.ib() _terminal: "Optional[subprocess.Popen[Any]]" = attr.ib(repr=False, init=False, default=None) _terminal_stdout: "Optional[SpooledTemporaryFile[bytes]]" = attr.ib( repr=False, init=False, default=None ) _terminal_stderr: "Optional[SpooledTemporaryFile[bytes]]" = attr.ib( repr=False, init=False, default=None ) _terminal_result: Optional[ProcessResult] = attr.ib(repr=False, init=False, default=None) _terminal_timeout: Union[int, float] = attr.ib(repr=False, init=False, default=None) _children: List[psutil.Process] = attr.ib(repr=False, init=False, factory=list) def cmdline(self, *args: str, **kwargs: Any) -> List[str]: """ Construct a list of arguments to use when starting the subprocess. Arguments: args: Additional arguments to use when starting the subprocess By default, this method will just call it's factory's ``cmdline()`` method, but can be overridden. """ return self.factory.cmdline(*args) def init_terminal( self, cmdline: List[str], shell: bool = False, env: Optional[EnvironDict] = None, cwd: Optional[Union[str, pathlib.Path]] = None, ) -> "subprocess.Popen[Any]": """ Instantiate a terminal with the passed command line(``cmdline``) and return it. Additionally, it sets a reference to it in ``self._terminal`` and also collects an initial listing of child processes which will be used when terminating the terminal Arguments: cmdline: List of strings to pass as ``args`` to :py:class:`~subprocess.Popen` Keyword Arguments: shell: Pass the value of ``shell`` to :py:class:`~subprocess.Popen` env: A dictionary of ``key``, ``value`` pairs to add to the :py:attr:`pytestshellutils.shell.Factory.environ`. cwd: A path for the CWD when running the process. Returns: A :py:class:`~subprocess.Popen` instance. """ environ = self.factory.environ.copy() if env is not None: environ.update(env) self._terminal_stdout = SpooledTemporaryFile(512000, buffering=0) self._terminal_stderr = SpooledTemporaryFile(512000, buffering=0) close_fds: bool if platform.is_windows(): # pragma: is-windows # Windows does not support closing FDs close_fds = False elif platform.is_freebsd() and sys.version_info < (3, 9): # pragma: is-bsd-lt-py39 # Closing FDs in FreeBSD before Py3.9 can be slow # https://bugs.python.org/issue38061 close_fds = False else: close_fds = True self._terminal = subprocess.Popen( cmdline, stdout=self._terminal_stdout, stderr=self._terminal_stderr, shell=shell, # nosec B602 cwd=str(cwd or self.factory.cwd), universal_newlines=True, close_fds=close_fds, env=environ, bufsize=0, ) # Reset the previous _terminal_result if set self._terminal_result = None try: # Check if the process starts properly self._terminal.wait(timeout=0.05) # If TimeoutExpired is not raised, it means the process failed to start except subprocess.TimeoutExpired: # We're good # Collect any child processes, though, this early there likely is none with contextlib.suppress(psutil.NoSuchProcess, psutil.AccessDenied): for child in psutil.Process(self._terminal.pid).children( recursive=True ): # pragma: no cover if child not in self._children: self._children.append(child) atexit.register(self.terminate) return self._terminal def is_running(self) -> bool: """ Returns true if the sub-process is alive. Returns: Returns true if the sub-process is alive """ if not self._terminal: return False return self._terminal.poll() is None def terminate(self) -> ProcessResult: """ Terminate the started subprocess. """ return self._terminate() def _terminate(self) -> ProcessResult: """ This method actually terminates the started subprocess. """ if self._terminal is None: if TYPE_CHECKING: # Make mypy happy assert self._terminal_result return self._terminal_result atexit.unregister(self.terminate) log.info("Stopping %s", self.factory) # Collect any child processes information before terminating the process with contextlib.suppress(psutil.NoSuchProcess, psutil.AccessDenied): for child in psutil.Process(self._terminal.pid).children(recursive=True): if child not in self._children: self._children.append(child) with self._terminal: try: if self.factory.slow_stop: self._terminal.terminate() else: self._terminal.kill() try: # Allow the process to exit by itself in case slow_stop is True self._terminal.wait(10) except subprocess.TimeoutExpired: # pragma: no cover # The process failed to stop, no worries, we'll make sure it exit along with it's # child processes bellow pass except ProcessLookupError: # The process is already gone pass # Lets log and kill any child processes left behind, including the main subprocess # If it failed to properly stop terminate_process( pid=self._terminal.pid, kill_children=True, children=self._children, slow_stop=self.factory.slow_stop, ) # Wait for the process to terminate, to avoid zombies. self._terminal.wait() # poll the terminal so the right returncode is set on the popen object self._terminal.poll() # This call shouldn't really be necessary self._terminal.communicate() if TYPE_CHECKING: # Make mypy happy assert self._terminal_stdout self._terminal_stdout.flush() self._terminal_stdout.seek(0) _read_stdout = self._terminal_stdout.read() stdout = self._terminal._translate_newlines( # type: ignore[attr-defined] _read_stdout, self.factory.system_encoding, sys.stdout.errors, ) self._terminal_stdout.close() if TYPE_CHECKING: # Make mypy happy assert self._terminal_stderr self._terminal_stderr.flush() self._terminal_stderr.seek(0) _read_stderr = self._terminal_stderr.read() stderr = self._terminal._translate_newlines( # type: ignore[attr-defined] _read_stderr, self.factory.system_encoding, sys.stderr.errors, ) self._terminal_stderr.close() try: self._terminal_result = ProcessResult( returncode=self._terminal.returncode, stdout=stdout, stderr=stderr, cmdline=cast(List[str], self._terminal.args), ) log.info("%s %s", self.factory.__class__.__name__, self._terminal_result) return self._terminal_result finally: self._terminal = None self._terminal_stdout = None self._terminal_stderr = None self._children = [] @property def pid(self) -> Optional[int]: """ The pid of the running process. None if not running. """ if not self._terminal: # pragma: no cover return None return self._terminal.pid def run( self, *args: str, shell: bool = False, env: Optional[EnvironDict] = None, cwd: Optional[Union[str, pathlib.Path]] = None, **kwargs: Any, ) -> "subprocess.Popen[Any]": """ Run the given command synchronously. Arguments: args: The command to run. Keyword Arguments: shell: Pass the value of `shell` to :py:meth:`pytestshellutils.shell.Factory.init_terminal` env: A dictionary of ``key``, ``value`` pairs to add to the :py:attr:`pytestshellutils.shell.Factory.environ`. cwd: A path for the CWD when running the process. Returns: A :py:class:`~subprocess.Popen` instance. """ cmdline = self.cmdline(*args, **kwargs) log.info("%s is running %r in CWD: %s ...", self.factory, cmdline, cwd or self.factory.cwd) return self.init_terminal(cmdline, shell=shell, env=env, cwd=cwd) @attr.s(slots=True, kw_only=True) class Factory(BaseFactory): """ Base shell factory class. Keyword Arguments: slow_stop: Whether to terminate the processes by sending a :py:attr:`SIGTERM` signal or by calling :py:meth:`~subprocess.Popen.terminate` on the sub-process. When code coverage is enabled, one will want `slow_stop` set to `True` so that coverage data can be written down to disk. system_encoding: The system encoding to use when decoding the subprocess output. Defaults to "utf-8". timeout: The default maximum amount of seconds that a script should run. This value can be overridden when calling :py:meth:`~pytestshellutils.shell.Process.run` through the ``_timeout`` keyword argument, and, in that case, the timeout value applied would be that of ``_timeout`` instead of ``self.timeout``. """ slow_stop: bool = attr.ib(default=True) system_encoding: str = attr.ib(repr=False) timeout: Union[int, float] = attr.ib() impl: SubprocessImpl = attr.ib(repr=False, init=False) # Internal attributes _cmdline: List[Any] = attr.ib(repr=False, init=False, default=None) @system_encoding.default def _default_system_encoding(self) -> str: return self._get_default_system_encoding() @timeout.default def _set_timeout(self) -> Optional[int]: return self._get_default_timeout() def _get_default_system_encoding(self) -> str: encoding: Optional[str] = None if not platform.is_windows() and sys.stdin is not None: # On Linux we can rely on sys.stdin for the encoding since it # most commonly matches the file-system encoding. This however # does not apply to windows encoding = sys.stdin.encoding if not encoding: # If the system is properly configured this should return a valid # encoding. MS Windows has problems with this and reports the wrong # encoding try: encoding = locale.getencoding() # type: ignore[attr-defined] except AttributeError: # Python < 3.11 encoding = locale.getpreferredencoding(do_setlocale=True) if not encoding: # This is most likely ascii which is not the best but we were # unable to find a better encoding. If this fails, we fall all # the way back to ascii encoding = sys.getdefaultencoding() if not encoding: if platform.is_darwin(): # Mac OS X uses UTF-8 encoding = "utf-8" elif platform.is_windows(): # Windows uses a configurable encoding; on Windows, Python uses the name “mbcs” # to refer to whatever the currently configured encoding is. encoding = "mbcs" else: # On linux default to ascii as a last resort encoding = "ascii" if not encoding: # If we still didn't detect the encoding, default to utf-8 encoding = "utf-8" return encoding def _get_default_timeout(self) -> Optional[int]: return None def _get_impl_class(self) -> "Type[SubprocessImpl]": """ Return the ``impl`` class to use. """ return SubprocessImpl def __attrs_post_init__(self) -> None: """ Post ``attrs`` class initialization routines. """ impl_class = self._get_impl_class() self.impl = impl_class(factory=self) def cmdline(self, *args: str) -> List[str]: """ Method to construct a command line. """ self._cmdline = list(args) return self._cmdline def get_display_name(self) -> str: """ Returns a human readable name for the factory. """ return "{}({})".format(self.__class__.__name__, self._cmdline or "") def is_running(self) -> bool: """ Returns true if the sub-process is alive. """ return self.impl.is_running() def terminate(self) -> ProcessResult: """ Terminate the started subprocess. """ return self.impl.terminate() @property def pid(self) -> Optional[int]: """ The pid of the running process. None if not running. """ return self.impl.pid @attr.s(slots=True, kw_only=True) class Subprocess(Factory): """ Base shell factory class. """ def run( self, *args: str, env: Optional[EnvironDict] = None, _timeout: Optional[Union[int, float]] = None, **kwargs: Any, ) -> ProcessResult: """ Run the given command synchronously. Keyword Arguments: args: The list of arguments to pass to :py:meth:`~pytestshellutils.shell.Subprocess.cmdline` to construct the command to run env: Pass a dictionary of environment key, value pairs to inject into the subprocess. _timeout: The timeout value for this particular ``run()`` call. If this value is not ``None``, it will be used instead of :py:attr:`~pytestshellutils.shell.Subprocess.timeout`, the default timeout. """ start_time = time.time() # Build the cmdline to pass to the terminal # We set the _terminal_timeout attribute while calling cmdline in case it needs # access to that information to build the command line self.impl._terminal_timeout = _timeout or self.timeout timmed_out = False try: self.impl.run(*args, env=env, **kwargs) if TYPE_CHECKING: # Make mypy happy assert self.impl._terminal self.impl._terminal.communicate(timeout=self.impl._terminal_timeout) except subprocess.TimeoutExpired: timmed_out = True result = self.terminate() cmdline = result.cmdline returncode = result.returncode if timmed_out: raise FactoryTimeout( "{} Failed to run: {}; Error: Timed out after {:.2f} seconds!".format( self, cmdline, time.time() - start_time ), process_result=result, ) stdout, stderr, json_out = self.process_output( result.stdout, result.stderr, cmdline=cmdline ) log.info( "%s completed %r in CWD: %s after %.2f seconds", self, cmdline, self.cwd, time.time() - start_time, ) return ProcessResult( returncode=returncode, stdout=stdout, stderr=stderr, data=json_out, cmdline=cmdline ) def process_output( self, stdout: str, stderr: str, cmdline: Optional[List[str]] = None ) -> Tuple[str, str, Optional[Dict[Any, Any]]]: """ Process the output. When possible JSON is loaded from the output. Returns: Returns a tuple in the form of ``(stdout, stderr, loaded_json)`` """ if stdout: try: json_out = json.loads(stdout) except ValueError: log.debug("%s failed to load JSON from the following output:\n%r", self, stdout) json_out = None else: json_out = None return stdout, stderr, json_out @attr.s(slots=True, kw_only=True) class ScriptSubprocess(Subprocess): """ Base CLI script/binary class. Keyword Arguments: script_name: This is the string containing the name of the binary to call on the subprocess, either the full path to it, or the basename. In case of the basename, the directory containing the basename must be in your ``$PATH`` variable. base_script_args: An list or tuple iterable of the base arguments to use when building the command line to launch the process Please look at :py:class:`~pytestshellutils.shell.Factory` for the additional supported keyword arguments documentation. """ script_name: str = attr.ib() base_script_args: List[str] = attr.ib(factory=list) def get_display_name(self) -> str: """ Returns a human readable name for the factory. """ return f"{self.__class__.__name__}({pathlib.Path(self.script_name).name})" def get_script_path(self) -> str: """ Returns the path to the script to run. """ script_path: Optional[str] if os.path.isabs(self.script_name): script_path = self.script_name else: script_path = shutil.which(self.script_name) if not script_path or not os.path.exists(script_path): raise FileNotFoundError(f"The CLI script '{self.script_name}' does not exist") if TYPE_CHECKING: # Make mypy happy assert script_path return script_path def get_base_script_args(self) -> List[str]: """ Returns any additional arguments to pass to the CLI script. """ return list(self.base_script_args) def get_script_args(self) -> List[str]: # pylint: disable=no-self-use """ Returns any additional arguments to pass to the CLI script. """ return [] def cmdline(self, *args: str) -> List[str]: """ Construct a list of arguments to use when starting the subprocess. Arguments: args: Additional arguments to use when starting the subprocess """ return ( [self.get_script_path()] + self.get_base_script_args() + self.get_script_args() + list(args) ) @attr.s(kw_only=True, slots=True, frozen=True) class StartDaemonCallArguments: """ This class holds the arguments and keyword arguments used to start a daemon. It's used when restarting the daemon so that the same call is used. Keyword Arguments: args: List of arguments kwargs: Dictionary of keyword arguments """ args: Tuple[str, ...] = attr.ib() kwargs: Dict[str, Any] = attr.ib() @attr.s(slots=True, kw_only=True) class DaemonImpl(SubprocessImpl): """ Daemon subprocess interaction implementation. Please look at :py:class:`~pytestshellutils.shell.SubprocessImpl` for the additional supported keyword arguments documentation. """ factory: "Daemon" = attr.ib() _before_start_callbacks: List[Callback] = attr.ib(repr=False, hash=False, factory=list) _after_start_callbacks: List[Callback] = attr.ib(repr=False, hash=False, factory=list) _before_terminate_callbacks: List[Callback] = attr.ib(repr=False, hash=False, factory=list) _after_terminate_callbacks: List[Callback] = attr.ib(repr=False, hash=False, factory=list) _start_args_and_kwargs: StartDaemonCallArguments = attr.ib( init=False, repr=False, hash=False, default=None ) def before_start(self, callback: Callable[[], None], *args: Any, **kwargs: Any) -> None: """ Register a function callback to run before the daemon starts. Arguments: callback: The function to call back Keyword Arguments: args: The arguments to pass to the callback kwargs: The keyword arguments to pass to the callback Returns: Nothing. """ self._before_start_callbacks.append(Callback(func=callback, args=args, kwargs=kwargs)) def after_start(self, callback: Callable[[], None], *args: Any, **kwargs: Any) -> None: """ Register a function callback to run after the daemon starts. Arguments: callback: The function to call back Keyword Arguments: args: The arguments to pass to the callback kwargs: The keyword arguments to pass to the callback Returns: Nothing. """ self._after_start_callbacks.append(Callback(func=callback, args=args, kwargs=kwargs)) def before_terminate(self, callback: Callable[[], None], *args: Any, **kwargs: Any) -> None: """ Register a function callback to run before the daemon terminates. Arguments: callback: The function to call back Keyword Arguments: args: The arguments to pass to the callback kwargs: The keyword arguments to pass to the callback Returns: Nothing. """ self._before_terminate_callbacks.append(Callback(func=callback, args=args, kwargs=kwargs)) def after_terminate(self, callback: Callable[[], None], *args: Any, **kwargs: Any) -> None: """ Register a function callback to run after the daemon terminates. Arguments: callback: The function to call back Keyword Arguments: args: The arguments to pass to the callback kwargs: The keyword arguments to pass to the callback Returns: Nothing. """ self._after_terminate_callbacks.append(Callback(func=callback, args=args, kwargs=kwargs)) def start( self, *extra_cli_arguments: str, max_start_attempts: Optional[int] = None, start_timeout: Optional[Union[int, float]] = None, ) -> bool: """ Start the daemon. Keyword Arguments: extra_cli_arguments: Extra arguments to pass to the CLI that starts the daemon max_start_attempts: Maximum number of attempts to try and start the daemon in case of failures start_timeout: The maximum number of seconds to wait before considering that the daemon did not start Returns: bool: A boolean indicating if the start was successful or not. """ if self.is_running(): # pragma: no cover log.warning("%s is already running.", self) return True self._start_args_and_kwargs = StartDaemonCallArguments( args=extra_cli_arguments, kwargs={"max_start_attempts": max_start_attempts, "start_timeout": start_timeout}, ) process_running = False start_time = time.time() start_attempts = max_start_attempts or self.factory.max_start_attempts current_attempt = 0 run_arguments = list(extra_cli_arguments) while True: if process_running: break current_attempt += 1 if current_attempt > start_attempts: break log.info( "Starting %s. Attempt: %d of %d", self.factory, current_attempt, start_attempts ) for callback in self._before_start_callbacks: # pylint: disable=not-an-iterable try: callback() except CallbackException as exc: # pragma: no cover log.info( "Exception raised when running %s: %s", callback, exc, exc_info=True, ) current_start_time = time.time() start_running_timeout = current_start_time + ( start_timeout or self.factory.start_timeout ) if current_attempt > 1 and self.factory.extra_cli_arguments_after_first_start_failure: run_arguments = list(extra_cli_arguments) + list( self.factory.extra_cli_arguments_after_first_start_failure ) self.run(*run_arguments) if not self.is_running(): # pragma: no cover # A little breathe time to allow the process to start if not started already time.sleep(0.5) while time.time() <= start_running_timeout: if not self.is_running(): log.warning("%s is no longer running", self.factory) self.terminate() break try: if ( self.factory.run_start_checks(current_start_time, start_running_timeout) is False ): time.sleep(1) continue except FactoryNotStarted: self.terminate() break log.info( "The %s factory is running after %d attempts. Took %1.2f seconds", self.factory, current_attempt, time.time() - start_time, ) process_running = True break else: # The factory failed to confirm it's running status self.terminate() if process_running: for callback in self._after_start_callbacks: # pylint: disable=not-an-iterable try: callback() except CallbackException as exc: # pragma: no cover log.info( "Exception raised when running %s: %s", callback, exc, exc_info=True, ) return process_running result = self.terminate() raise FactoryNotStarted( "The {} factory has failed to confirm running status after {} attempts, which " "took {:.2f} seconds".format( self.factory, current_attempt - 1, time.time() - start_time, ), process_result=result, ) def terminate(self) -> ProcessResult: """ Terminate the daemon. """ if self._terminal_result is not None: # This factory has already been terminated return self._terminal_result for callback in self._before_terminate_callbacks: # pylint: disable=not-an-iterable try: callback() except CallbackException as exc: # pragma: no cover log.info( "Exception raised when running %s: %s", callback, exc, exc_info=True, ) try: return super().terminate() finally: for callback in self._after_terminate_callbacks: # pylint: disable=not-an-iterable try: callback() except CallbackException as exc: # pragma: no cover log.warning( "Exception raised when running %s: %s", callback, exc, exc_info=True, ) def get_start_arguments(self) -> StartDaemonCallArguments: """ Return the arguments and keyword arguments used when starting the daemon. """ return self._start_args_and_kwargs @attr.s(slots=True, kw_only=True) class Daemon(ScriptSubprocess): """ Base daemon factory. Keyword Arguments: check_ports: List of ports to try and connect to while confirming that the daemon is up and running extra_cli_arguments_after_first_start_failure: Extra arguments to pass to the CLI that starts the daemon after the first failure max_start_attempts: Maximum number of attempts to try and start the daemon in case of failures start_timeout: The maximum number of seconds to wait before considering that the daemon did not start Please look at :py:class:`~pytestshellutils.shell.Subprocess` for the additional supported keyword arguments documentation. """ impl: DaemonImpl = attr.ib(repr=False, init=False) script_name: str = attr.ib() base_script_args: List[str] = attr.ib(factory=list) check_ports: List[int] = attr.ib(factory=list) stats_processes: "StatsProcesses" = attr.ib(repr=False, hash=False, default=None) start_timeout: Union[int, float] = attr.ib(repr=False) max_start_attempts: int = attr.ib(repr=False, default=3) extra_cli_arguments_after_first_start_failure: List[str] = attr.ib(hash=False, factory=list) listen_ports: List[int] = attr.ib(init=False, repr=False, hash=False, factory=list) _start_checks_callbacks: List[Callback] = attr.ib(repr=False, hash=False, factory=list) def _get_impl_class(self) -> "Type[DaemonImpl]": """ Return the ``impl`` class to use. """ return DaemonImpl def __attrs_post_init__(self) -> None: """ Post ``attrs`` class initialization routines. """ super().__attrs_post_init__() if self.check_ports and not isinstance(self.check_ports, (list, tuple)): self.check_ports = [self.check_ports] if self.check_ports: self.listen_ports.extend(self.check_ports) self.after_start(self._add_factory_to_stats_processes) self.after_terminate(self._terminate_processes_matching_listen_ports) self.after_terminate(self._remove_factory_from_stats_processes) self.start_check(self._check_listening_ports) def before_start(self, callback: Callable[[], None], *args: Any, **kwargs: Any) -> None: """ Register a function callback to run before the daemon starts. Arguments: callback: The function to call back Keyword Arguments: args: The arguments to pass to the callback kwargs: The keyword arguments to pass to the callback Returns: Nothing. """ self.impl.before_start(callback, *args, **kwargs) def after_start(self, callback: Callable[[], None], *args: Any, **kwargs: Any) -> None: """ Register a function callback to run after the daemon starts. Arguments: callback: The function to call back Keyword Arguments: args: The arguments to pass to the callback kwargs: The keyword arguments to pass to the callback Returns: Nothing. """ self.impl.after_start(callback, *args, **kwargs) def before_terminate(self, callback: Callable[[], None], *args: Any, **kwargs: Any) -> None: """ Register a function callback to run before the daemon terminates. Arguments: callback: The function to call back Keyword Arguments: args: The arguments to pass to the callback kwargs: The keyword arguments to pass to the callback Returns: Nothing. """ self.impl.before_terminate(callback, *args, **kwargs) def after_terminate(self, callback: Callable[[], None], *args: Any, **kwargs: Any) -> None: """ Register a function callback to run after the daemon terminates. Arguments: callback: The function to call back Keyword Arguments: args: The arguments to pass to the callback kwargs: The keyword arguments to pass to the callback Returns: Nothing. """ self.impl.after_terminate(callback, *args, **kwargs) def start_check(self, callback: Callable[..., bool], *args: Any, **kwargs: Any) -> None: """ Register a function to run after the daemon starts to confirm readiness for work. The callback must accept as the first argument ``timeout_at`` which is a float. The callback must stop trying to confirm running behavior once ``time.time() > timeout_at``. The callback should return ``True`` to confirm that the daemon is ready for work. Arguments: callback: The function to call back Keyword Arguments: args: The arguments to pass to the callback kwargs: The keyword arguments to pass to the callback Returns: Nothing. Example: .. code-block:: python def check_running_state(timeout_at: float) -> bool: while time.time() <= timeout_at: # run some checks ... # if all is good break else: return False return True """ self._start_checks_callbacks.append(Callback(func=callback, args=args, kwargs=kwargs)) def get_check_ports(self) -> List[int]: """ Return a list of ports to check against to ensure the daemon is running. """ return self.check_ports or [] def get_start_check_callbacks(self) -> List[Callback]: """ Return a list of the start check callbacks. """ return self._start_checks_callbacks or [] def start( self, *extra_cli_arguments: str, max_start_attempts: Optional[int] = None, start_timeout: Optional[Union[int, float]] = None, ) -> bool: """ Start the daemon. """ return self.impl.start( *extra_cli_arguments, max_start_attempts=max_start_attempts, start_timeout=start_timeout ) @contextlib.contextmanager def started( self, *extra_cli_arguments: str, max_start_attempts: Optional[int] = None, start_timeout: Optional[Union[int, float]] = None, ) -> Generator["Daemon", None, None]: """ Start the daemon and return it's instance so it can be used as a context manager. """ try: self.start( *extra_cli_arguments, max_start_attempts=max_start_attempts, start_timeout=start_timeout, ) yield self finally: self.terminate() @contextlib.contextmanager def stopped( self, before_stop_callback: Optional[Callable[["Daemon"], None]] = None, after_stop_callback: Optional[Callable[["Daemon"], None]] = None, before_start_callback: Optional[Callable[["Daemon"], None]] = None, after_start_callback: Optional[Callable[["Daemon"], None]] = None, ) -> Generator["Daemon", None, None]: """ Stop the daemon and return it's instance so it can be used as a context manager. Keyword Arguments: before_stop_callback: A callable to run before stopping the daemon. The callback must accept one argument, the daemon instance. after_stop_callback: A callable to run after stopping the daemon. The callback must accept one argument, the daemon instance. before_start_callback: A callable to run before starting the daemon. The callback must accept one argument, the daemon instance. after_start_callback: A callable to run after starting the daemon. The callback must accept one argument, the daemon instance. This context manager will stop the factory while the context is in place, it re-starts it once out of context. Example: .. code-block:: python assert factory.is_running() is True with factory.stopped(): assert factory.is_running() is False assert factory.is_running() is True """ if not self.is_running(): raise FactoryNotRunning(f"{self} is not running ") start_arguments = self.impl.get_start_arguments() try: if before_stop_callback: try: before_stop_callback(self) except CallbackException as exc: # pragma: no cover log.info( "Exception raised when running %s: %s", format_callback_to_string(before_stop_callback), exc, exc_info=True, ) self.terminate() if after_stop_callback: try: after_stop_callback(self) except CallbackException as exc: # pragma: no cover log.info( "Exception raised when running %s: %s", format_callback_to_string(after_stop_callback), exc, exc_info=True, ) yield self except ShellUtilsException: # pragma: no cover pylint: disable=try-except-raise raise else: if before_start_callback: try: before_start_callback(self) except CallbackException as exc: # pragma: no cover log.info( "Exception raised when running %s: %s", format_callback_to_string(before_start_callback), exc, exc_info=True, ) _started = self.start( *start_arguments.args, # pylint: disable=not-an-iterable **start_arguments.kwargs, # pylint: disable=not-a-mapping ) if _started: if after_start_callback: try: after_start_callback(self) except CallbackException as exc: # pragma: no cover log.info( "Exception raised when running %s: %s", format_callback_to_string(after_start_callback), exc, exc_info=True, ) def run_start_checks(self, started_at: float, timeout_at: float) -> bool: """ Run checks to confirm that the daemon has started. """ start_check_callbacks = list(self.get_start_check_callbacks()) if not start_check_callbacks: log.debug("No start check callbacks to run for %s", self) return True checks_start_time = time.time() log.debug("%s is running start checks", self) while time.time() <= timeout_at: if not self.is_running(): raise FactoryNotStarted(f"{self} is no longer running") if not start_check_callbacks: break start_check = start_check_callbacks[0] try: ret = start_check(timeout_at) if ret is True: start_check_callbacks.pop(0) except Exception as exc: # pylint: disable=broad-except log.info( "Exception raised when running %s: %s", start_check, exc, exc_info=True, ) if start_check_callbacks: log.error( "Failed to run start check callbacks after %1.2f seconds for %s. " "Remaining start check callbacks: %s", time.time() - checks_start_time, self, start_check_callbacks, ) return False log.debug("All start check callbacks executed for %s", self) return True def _check_listening_ports(self, timeout_at: float) -> bool: """ Check if the defined ports are in a listening state. This callback will run when trying to assess if the daemon is ready to accept work by trying to connect to each of the ports it's supposed to be listening. """ check_ports = set(self.get_check_ports()) if not check_ports: log.debug("No ports to check connection to for %s", self) return True log.debug("Listening ports to check for %s: %s", self, set(self.get_check_ports())) checks_start_time = time.time() while time.time() <= timeout_at: if not self.is_running(): raise FactoryNotStarted(f"{self} is no longer running") if not check_ports: break check_ports -= ports.get_connectable_ports(check_ports) if check_ports: time.sleep(1.5) else: log.error( "Failed to check ports after %1.2f seconds for %s. Remaining ports to check: %s", time.time() - checks_start_time, self, check_ports, ) return False log.debug("All listening ports checked for %s: %s", self, set(self.get_check_ports())) return True def _add_factory_to_stats_processes(self) -> None: if self.stats_processes is not None: display_name = self.get_display_name() self.stats_processes.add(display_name, self.pid) def _remove_factory_from_stats_processes(self) -> None: if self.stats_processes is not None: display_name = self.get_display_name() self.stats_processes.remove(display_name) def _terminate_processes_matching_listen_ports(self) -> None: if not self.listen_ports: return # If any processes were not terminated and are listening on the ports # we have set on listen_ports, terminate those processes. found_processes = [] psutil_majorver, _, _ = psutil.version_info if psutil_majorver < 6: for process in psutil.process_iter(["connections"]): try: for connection in process.connections(): if connection.status != psutil.CONN_LISTEN: # We only care about listening services continue if connection.laddr.port in self.check_ports: found_processes.append(process) # We already found one connection, no need to check the others break except psutil.AccessDenied: # pragma: no cover # We've been denied access to this process connections. Carry on. continue except psutil.ZombieProcess: continue else: for process in psutil.process_iter(["net_connections"]): try: for connection in process.net_connections(): if connection.status != psutil.CONN_LISTEN: # We only care about listening services continue if connection.laddr.port in self.check_ports: found_processes.append(process) # We already found one connection, no need to check the others break except psutil.AccessDenied: # pragma: no cover # We've been denied access to this process net_connections. Carry on. continue except psutil.ZombieProcess: continue if found_processes: log.debug( "The following processes were found listening on ports %s: %s", ", ".join( [str(port) for port in self.listen_ports], # pylint: disable=not-an-iterable ), found_processes, ) terminate_process_list(found_processes, kill=True, slow_stop=False) else: log.debug( "No astray processes were found listening on ports: %s", ", ".join( [str(port) for port in self.listen_ports], # pylint: disable=not-an-iterable ), ) def __enter__(self) -> "Daemon": """ Use class as a context manager. """ if not self.is_running(): raise RuntimeError( "Factory not yet started. Perhaps you're after something like:\n\n" "with {}.started() as factory:\n" " yield factory".format(self.__class__.__name__) ) return self def __exit__(self, *_: Any) -> None: """ Exit the class context manager. """ self.terminate() pytest-shell-utilities-1.9.7/src/pytestshellutils/utils/000077500000000000000000000000001470601045300235665ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/src/pytestshellutils/utils/__init__.py000066400000000000000000000100351470601045300256760ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # import inspect import pathlib import sys import warnings from typing import Any from typing import Callable from typing import Dict from typing import Optional from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import Union import packaging.version import pytestshellutils if TYPE_CHECKING: from packaging.version import Version def resolved_pathlib_path(path: Union[str, pathlib.Path]) -> pathlib.Path: """ Return a resolved ``pathlib.Path``. """ if isinstance(path, str): path = pathlib.Path(path) return path.resolve() def format_callback_to_string( callback: Union[str, Callable[..., Any]], args: Optional[Tuple[Any, ...]] = None, kwargs: Optional[Dict[str, Any]] = None, ) -> str: """ Convert a callback, its arguments and keyword arguments to a string suitable for logging purposes. Arguments: callback: The callback function Keyword Arguments: args: The callback arguments kwargs: The callback keyword arguments Returns: str: The formatted callback string """ callback_str: str if not isinstance(callback, str): try: callback_str = f"{callback.__qualname__}(" except AttributeError: # pragma: no cover callback_str = f"{callback.__name__}(" else: callback_str = f"{callback}(" if args: callback_str += ", ".join([repr(arg) for arg in args]) if kwargs: if args: callback_str += ", " callback_str += ", ".join([f"{k}={v!r}" for (k, v) in kwargs.items()]) callback_str += ")" return callback_str def warn_until( # pragma: no cover version: str, message: str, category: Type[Warning] = DeprecationWarning, stacklevel: Optional[int] = None, _dont_call_warnings: bool = False, _pkg_version_: Optional[str] = None, ) -> None: """ Show a deprecation warning. Helper function to raise a warning, by default, a ``DeprecationWarning``, until the provided ``version``, after which, a ``RuntimeError`` will be raised to remind the developers to remove the warning because the target version has been reached. Arguments: version: The version string after which the warning becomes a ``RuntimeError``. For example ``2.1``. message: The warning message to be displayed. Keyword Arguments: category: The warning class to be thrown, by default ``DeprecationWarning`` stacklevel: There should be no need to set the value of ``stacklevel``. _dont_call_warnings: This parameter is used just to get the functionality until the actual error is to be issued. When we're only after the version checks to raise a ``RuntimeError``. Returns: Nothing. """ _version = packaging.version.parse(version) if _pkg_version_ is None: _pkg_version_ = pytestshellutils.__version__ # type: ignore[attr-defined] _pkg_version = packaging.version.parse(_pkg_version_) if stacklevel is None: # Attribute the warning to the calling function, not to warn_until() stacklevel = 3 if _pkg_version >= _version: caller = inspect.getframeinfo(sys._getframe(stacklevel - 1)) raise RuntimeError( "The warning triggered on filename '{filename}', line number " "{lineno}, is supposed to be shown until version " "{until_version} is released. Current version is now " "{version}. Please remove the warning.".format( filename=caller.filename, lineno=caller.lineno, until_version=_pkg_version_, version=version, ), ) if _dont_call_warnings is False: warnings.warn( message.format(version=version), category, stacklevel=stacklevel, ) pytest-shell-utilities-1.9.7/src/pytestshellutils/utils/ports.py000066400000000000000000000042611470601045300253120ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 """ Ports related utility functions. """ import contextlib import logging from typing import Iterable from typing import Set import pytest import pytestshellutils.utils.socket as socket log = logging.getLogger(__name__) def get_unused_localhost_port(use_cache: bool = False) -> int: """ Return a random unused port on localhost. Keyword Arguments: use_cache: If ``use_cache`` is ``True``, consecutive calls to this function will never return the cached port. """ if not isinstance(use_cache, bool): # pragma: no cover raise pytest.UsageError( f"The value of 'use_cache' needs to be an boolean, not {type(use_cache)}" ) with contextlib.closing(socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)) as usock: usock.bind(("127.0.0.1", 0)) port: int = usock.getsockname()[1] if use_cache: try: cached_ports = get_unused_localhost_port.__cached_ports__ # type: ignore[attr-defined] except AttributeError: cached_ports = get_unused_localhost_port.__cached_ports__ = set() # type: ignore[attr-defined] if port in cached_ports: return get_unused_localhost_port(use_cache=use_cache) cached_ports.add(port) return port def get_connectable_ports(ports: Iterable[int]) -> Set[int]: """ Given a list of ports, returns those that we can connect to. Arguments: ports: An iterable of ports to try and connect to Returns: set: Returns a set of the ports where connection was successful """ connectable_ports = set() check_ports = set(ports) for port in set(check_ports): with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: conn = sock.connect_ex(("localhost", port)) try: if conn == 0: log.debug("Port %s is connectable!", port) connectable_ports.add(port) sock.shutdown(socket.SHUT_RDWR) except OSError: # pragma: no cover continue return connectable_ports pytest-shell-utilities-1.9.7/src/pytestshellutils/utils/processes.py000066400000000000000000000364231470601045300261560ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 """ Process related utilities. """ import errno import json import logging import pprint import signal import weakref from typing import Any from typing import Dict from typing import List from typing import Optional import attr import psutil try: from pytest import LineMatcher except ImportError: # pragma: no cover # Older pytest from _pytest.pytester import LineMatcher from pytestshellutils.utils import warn_until log = logging.getLogger(__name__) class MatchString(str): """ Simple subclass around ``str`` which provides a ``.matcher`` property. This ``.matcher`` property is an instance of :py:class:`~pytest.LineMatcher` """ @property def matcher(self) -> LineMatcher: """ Return an instance of :py:class:`~pytest.LineMatcher`. """ return LineMatcher(self.splitlines()) def convert_string_to_match_string(value: Optional[str]) -> Optional[MatchString]: """ Convert strings into ``MatchString`` instances. """ if isinstance(value, str): return MatchString(value) return value @attr.s(frozen=True, kw_only=True) class ProcessResult: """ Wrapper class around a subprocess result. This class serves the purpose of having a common result class which will hold the resulting data from a subprocess command. Keyword Arguments: returncode: The returncode returned by the process stdout: The ``stdout`` returned by the process stderr: The ``stderr`` returned by the process cmdline: The command line used to start the process data: The data returned by parsing ``stdout``, when possible. data_key: When ``stdout`` can be parsed as JSON, sometimes there's a top level key which is not that interesting. By using ``data_key``, we define that we're actually only interested on the data structure which is keyed by ``data_key``. Note: Cast :py:class:`~pytestshellutils.utils.processes.ProcessResult` to a string to pretty-print it. """ returncode: int = attr.ib() stdout: MatchString = attr.ib(converter=convert_string_to_match_string) stderr: MatchString = attr.ib(converter=convert_string_to_match_string) cmdline: Optional[List[str]] = attr.ib(default=None) data_key: Optional[str] = attr.ib(default=None) data: Optional[Dict[Any, Any]] = attr.ib() @returncode.validator def _validate_returncode(self, attribute: Any, value: int) -> None: """ Validate the value type. """ if not isinstance(value, int): raise ValueError(f"'returncode' needs to be an integer, not '{type(value)}'") @data.default def _default_data(self) -> Optional[Dict[Any, Any]]: """ Try to parse the passed ``stdout`` as JSON as the default data value. """ stdout: Optional[str] = self.stdout.strip() if self.stdout else None if stdout: try: data: Optional[Dict[Any, Any]] = json.loads(stdout.strip()) if data and self.data_key and self.data_key in data: data = data[self.data_key] return data except ValueError: pass return None @property def exitcode(self) -> int: # pragma: no cover """ Return the process returncode. This property is deprecated and should not be used. It only exists to support projects that are migrating from pytest-salt-factories versions. Use ``.returncode`` instead. """ warn_until( "2.0.0", "The '.exitcode' property is deprecated and will cease to exist after " "pytest-shell-utilities {version}. Please use '.returncode' instead.", ) return self.returncode @property def json(self) -> Optional[Dict[Any, Any]]: # pragma: no cover """ Return the process output parsed as JSON, if possible. This property is deprecated and should not be used. It only exists to support projects that are migrating from pytest-salt-factories versions. Use ``.data`` instead. """ warn_until( "2.0.0", "The '.json' property is deprecated and will cease to exist after " "pytest-shell-utilities {version}. Please use '.data' instead.", ) return self.data def __str__(self) -> str: """ String representation of the class. """ message = self.__class__.__name__ if self.cmdline: message += f"\n Command Line: {self.cmdline}" if self.returncode is not None: message += f"\n Returncode: {self.returncode}" if (self.stdout and self.stdout.strip()) or (self.stderr and self.stderr.strip()): message += "\n Process Output:" if self.stdout and self.stdout.strip(): message += f"\n >>>>> STDOUT >>>>>\n{self.stdout}\n <<<<< STDOUT <<<<<" if self.stderr and self.stderr.strip(): message += f"\n >>>>> STDERR >>>>>\n{self.stderr}\n <<<<< STDERR <<<<<" if self.data: message += f"\n Parsed JSON Data:\n{self._to_printable_data(self.data)}" return message + "\n" @staticmethod def _to_printable_data(data: Dict[str, Any]) -> str: """ Convert a data dictionary into a JSON string. """ return "\n".join( f" {line}" for line in json.dumps(data, indent=2, sort_keys=False).splitlines() ).rstrip() def collect_child_processes(pid: int) -> List[psutil.Process]: """ Try to collect any started child processes of the provided pid. Arguments: pid: The PID of the process Returns: List of child processes """ # Let's get the child processes of the started subprocess children: List[psutil.Process] try: parent = psutil.Process(pid) children = parent.children(recursive=True) except psutil.NoSuchProcess: children = [] return children def _get_cmdline(proc: psutil.Process) -> Optional[Any]: # pylint: disable=protected-access try: return proc._cmdline except AttributeError: # Cache the cmdline since that will be inaccessible once the process is terminated # and we use it in log calls try: cmdline = proc.cmdline() except (psutil.NoSuchProcess, psutil.AccessDenied): # pragma: no cover # OSX is more restrictive about the above information cmdline = None except OSError: # pragma: no cover # On Windows we've seen something like: # File " c: ... \lib\site-packages\pytestsalt\utils\__init__.py", line 182, in terminate_process # terminate_process_list(process_list, kill=slow_stop is False, slow_stop=slow_stop) # File " c: ... \lib\site-packages\pytestsalt\utils\__init__.py", line 130, in terminate_process_list # _terminate_process_list(process_list, kill=kill, slow_stop=slow_stop) # File " c: ... \lib\site-packages\pytestsalt\utils\__init__.py", line 78, in _terminate_process_list # cmdline = process.cmdline() # File " c: ... \lib\site-packages\psutil\__init__.py", line 786, in cmdline # return self._proc.cmdline() # File " c: ... \lib\site-packages\psutil\_pswindows.py", line 667, in wrapper # return fun(self, *args, **kwargs) # File " c: ... \lib\site-packages\psutil\_pswindows.py", line 745, in cmdline # ret = cext.proc_cmdline(self.pid, use_peb=True) # OSError: [WinError 299] Only part of a ReadProcessMemory or WriteProcessMemory request was completed: 'originated from ReadProcessMemory(ProcessParameters) cmdline = None except RuntimeError: # pragma: no cover # Also on windows # saltfactories\utils\processes\helpers.py:68: in _get_cmdline # cmdline = proc.as_dict() # c: ... \lib\site-packages\psutil\__init__.py:634: in as_dict # ret = meth() # c: ... \lib\site-packages\psutil\__init__.py:1186: in memory_full_info # return self._proc.memory_full_info() # c: ... \lib\site-packages\psutil\_pswindows.py:667: in wrapper # return fun(self, *args, **kwargs) # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ # # self = # # @wrap_exceptions # def memory_full_info(self): # basic_mem = self.memory_info() # > uss = cext.proc_memory_uss(self.pid) # E RuntimeError: NtQueryVirtualMemory failed # # c: ... \lib\site-packages\psutil\_pswindows.py:806: RuntimeError cmdline = None if not cmdline: # pragma: no cover try: cmdline = proc.as_dict() except psutil.NoSuchProcess: cmdline = f"" except (psutil.AccessDenied, OSError): # pragma: no cover cmdline = weakref.proxy(proc) proc._cmdline = cmdline return proc._cmdline # pylint: enable=protected-access def _terminate_process_list( process_list: List[psutil.Process], kill: bool = False, slow_stop: bool = False ) -> None: log.info( "Terminating process list:\n%s", pprint.pformat([_get_cmdline(proc) for proc in process_list]), ) for process in process_list[:]: # Iterate over copy of the list if not psutil.pid_exists(process.pid): process_list.remove(process) continue try: if not kill and process.status() == psutil.STATUS_ZOMBIE: # pragma: no cover # Zombie processes will exit once child processes also exit continue if kill: log.info("Killing process(%s): %s", process.pid, _get_cmdline(process)) process.kill() else: log.info("Terminating process(%s): %s", process.pid, _get_cmdline(process)) try: if slow_stop: # Allow coverage data to be written down to disk process.send_signal(signal.SIGTERM) try: process.wait(2) except psutil.TimeoutExpired: # pragma: no cover if psutil.pid_exists(process.pid): continue else: process.terminate() except OSError as exc: # pragma: no cover if exc.errno not in (errno.ESRCH, errno.EACCES): raise if not psutil.pid_exists(process.pid): process_list.remove(process) except psutil.NoSuchProcess: process_list.remove(process) def terminate_process_list( process_list: List[psutil.Process], kill: bool = False, slow_stop: bool = False ) -> None: """ Terminate a list of processes. Arguments: process_list: An iterable of :py:class:`psutil.Process` instances to terminate Keyword Arguments: kill: Kill the process instead of terminating it. slow_stop: First try to terminate each process in the list, and if termination was not successful, kill it. Returns: Nothing. """ def on_process_terminated(proc: psutil.Process) -> None: log.info( "Process %s terminated with exit code: %s", getattr(proc, "_cmdline", proc), proc.returncode, ) # Try to terminate processes with the provided kill and slow_stop parameters log.info("Terminating process list. 1st step. kill: %s, slow stop: %s", kill, slow_stop) # Remove duplicates from the process list seen_pids = [] start_count = len(process_list) for proc in process_list[:]: if proc.pid in seen_pids: process_list.remove(proc) seen_pids.append(proc.pid) end_count = len(process_list) if end_count < start_count: log.debug("Removed %d duplicates from the initial process list", start_count - end_count) _terminate_process_list(process_list, kill=kill, slow_stop=slow_stop) psutil.wait_procs(process_list, timeout=5, callback=on_process_terminated) if process_list: # If there's still processes to be terminated, retry and kill them if slow_stop is False log.info( "Terminating process list. 2nd step. kill: %s, slow stop: %s", slow_stop is False, slow_stop, ) _terminate_process_list(process_list, kill=slow_stop is False, slow_stop=slow_stop) psutil.wait_procs(process_list, timeout=5, callback=on_process_terminated) if process_list: # If there's still processes to be terminated, just kill them, no slow stopping now log.info("Terminating process list. 3rd step. kill: True, slow stop: False") _terminate_process_list(process_list, kill=True, slow_stop=False) psutil.wait_procs(process_list, timeout=5, callback=on_process_terminated) if process_list: # In there's still processes to be terminated, log a warning about it log.warning("Some processes failed to properly terminate: %s", process_list) def terminate_process( pid: Optional[int] = None, process: Optional[psutil.Process] = None, children: Optional[List[psutil.Process]] = None, kill_children: Optional[bool] = None, slow_stop: bool = False, ) -> None: """ Try to terminate/kill the started process. Keyword Arguments: pid: The PID of the process process: An instance of :py:class:`psutil.Process` children: An iterable of :py:class:`psutil.Process` instances, children to the process being terminated kill_children: Also try to terminate/kill child processes slow_stop: First try to terminate each process in the list, and if termination was not successful, kill it. """ children = children or [] process_list: List[psutil.Process] = [] if kill_children is None: # Always kill children if kill the parent process and kill_children was not set kill_children = True if slow_stop is False else kill_children if pid and not process: try: process = psutil.Process(pid) process_list.append(process) except psutil.NoSuchProcess: # Process is already gone process = None if kill_children: if process: children.extend(collect_child_processes(process.pid)) if children: process_list.extend(children) if process_list: if process: log.info("Stopping process %s and respective children: %s", process, children) else: log.info("Terminating process list: %s", process_list) terminate_process_list(process_list, kill=slow_stop is False, slow_stop=slow_stop) pytest-shell-utilities-1.9.7/src/pytestshellutils/utils/socket.py000066400000000000000000000010001470601045300254170ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # # pylint: disable=wildcard-import,unused-wildcard-import """ Namespace the standard library :py:mod:`socket` module. This module's sole purpose is to have the standard library :py:mod:`socket` module functions under a different namespace to be used in pytest-shell-utilities so that projects using it, which need to mock :py:mod:`socket` functions, don't influence the pytest-shell-utilities run time behavior. """ from socket import * pytest-shell-utilities-1.9.7/src/pytestshellutils/utils/socket.pyi000066400000000000000000000000251470601045300255760ustar00rootroot00000000000000from socket import * pytest-shell-utilities-1.9.7/src/pytestshellutils/utils/time.py000066400000000000000000000007701470601045300251020ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # # pylint: disable=wildcard-import,unused-wildcard-import """ Namespace the standard library :py:mod:`time` module. This module's sole purpose is to have the standard library :py:mod:`time` module functions under a different namespace to be used in pytest-shell-utilities so that projects using it, which need to mock :py:mod:`time` functions, don't influence the pytest-shell-utilities run time behavior. """ from time import * pytest-shell-utilities-1.9.7/src/pytestshellutils/utils/time.pyi000066400000000000000000000000231470601045300252420ustar00rootroot00000000000000from time import * pytest-shell-utilities-1.9.7/tests/000077500000000000000000000000001470601045300173405ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tests/__init__.py000066400000000000000000000000001470601045300214370ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tests/conftest.py000066400000000000000000000052441470601045300215440ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # import functools import logging import os import stat import tempfile import textwrap from typing import Optional from typing import Tuple import pytest try: from pytest import FixtureRequest except ImportError: from _pytest.fixtures import FixtureRequest try: # pragma: no cover import importlib.metadata pkg_version = importlib.metadata.version except ImportError: # pragma: no cover try: import importlib_metadata pkg_version = importlib_metadata.version except ImportError: # pragma: no cover import pkg_resources def pkg_version(package): # type: ignore[no-untyped-def] return pkg_resources.get_distribution(package).version log = logging.getLogger(__name__) def pkg_version_info(package: str) -> Tuple[int, ...]: """ Return a version info tuple for the given package. """ return tuple(int(part) for part in pkg_version(package).split(".") if part.isdigit()) if pkg_version_info("pytest") >= (6, 2): pytest_plugins = ["pytester"] else: # pragma: no cover @pytest.fixture def pytester() -> None: pytest.skip("The pytester fixture is not available in Pytest < 6.2.0") class Tempfiles: """ Class which generates temporary files and cleans them when done. """ def __init__(self, request: FixtureRequest): self.request = request def makepyfile( self, contents: str, prefix: Optional[str] = None, executable: bool = False ) -> str: """ Creates a python file and returns it's path. """ tfile = tempfile.NamedTemporaryFile("w", prefix=prefix or "tmp", suffix=".py", delete=False) contents = textwrap.dedent(contents.lstrip("\n")).strip() tfile.write(contents) tfile.close() if executable is True: st = os.stat(tfile.name) os.chmod(tfile.name, st.st_mode | stat.S_IEXEC) self.request.addfinalizer(functools.partial(self._delete_temp_file, tfile.name)) with open(tfile.name, encoding="utf-8") as rfh: log.debug( "Created python file with contents:\n>>>>> %s >>>>>\n%s\n<<<<< %s <<<<<\n", tfile.name, rfh.read(), tfile.name, ) return tfile.name def _delete_temp_file(self, fpath: str) -> None: """ Cleanup the temporary path. """ if os.path.exists(fpath): # pragma: no branch os.unlink(fpath) @pytest.fixture def tempfiles(request: FixtureRequest) -> Tempfiles: """ Temporary files fixture. """ return Tempfiles(request) pytest-shell-utilities-1.9.7/tests/functional/000077500000000000000000000000001470601045300215025ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tests/functional/__init__.py000066400000000000000000000000001470601045300236010ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tests/functional/shell/000077500000000000000000000000001470601045300226115ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tests/functional/shell/__init__.py000066400000000000000000000000001470601045300247100ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tests/functional/shell/test_daemon.py000066400000000000000000000610151470601045300254700ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # import functools import logging import pprint import re import sys import time from typing import List import attr import psutil import pytest from pytestskipmarkers.utils import platform from pytestshellutils.exceptions import FactoryNotRunning from pytestshellutils.exceptions import FactoryNotStarted from pytestshellutils.shell import Daemon from pytestshellutils.utils.processes import _get_cmdline from tests.conftest import Tempfiles try: from pytest import FixtureRequest from pytest import LogCaptureFixture except ImportError: from _pytest.fixtures import FixtureRequest from _pytest.logging import LogCaptureFixture PROCESS_START_TIMEOUT = 2 log = logging.getLogger(__name__) def kill_children(procs: List[psutil.Process]) -> None: # pragma: no cover _, alive = psutil.wait_procs(procs, timeout=3) for p in alive: p.kill() def test_daemon_process_termination(request: FixtureRequest, tempfiles: Tempfiles) -> None: primary_childrend_count = 5 secondary_children_count = 3 script = tempfiles.makepyfile( """ #!{shebang} # coding=utf-8 import time import multiprocessing def spin(): while True: try: time.sleep(0.25) except KeyboardInterrupt: break def spin_children(): procs = [] for idx in range({secondary_children_count}): proc = multiprocessing.Process(target=spin) proc.daemon = True proc.start() procs.append(proc) while True: try: time.sleep(0.25) except KeyboardInterrupt: break def main(): procs = [] for idx in range({primary_childrend_count}): proc = multiprocessing.Process(target=spin_children) procs.append(proc) proc.start() while True: try: time.sleep(0.25) except KeyboardInterrupt: break # We're not terminating child processes on purpose. Our code should handle it. # Support for windows test runs if __name__ == '__main__': multiprocessing.freeze_support() main() """.format( shebang=sys.executable, primary_childrend_count=primary_childrend_count, secondary_children_count=secondary_children_count, ), executable=True, ) if not platform.is_windows(): daemon = Daemon(start_timeout=1, script_name=script) else: # pragma: is-windows # Windows don't know how to handle python scripts directly daemon = Daemon(start_timeout=1, script_name=sys.executable, base_script_args=[script]) daemon.start() daemon_pid = daemon.pid # Make sure the daemon is terminated no matter what request.addfinalizer(daemon.terminate) # Allow the script to start time.sleep(PROCESS_START_TIMEOUT) assert psutil.pid_exists(daemon_pid) proc = psutil.Process(daemon_pid) children = proc.children(recursive=True) request.addfinalizer(functools.partial(kill_children, children)) child_count = len(children) expected_count = primary_childrend_count + (primary_childrend_count * secondary_children_count) if platform.is_windows() and sys.version_info >= (3, 7): # pragma: is-windows-ge-py37 # After Python 3.7 there's an extra spawning process expected_count += 1 if platform.is_darwin() and sys.version_info >= (3, 8): # pragma: is-darwin-ge-py38 # macOS defaults to spawning new processed after Python 3.8 # Account for the forking process expected_count += 1 assert child_count == expected_count, "{}!={}\n{}".format( child_count, expected_count, pprint.pformat([_get_cmdline(child) or child for child in children]), ) daemon.terminate() assert psutil.pid_exists(daemon_pid) is False for child in list(children): # pragma: no cover if psutil.pid_exists(child.pid): continue children.remove(child) assert not children, "len(children)=={} != 0\n{}".format( len(children), pprint.pformat([_get_cmdline(child) or child for child in children]) ) @pytest.mark.skip("Will debug later") def test_daemon_process_termination_parent_killed( request: FixtureRequest, tempfiles: Tempfiles ) -> None: primary_childrend_count = 5 secondary_children_count = 3 script = tempfiles.makepyfile( """ #!{shebang} # coding=utf-8 import time import multiprocessing def spin(): while True: try: time.sleep(0.25) except KeyboardInterrupt: break def spin_children(): procs = [] for idx in range({secondary_children_count}): proc = multiprocessing.Process(target=spin) proc.daemon = True proc.start() procs.append(proc) while True: try: time.sleep(0.25) except KeyboardInterrupt: break def main(): procs = [] for idx in range({primary_childrend_count}): proc = multiprocessing.Process(target=spin_children) procs.append(proc) proc.start() while True: try: time.sleep(0.25) except KeyboardInterrupt: break # We're not terminating child processes on purpose. Our code should handle it. # Support for windows test runs if __name__ == '__main__': multiprocessing.freeze_support() main() """.format( shebang=sys.executable, primary_childrend_count=primary_childrend_count, secondary_children_count=secondary_children_count, ), executable=True, ) if not platform.is_windows(): daemon = Daemon(start_timeout=1, script_name=script) else: # pragma: is-windows # Windows don't know how to handle python scripts directly daemon = Daemon(start_timeout=1, script_name=sys.executable, base_script_args=[script]) daemon.start() daemon_pid = daemon.pid # Make sure the daemon is terminated no matter what request.addfinalizer(daemon.terminate) # Allow the script to start time.sleep(PROCESS_START_TIMEOUT) assert psutil.pid_exists(daemon_pid) proc = psutil.Process(daemon_pid) children = proc.children(recursive=True) request.addfinalizer(functools.partial(kill_children, children)) assert len(children) == primary_childrend_count + ( primary_childrend_count * secondary_children_count ) # Pretend the parent process died. proc.kill() time.sleep(0.5) # We should should still be able to terminate all child processes daemon.terminate() assert psutil.pid_exists(daemon_pid) is False psutil.wait_procs(children, timeout=3) for child in list(children): if psutil.pid_exists(child.pid): continue children.remove(child) assert not children, "len(children)=={} != 0\n{}".format( len(children), pprint.pformat(children) ) @pytest.mark.parametrize("start_timeout", [0.1, 0.3]) def test_started_context_manager( request: FixtureRequest, tempfiles: Tempfiles, start_timeout: float ) -> None: script = tempfiles.makepyfile( r""" # coding=utf-8 import sys import time import multiprocessing def main(): time.sleep(3) sys.stdout.write("Done!\n") sys.stdout.flush() sys.exit(0) # Support for windows test runs if __name__ == '__main__': multiprocessing.freeze_support() main() """, executable=True, ) daemon = Daemon( script_name=sys.executable, base_script_args=[script], start_timeout=2, max_start_attempts=1, check_ports=[12345], ) # Make sure the daemon is terminated no matter what request.addfinalizer(daemon.terminate) with pytest.raises(FactoryNotStarted) as exc: daemon.start(start_timeout=start_timeout) match = re.search(r"which took (?P.*) seconds", str(exc.value)) assert match # XXX: Revisit logic # seconds = float(match.group("seconds")) # Must take at least start_timeout to start # assert seconds > start_timeout # Should not take more than start_timeout + 0.3 to start and fail # assert seconds < start_timeout + 0.3 # And using a context manager? with pytest.raises(FactoryNotStarted) as exc: started = None with daemon.started(start_timeout=start_timeout): # We should not even be able to set the following variable started = False # pragma: no cover assert started is None match = re.search(r"which took (?P.*) seconds", str(exc.value)) assert match # XXX: Revisit logic # seconds = float(match.group("seconds")) # Must take at least start_timeout to start # assert seconds > start_timeout # Should not take more than start_timeout + 0.3 to start and fail # assert seconds < start_timeout + 0.3 @pytest.fixture def factory_stopped_script(tempfiles: Tempfiles) -> str: return tempfiles.makepyfile( r""" # coding=utf-8 import os import sys import time import socket import multiprocessing def main(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('127.0.0.1', 12345)) sock.listen(5) try: while True: connection, address = sock.accept() connection.close() except (KeyboardInterrupt, SystemExit): pass finally: sock.close() sys.exit(0) # Support for windows test runs if __name__ == '__main__': multiprocessing.freeze_support() main() """, executable=True, ) def test_stopped_context_manager_raises_FactoryNotRunning( request: FixtureRequest, factory_stopped_script: str ) -> None: daemon = Daemon( script_name=sys.executable, base_script_args=[factory_stopped_script], start_timeout=3, max_start_attempts=1, check_ports=[12345], ) # Make sure the daemon is terminated no matter what request.addfinalizer(daemon.terminate) with pytest.raises(FactoryNotRunning): with daemon.stopped(): pass # pragma: no cover def test_stopped_context_manager(request: FixtureRequest, factory_stopped_script: str) -> None: daemon = Daemon( script_name=sys.executable, base_script_args=[factory_stopped_script], start_timeout=3, max_start_attempts=1, check_ports=[12345], ) # Make sure the daemon is terminated no matter what request.addfinalizer(daemon.terminate) with daemon.started(): assert daemon.is_running() with daemon.stopped(): assert daemon.is_running() is False assert daemon.is_running() @attr.s class DaemonCallbackCounter: before_start_callback_counter = attr.ib(default=0) # type: int after_start_callback_counter = attr.ib(default=0) # type: int before_terminate_callback_counter = attr.ib(default=0) # type: int after_terminate_callback_counter = attr.ib(default=0) # type: int def before_start_callback(self) -> None: self.before_start_callback_counter += 1 def after_start_callback(self) -> None: self.after_start_callback_counter += 1 def before_terminate_callback(self) -> None: self.before_terminate_callback_counter += 1 def after_terminate_callback(self) -> None: self.after_terminate_callback_counter += 1 @attr.s class DaemonContextCallbackCounter: daemon = attr.ib() # type: Daemon before_start_callback_counter = attr.ib(default=0) # type: int after_start_callback_counter = attr.ib(default=0) # type: int before_stop_callback_counter = attr.ib(default=0) # type: int after_stop_callback_counter = attr.ib(default=0) # type: int def before_start_callback(self, daemon: Daemon) -> None: assert daemon is self.daemon self.before_start_callback_counter += 1 def after_start_callback(self, daemon: Daemon) -> None: assert daemon is self.daemon self.after_start_callback_counter += 1 def before_stop_callback(self, daemon: Daemon) -> None: assert daemon is self.daemon self.before_stop_callback_counter += 1 def after_stop_callback(self, daemon: Daemon) -> None: assert daemon is self.daemon self.after_stop_callback_counter += 1 def test_daemon_callbacks(request: FixtureRequest, factory_stopped_script: str) -> None: daemon = Daemon( script_name=sys.executable, base_script_args=[factory_stopped_script], start_timeout=3, max_start_attempts=1, check_ports=[12345], ) callbacks = DaemonCallbackCounter() daemon.before_start(callbacks.before_start_callback) daemon.after_start(callbacks.after_start_callback) daemon.before_terminate(callbacks.before_terminate_callback) daemon.after_terminate(callbacks.after_terminate_callback) stopped_callbacks = DaemonContextCallbackCounter(daemon) # Make sure the daemon is terminated no matter what request.addfinalizer(daemon.terminate) daemon_started_once = False with daemon.started(): daemon_started_once = daemon.is_running() assert daemon_started_once is True # Assert against the non context manager callbacks assert callbacks.before_start_callback_counter == 1 assert callbacks.after_start_callback_counter == 1 assert callbacks.before_terminate_callback_counter == 0 assert callbacks.after_terminate_callback_counter == 0 # Assert against the context manager callbacks assert stopped_callbacks.before_stop_callback_counter == 0 assert stopped_callbacks.after_stop_callback_counter == 0 assert stopped_callbacks.before_start_callback_counter == 0 assert stopped_callbacks.after_start_callback_counter == 0 with daemon.stopped( before_stop_callback=stopped_callbacks.before_stop_callback, after_stop_callback=stopped_callbacks.after_stop_callback, before_start_callback=stopped_callbacks.before_start_callback, after_start_callback=stopped_callbacks.after_start_callback, ): assert daemon.is_running() is False # Assert against the context manager callbacks assert stopped_callbacks.before_stop_callback_counter == 1 assert stopped_callbacks.after_stop_callback_counter == 1 assert stopped_callbacks.before_start_callback_counter == 0 assert stopped_callbacks.after_start_callback_counter == 0 # Assert against the non context manager callbacks assert callbacks.before_start_callback_counter == 1 assert callbacks.after_start_callback_counter == 1 assert callbacks.before_terminate_callback_counter == 1 assert callbacks.after_terminate_callback_counter == 1 assert daemon.is_running() # Assert against the context manager callbacks assert stopped_callbacks.before_stop_callback_counter == 1 assert stopped_callbacks.after_stop_callback_counter == 1 assert stopped_callbacks.before_start_callback_counter == 1 assert stopped_callbacks.after_start_callback_counter == 1 # Assert against the non context manager callbacks assert callbacks.before_start_callback_counter == 2 assert callbacks.after_start_callback_counter == 2 assert callbacks.before_terminate_callback_counter == 1 assert callbacks.after_terminate_callback_counter == 1 # Let's got through stopped again, the stopped_callbacks should not be called again # because they are not passed into .stopped() with daemon.stopped(): assert daemon.is_running() is False assert daemon.is_running() # Assert against the context manager callbacks assert stopped_callbacks.before_stop_callback_counter == 1 assert stopped_callbacks.after_stop_callback_counter == 1 assert stopped_callbacks.before_start_callback_counter == 1 assert stopped_callbacks.after_start_callback_counter == 1 assert daemon_started_once is True # Assert against the non context manager callbacks assert callbacks.before_start_callback_counter == 3 assert callbacks.after_start_callback_counter == 3 assert callbacks.before_terminate_callback_counter == 3 assert callbacks.after_terminate_callback_counter == 3 @attr.s class DaemonStartCheckCounter: custom_start_check_1_callback_counter = attr.ib(default=0) # type: int custom_start_check_2_callback_counter = attr.ib(default=0) # type: int custom_start_check_3_callback_counter = attr.ib(default=0) # type: int def custom_start_check_1_callback(self, timeout_at: float) -> bool: self.custom_start_check_1_callback_counter += 1 if self.custom_start_check_1_callback_counter > 2: return True return False def custom_start_check_2_callback(self, timeout_at: float) -> bool: self.custom_start_check_2_callback_counter += 1 if self.custom_start_check_2_callback_counter > 2: return True raise Exception("Foo!") def custom_start_check_3_callback(self, timeout_at: float) -> bool: self.custom_start_check_3_callback_counter += 1 time.sleep(1) return False def test_daemon_start_check_callbacks(request: FixtureRequest, factory_stopped_script: str) -> None: daemon = Daemon( script_name=sys.executable, base_script_args=[factory_stopped_script], start_timeout=3, max_start_attempts=1, check_ports=[12345], ) callbacks = DaemonStartCheckCounter() daemon.start_check(callbacks.custom_start_check_1_callback) daemon.start_check(callbacks.custom_start_check_2_callback) daemon_start_check_callbacks = daemon.get_start_check_callbacks() with daemon.started(): # Both start callbacks should have run 3 times by now, at which # time, they would have returned True pass assert callbacks.custom_start_check_1_callback_counter == 3 assert callbacks.custom_start_check_2_callback_counter == 3 # Assert that the list of callbacks is the same before running the start checks assert daemon.get_start_check_callbacks() == daemon_start_check_callbacks def test_daemon_no_start_check_callbacks(request: FixtureRequest, tempfiles: Tempfiles) -> None: script = tempfiles.makepyfile( r""" # coding=utf-8 import sys import time import multiprocessing def main(): time.sleep(3) sys.stdout.write("Done!\n") sys.stdout.flush() sys.exit(0) # Support for windows test runs if __name__ == '__main__': multiprocessing.freeze_support() main() """, executable=True, ) daemon = Daemon( script_name=sys.executable, base_script_args=[script], start_timeout=2, max_start_attempts=1, ) # Remove the check ports callback daemon._start_checks_callbacks.clear() # Make sure the daemon is terminated no matter what request.addfinalizer(daemon.terminate) with daemon.started(): # Daemon started without running any start checks pass assert not daemon.get_start_check_callbacks() def test_daemon_start_check_callbacks_factory_not_running( request: FixtureRequest, tempfiles: Tempfiles ) -> None: script = tempfiles.makepyfile( r""" # coding=utf-8 import sys import time import multiprocessing def main(): time.sleep(2) sys.stdout.write("Done!\n") sys.stdout.flush() sys.exit(0) # Support for windows test runs if __name__ == '__main__': multiprocessing.freeze_support() main() """, executable=True, ) callbacks = DaemonStartCheckCounter() daemon = Daemon( script_name=sys.executable, base_script_args=[script], start_timeout=2, max_start_attempts=1, ) # Make sure the daemon is terminated no matter what request.addfinalizer(daemon.terminate) daemon.start_check(callbacks.custom_start_check_3_callback) with pytest.raises(FactoryNotStarted): daemon.start() # Make sure the callback was called at least once assert callbacks.custom_start_check_3_callback_counter > 1 def test_context_manager_returns_class_instance(tempfiles: Tempfiles) -> None: script = tempfiles.makepyfile( r""" # coding=utf-8 import sys import time import multiprocessing def main(): while True: try: time.sleep(0.1) except KeyboardInterrupt: break sys.stdout.write("Done!\n") sys.stdout.flush() sys.exit(0) # Support for windows test runs if __name__ == '__main__': multiprocessing.freeze_support() main() """, executable=True, ) daemon = Daemon( script_name=sys.executable, base_script_args=[script], start_timeout=1, max_start_attempts=1, ) # Without starting the factory started = d = None with pytest.raises(RuntimeError): with daemon as d: # We should not even be able to set the following variable started = d.is_running() # pragma: no cover assert d is None assert started is None # After starting the factory started = False daemon.start() with daemon as d: # We should not even be able to set the following variable started = d.is_running() assert d.is_running() is False assert started is True # By starting the factory and passing timeout directly started = False with daemon.started(start_timeout=1) as d: # We should not even be able to set the following variable started = d.is_running() assert d.is_running() is False assert started is True # By starting the factory without any keyword arguments started = False with daemon.started() as d: # We should not even be able to set the following variable started = d.is_running() assert d.is_running() is False assert started is True @pytest.mark.parametrize("max_start_attempts", [1, 2, 3]) def test_exact_max_start_attempts( tempfiles: Tempfiles, caplog: LogCaptureFixture, max_start_attempts: int ) -> None: """ This test asserts that we properly report max_start_attempts. """ script = tempfiles.makepyfile( r""" # coding=utf-8 import sys import time import multiprocessing def main(): time.sleep(0.125) sys.exit(1) # Support for windows test runs if __name__ == '__main__': multiprocessing.freeze_support() main() """, executable=True, ) daemon = Daemon( script_name=sys.executable, base_script_args=[script], start_timeout=0.1, max_start_attempts=max_start_attempts, check_ports=[12345], ) with caplog.at_level(logging.INFO): with pytest.raises(FactoryNotStarted) as exc: daemon.start() assert "confirm running status after {} attempts".format(max_start_attempts) in str( exc.value ) start_attempts = [ "Attempt: {} of {}".format(n, max_start_attempts) for n in range(1, max_start_attempts + 1) ] for record in caplog.records: if not record.message.startswith("Starting Daemon"): continue for idx, start_attempt in enumerate(list(start_attempts)): if start_attempt in record.message: start_attempts.pop(idx) assert not start_attempts pytest-shell-utilities-1.9.7/tests/functional/shell/test_fixture.py000066400000000000000000000025621470601045300257150ustar00rootroot00000000000000# Copyright 2022-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # import pathlib import sys import tempfile import pytest from pytestshellutils.shell import Subprocess from tests.conftest import Tempfiles def test_run_call(tempfiles: Tempfiles, shell: Subprocess) -> None: script = tempfiles.makepyfile( """ # coding=utf-8 import os import json print(json.dumps(dict(**os.environ)), flush=True) exit(0) """ ) result = shell.run(sys.executable, script) assert result.returncode == 0 def test_run_cwd(tempfiles: Tempfiles, shell: Subprocess) -> None: system_tempdir = str(pathlib.Path(tempfile.gettempdir()).resolve()) assert str(shell.cwd.resolve()) != system_tempdir script = tempfiles.makepyfile( """ # coding=utf-8 import pathlib print(str(pathlib.Path.cwd().resolve()), flush=True) exit(0) """ ) result = shell.run(sys.executable, script, cwd=system_tempdir) assert result.returncode == 0 assert result.stdout.strip() == system_tempdir def test_run_shell(shell: Subprocess) -> None: with pytest.raises(FileNotFoundError): shell.run("exit", "0") with pytest.raises(FileNotFoundError): shell.run("exit", "0", shell=False) result = shell.run("exit", "0", shell=True) assert result.returncode == 0 pytest-shell-utilities-1.9.7/tests/functional/shell/test_script_subprocess.py000066400000000000000000000147521470601045300300070ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # import os import pathlib import sys from typing import Any from typing import cast import pytest from pytest_subtests import SubTests from pytestshellutils.customtypes import EnvironDict from pytestshellutils.exceptions import FactoryTimeout from pytestshellutils.shell import ScriptSubprocess from tests.conftest import Tempfiles @pytest.mark.parametrize("exitcode", [0, 1, 3, 9, 40, 120]) def test_exitcode(exitcode: int, tempfiles: Tempfiles) -> None: shell = ScriptSubprocess(script_name=sys.executable) script = tempfiles.makepyfile( """ # coding=utf-8 import time time.sleep(0.125) exit({}) """.format( exitcode ) ) result = shell.run(script) assert result.returncode == exitcode def test_timeout_defined_on_class_instantiation(tempfiles: Tempfiles) -> None: shell = ScriptSubprocess(script_name=sys.executable, timeout=0.5) script = tempfiles.makepyfile( """ # coding=utf-8 import time time.sleep(1) exit(0) """ ) with pytest.raises(FactoryTimeout): shell.run(script) def test_timeout_defined_run(tempfiles: Tempfiles) -> None: shell = ScriptSubprocess(script_name=sys.executable) script = tempfiles.makepyfile( """ # coding=utf-8 import time time.sleep(0.5) exit(0) """ ) result = shell.run(script) assert result.returncode == 0 script = tempfiles.makepyfile( """ # coding=utf-8 import time time.sleep(0.5) exit(0) """ ) with pytest.raises(FactoryTimeout): shell.run(script, _timeout=0.1) @pytest.mark.parametrize( "input_str,expected_object", [ # Good JSON ('{"a": "a", "1": 1}', {"a": "a", "1": 1}), # Bad JSON ("{'a': 'a', '1': 1}", None), ], ) def test_json_output(input_str: str, expected_object: Any, tempfiles: Tempfiles) -> None: shell = ScriptSubprocess(script_name=sys.executable) script = tempfiles.makepyfile( """ # coding=utf-8 import sys sys.stdout.write('''{}''') exit(0) """.format( input_str ) ) result = shell.run(script) assert result.returncode == 0 if expected_object: assert result.data == expected_object assert result.stdout == input_str def test_stderr_output(tempfiles: Tempfiles) -> None: input_str = "Thou shalt not exit cleanly" shell = ScriptSubprocess(script_name=sys.executable) script = tempfiles.makepyfile( """ # coding=utf-8 exit("{}") """.format( input_str ) ) result = shell.run(script) assert result.returncode == 1 assert result.stderr == input_str + "\n" def test_unicode_output(tempfiles: Tempfiles) -> None: shell = ScriptSubprocess(script_name=sys.executable) script = tempfiles.makepyfile( r""" # coding=utf-8 from __future__ import print_function import sys sys.stdout.write(u'STDOUT F\xe1tima') sys.stdout.flush() sys.stderr.write(u'STDERR F\xe1tima') sys.stderr.flush() exit(0) """ ) result = shell.run(script) assert result.returncode == 0, str(result) assert result.stdout == "STDOUT Fátima" assert result.stderr == "STDERR Fátima" def test_process_failed_to_start(tempfiles: Tempfiles) -> None: shell = ScriptSubprocess(script_name=sys.executable) script = tempfiles.makepyfile( """ # coding=utf-8 1/0 """ ) result = shell.run(script) assert result.returncode == 1 assert "ZeroDivisionError: division by zero" in result.stderr def test_environ(tempfiles: Tempfiles) -> None: environ = cast(EnvironDict, os.environ.copy()) environ["FOO"] = "foo" shell = ScriptSubprocess(script_name=sys.executable, environ=environ) script = tempfiles.makepyfile( """ # coding=utf-8 import os import json print(json.dumps(dict(**os.environ)), flush=True) exit(0) """ ) result = shell.run(script) assert result.returncode == 0 assert result.data assert "FOO" in result.data assert result.data["FOO"] == "foo" def test_env_in_run_call(tempfiles: Tempfiles) -> None: env = cast(EnvironDict, {"FOO": "bar"}) environ = cast(EnvironDict, os.environ.copy()) environ["FOO"] = "foo" shell = ScriptSubprocess(script_name=sys.executable, environ=environ) script = tempfiles.makepyfile( """ # coding=utf-8 import os import json print(json.dumps(dict(**os.environ)), flush=True) exit(0) """ ) result = shell.run(script, env=env) assert result.returncode == 0 assert result.data assert "FOO" in result.data assert result.data["FOO"] == env["FOO"] def test_not_started() -> None: shell = ScriptSubprocess(script_name=sys.executable) assert shell.is_running() is False assert not shell.pid assert shell.terminate() is None def test_display_name(tempfiles: Tempfiles) -> None: python_binary_name = pathlib.Path(sys.executable).name display_name = "ScriptSubprocess({})".format(python_binary_name) shell = ScriptSubprocess(script_name=sys.executable) assert shell.get_display_name() == display_name script = tempfiles.makepyfile( """ # coding=utf-8 import time time.sleep(0.125) exit(0) """ ) result = shell.run(script) assert result.returncode == 0 assert shell.get_display_name() == display_name def test_get_script_path(subtests: SubTests) -> None: python_binary_name = pathlib.Path(sys.executable).name with subtests.test(script_name=sys.executable): shell = ScriptSubprocess(script_name=sys.executable) assert shell.get_script_path() == sys.executable with subtests.test(script_name=python_binary_name): shell = ScriptSubprocess(script_name=python_binary_name) assert shell.get_script_path() == sys.executable python_binary_name = "{}3.100".format(python_binary_name) with subtests.test(script_name=python_binary_name): shell = ScriptSubprocess(script_name=python_binary_name) with pytest.raises(FileNotFoundError) as exc: shell.get_script_path() assert "The CLI script {!r} does not exist".format(python_binary_name) in str(exc) pytest-shell-utilities-1.9.7/tests/functional/shell/test_subprocess.py000066400000000000000000000144041470601045300264150ustar00rootroot00000000000000# Copyright 2022-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # import os import pathlib import sys import tempfile from typing import Any from typing import cast import pytest from pytestshellutils.customtypes import EnvironDict from pytestshellutils.exceptions import FactoryTimeout from pytestshellutils.shell import Subprocess from tests.conftest import Tempfiles @pytest.mark.parametrize("exitcode", [0, 1, 3, 9, 40, 120]) def test_exitcode(exitcode: int, tempfiles: Tempfiles) -> None: shell = Subprocess() script = tempfiles.makepyfile( """ # coding=utf-8 import time time.sleep(0.125) exit({}) """.format( exitcode ) ) result = shell.run(sys.executable, script) assert result.returncode == exitcode def test_timeout_defined_on_class_instantiation(tempfiles: Tempfiles) -> None: shell = Subprocess(timeout=0.5) script = tempfiles.makepyfile( """ # coding=utf-8 import time time.sleep(1) exit(0) """ ) with pytest.raises(FactoryTimeout): shell.run(sys.executable, script) def test_timeout_defined_run(tempfiles: Tempfiles) -> None: shell = Subprocess() script = tempfiles.makepyfile( """ # coding=utf-8 import time time.sleep(0.5) exit(0) """ ) result = shell.run(sys.executable, script) assert result.returncode == 0 script = tempfiles.makepyfile( """ # coding=utf-8 import time time.sleep(0.5) exit(0) """ ) with pytest.raises(FactoryTimeout): shell.run(sys.executable, script, _timeout=0.1) @pytest.mark.parametrize( "input_str,expected_object", [ # Good JSON ('{"a": "a", "1": 1}', {"a": "a", "1": 1}), # Bad JSON ("{'a': 'a', '1': 1}", None), ], ) def test_json_output(input_str: str, expected_object: Any, tempfiles: Tempfiles) -> None: shell = Subprocess() script = tempfiles.makepyfile( """ # coding=utf-8 import sys sys.stdout.write('''{}''') exit(0) """.format( input_str ) ) result = shell.run(sys.executable, script) assert result.returncode == 0 if expected_object: assert result.data == expected_object assert result.stdout == input_str def test_stderr_output(tempfiles: Tempfiles) -> None: input_str = "Thou shalt not exit cleanly" shell = Subprocess() script = tempfiles.makepyfile( """ # coding=utf-8 exit("{}") """.format( input_str ) ) result = shell.run(sys.executable, script) assert result.returncode == 1 assert result.stderr == input_str + "\n" def test_unicode_output(tempfiles: Tempfiles) -> None: shell = Subprocess() script = tempfiles.makepyfile( r""" # coding=utf-8 from __future__ import print_function import sys sys.stdout.write(u'STDOUT F\xe1tima') sys.stdout.flush() sys.stderr.write(u'STDERR F\xe1tima') sys.stderr.flush() exit(0) """ ) result = shell.run(sys.executable, script) assert result.returncode == 0, str(result) assert result.stdout == "STDOUT Fátima" assert result.stderr == "STDERR Fátima" def test_process_failed_to_start(tempfiles: Tempfiles) -> None: shell = Subprocess() script = tempfiles.makepyfile( """ # coding=utf-8 1/0 """ ) result = shell.run(sys.executable, script) assert result.returncode == 1 assert "ZeroDivisionError: division by zero" in result.stderr def test_environ(tempfiles: Tempfiles) -> None: environ = cast(EnvironDict, os.environ.copy()) environ["FOO"] = "foo" shell = Subprocess(environ=environ) script = tempfiles.makepyfile( """ # coding=utf-8 import os import json print(json.dumps(dict(**os.environ)), flush=True) exit(0) """ ) result = shell.run(sys.executable, script) assert result.returncode == 0 assert result.data assert "FOO" in result.data assert result.data["FOO"] == "foo" def test_env_in_run_call(tempfiles: Tempfiles) -> None: env = cast(EnvironDict, {"FOO": "bar"}) environ = cast(EnvironDict, os.environ.copy()) environ["FOO"] = "foo" shell = Subprocess(environ=environ) script = tempfiles.makepyfile( """ # coding=utf-8 import os import json print(json.dumps(dict(**os.environ)), flush=True) exit(0) """ ) result = shell.run(sys.executable, script, env=env) assert result.returncode == 0 assert result.data assert "FOO" in result.data assert result.data["FOO"] == env["FOO"] def test_not_started() -> None: shell = Subprocess() assert shell.is_running() is False assert not shell.pid assert shell.terminate() is None def test_display_name(tempfiles: Tempfiles) -> None: shell = Subprocess() assert shell.get_display_name() == "Subprocess()" script = tempfiles.makepyfile( """ # coding=utf-8 import time time.sleep(0.125) exit(0) """ ) result = shell.run(sys.executable, script) assert result.returncode == 0 assert shell.get_display_name() == "Subprocess([{!r}, {!r}])".format(sys.executable, script) def test_run_cwd(tmp_path: str, tempfiles: Tempfiles, shell: Subprocess) -> None: shell = Subprocess(cwd=tmp_path) system_tempdir = str(pathlib.Path(tempfile.gettempdir()).resolve()) assert str(shell.cwd.resolve()) != system_tempdir script = tempfiles.makepyfile( """ # coding=utf-8 import pathlib print(str(pathlib.Path.cwd().resolve()), flush=True) exit(0) """ ) result = shell.run(sys.executable, script, cwd=system_tempdir) assert result.returncode == 0 assert result.stdout.strip() == system_tempdir def test_run_shell() -> None: shell = Subprocess() with pytest.raises(FileNotFoundError): shell.run("exit", "0") with pytest.raises(FileNotFoundError): shell.run("exit", "0", shell=False) result = shell.run("exit", "0", shell=True) assert result.returncode == 0 pytest-shell-utilities-1.9.7/tests/functional/test_exceptions.py000066400000000000000000000121421470601045300252740ustar00rootroot00000000000000# Copyright 2022-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 import textwrap import pytest import pytestshellutils.exceptions as exceptions from pytestshellutils.utils.processes import ProcessResult def test_process_failed_message() -> None: message = "The message" with pytest.raises(exceptions.FactoryFailure) as exc: raise exceptions.FactoryFailure(message) assert str(exc.value) == message def test_process_failed_cmdline() -> None: message = "The message" cmdline = ["python", "--version"] expected = textwrap.dedent( """\ {} ProcessResult Command Line: {!r} Returncode: 0 """.format( message, cmdline ) ) pres = ProcessResult(returncode=0, cmdline=cmdline, stdout="", stderr="") with pytest.raises(exceptions.FactoryFailure) as exc: raise exceptions.FactoryFailure(message, process_result=pres) output = str(exc.value) assert output == expected def test_process_failed_returncode() -> None: message = "The message" returncode = 1 expected = textwrap.dedent( """\ {} ProcessResult Returncode: {} """.format( message, returncode ) ) pres = ProcessResult(returncode=returncode, stdout="", stderr="") with pytest.raises(exceptions.FactoryFailure) as exc: raise exceptions.FactoryFailure(message, process_result=pres) output = str(exc.value) assert output == expected def test_process_failed_stdout() -> None: message = "The message" stdout = "This is the STDOUT" expected = textwrap.dedent( """\ {} ProcessResult Returncode: 0 Process Output: >>>>> STDOUT >>>>> {} <<<<< STDOUT <<<<< """.format( message, stdout ) ) pres = ProcessResult(returncode=0, stdout=stdout, stderr="") with pytest.raises(exceptions.FactoryFailure) as exc: raise exceptions.FactoryFailure(message, process_result=pres) output = str(exc.value) assert output == expected def test_process_failed_stderr() -> None: message = "The message" stderr = "This is the STDERR" expected = textwrap.dedent( """\ {} ProcessResult Returncode: 0 Process Output: >>>>> STDERR >>>>> {} <<<<< STDERR <<<<< """.format( message, stderr ) ) pres = ProcessResult(returncode=0, stdout="", stderr=stderr) with pytest.raises(exceptions.FactoryFailure) as exc: raise exceptions.FactoryFailure(message, process_result=pres) output = str(exc.value) assert output == expected def test_process_failed_stdout_and_stderr() -> None: message = "The message" stdout = "This is the STDOUT" stderr = "This is the STDERR" expected = textwrap.dedent( """\ {} ProcessResult Returncode: 0 Process Output: >>>>> STDOUT >>>>> {} <<<<< STDOUT <<<<< >>>>> STDERR >>>>> {} <<<<< STDERR <<<<< """.format( message, stdout, stderr ) ) pres = ProcessResult(returncode=0, stdout=stdout, stderr=stderr) with pytest.raises(exceptions.FactoryFailure) as exc: raise exceptions.FactoryFailure(message, process_result=pres) output = str(exc.value) assert output == expected def test_process_failed_cmdline_stdout_and_stderr() -> None: message = "The message" stdout = "This is the STDOUT" stderr = "This is the STDERR" cmdline = ["python", "--version"] expected = textwrap.dedent( """\ {} ProcessResult Command Line: {!r} Returncode: 0 Process Output: >>>>> STDOUT >>>>> {} <<<<< STDOUT <<<<< >>>>> STDERR >>>>> {} <<<<< STDERR <<<<< """.format( message, cmdline, stdout, stderr ) ) pres = ProcessResult(returncode=0, stdout=stdout, stderr=stderr, cmdline=cmdline) with pytest.raises(exceptions.FactoryFailure) as exc: raise exceptions.FactoryFailure(message, process_result=pres) output = str(exc.value) assert output == expected def test_process_failed_cmdline_stdout_stderr_and_returncode() -> None: message = "The message" stdout = "This is the STDOUT" stderr = "This is the STDERR" cmdline = ["python", "--version"] returncode = 1 expected = textwrap.dedent( """\ {} ProcessResult Command Line: {!r} Returncode: {} Process Output: >>>>> STDOUT >>>>> {} <<<<< STDOUT <<<<< >>>>> STDERR >>>>> {} <<<<< STDERR <<<<< """.format( message, cmdline, returncode, stdout, stderr ) ) pres = ProcessResult(returncode=returncode, stdout=stdout, stderr=stderr, cmdline=cmdline) with pytest.raises(exceptions.FactoryFailure) as exc: raise exceptions.FactoryFailure(message, process_result=pres) output = str(exc.value) assert output == expected pytest-shell-utilities-1.9.7/tests/support/000077500000000000000000000000001470601045300210545ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tests/support/coverage/000077500000000000000000000000001470601045300226475ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tests/support/coverage/sitecustomize.py000066400000000000000000000002411470601045300261250ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # try: import coverage coverage.process_startup() except ImportError: pass pytest-shell-utilities-1.9.7/tests/unit/000077500000000000000000000000001470601045300203175ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tests/unit/__init__.py000066400000000000000000000000001470601045300224160ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tests/unit/customtypes/000077500000000000000000000000001470601045300227165ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tests/unit/customtypes/__init__.py000066400000000000000000000000001470601045300250150ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tests/unit/customtypes/test_callback.py000066400000000000000000000022431470601045300260640ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # from typing import Any from typing import Dict import pytest from pytestshellutils.customtypes import Callback def func(*args: Any, **kwargs: Any) -> Dict[str, Any]: return {"args": args, "kwargs": kwargs} @pytest.fixture def callback() -> Callback: return Callback(func=func, args=("a1", "a2"), kwargs={"keyword_argument": True}) def test___str__(callback: Callback) -> None: assert str(callback) == "func('a1', 'a2', keyword_argument=True)" def test___call__(callback: Callback) -> None: assert callback() == {"args": ("a1", "a2"), "kwargs": {"keyword_argument": True}} def test___call__extra_args(callback: Callback) -> None: assert callback("bar") == {"args": ("bar", "a1", "a2"), "kwargs": {"keyword_argument": True}} def test___call__extra_kwargs(callback: Callback) -> None: assert callback(bar=2) == {"args": ("a1", "a2"), "kwargs": {"keyword_argument": True, "bar": 2}} def test___call__override_kwarg(callback: Callback) -> None: assert callback(keyword_argument=False) == { "args": ("a1", "a2"), "kwargs": {"keyword_argument": False}, } pytest-shell-utilities-1.9.7/tests/unit/utils/000077500000000000000000000000001470601045300214575ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tests/unit/utils/__init__.py000066400000000000000000000000001470601045300235560ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tests/unit/utils/processes/000077500000000000000000000000001470601045300234655ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tests/unit/utils/processes/__init__.py000066400000000000000000000000001470601045300255640ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tests/unit/utils/processes/test_processresult.py000066400000000000000000000127721470601045300300240ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 """ Test ``pytestshellutils.utils.processes.ProcessResult``. """ import json as jsonlib import logging import textwrap import pytest from pytest_subtests import SubTests from pytestshellutils.utils.processes import ProcessResult log = logging.getLogger(__name__) @pytest.mark.parametrize("returncode", [None, 1.0, -1.0, "0"]) def test_non_int_returncode_raises_exception(returncode: int) -> None: with pytest.raises(ValueError): ProcessResult(returncode=returncode, stdout="", stderr="") def test_attributes(subtests: SubTests) -> None: returncode = 0 stdout = "STDOUT" stderr = "STDERR" cmdline = None data = None with subtests.test(returncode=returncode, stdout=stdout, stderr=stderr): ret = ProcessResult(returncode=returncode, stdout=stdout, stderr=stderr) assert ret.returncode == returncode assert ret.stdout == stdout assert ret.stderr == stderr assert ret.data == data assert ret.cmdline == cmdline cmdline = ["1", "2", "3"] with subtests.test(returncode=returncode, stdout=stdout, stderr=stderr, cmdline=cmdline): ret = ProcessResult(returncode=returncode, stdout=stdout, stderr=stderr, cmdline=cmdline) assert ret.returncode == returncode assert ret.stdout == stdout assert ret.stderr == stderr assert ret.data == data assert ret.cmdline == cmdline data = {"ret": {"a": 1}} stdout = jsonlib.dumps(data) with subtests.test( returncode=returncode, stdout=stdout, stderr=stderr, cmdline=cmdline, data=data ): ret = ProcessResult(returncode=returncode, stdout=stdout, stderr=stderr, cmdline=cmdline) assert ret.returncode == returncode assert ret.stdout == stdout assert ret.stderr == stderr assert ret.data == data assert ret.cmdline == cmdline data_key = "ret" with subtests.test( returncode=returncode, stdout=stdout, stderr=stderr, cmdline=cmdline, data=data, data_key=data_key, ): ret = ProcessResult( returncode=returncode, stdout=stdout, stderr=stderr, cmdline=cmdline, data_key="ret" ) assert ret.returncode == returncode assert ret.stdout == stdout assert ret.stderr == stderr assert ret.data == data[data_key] assert ret.cmdline == cmdline def test_str_formatting(subtests: SubTests) -> None: returncode = 0 stdout = "STDOUT" stderr = "STDERR" cmdline = None data = {"ret": {"a": 1}} data_key = None with subtests.test(returncode=returncode, stdout=stdout, stderr=stderr): ret = ProcessResult(returncode=returncode, stdout=stdout, stderr=stderr) expected = textwrap.dedent( """\ ProcessResult Returncode: {} Process Output: >>>>> STDOUT >>>>> {} <<<<< STDOUT <<<<< >>>>> STDERR >>>>> {} <<<<< STDERR <<<<< """.format( returncode, stdout, stderr ) ) assert str(ret) == expected cmdline = ["1", "2", "3"] with subtests.test(returncode=returncode, stdout=stdout, stderr=stderr, cmdline=cmdline): ret = ProcessResult(returncode=returncode, stdout=stdout, stderr=stderr, cmdline=cmdline) expected = textwrap.dedent( """\ ProcessResult Command Line: {!r} Returncode: {} Process Output: >>>>> STDOUT >>>>> {} <<<<< STDOUT <<<<< >>>>> STDERR >>>>> {} <<<<< STDERR <<<<< """.format( cmdline, returncode, stdout, stderr, ) ) assert str(ret) == expected stdout = jsonlib.dumps(data) with subtests.test(returncode=returncode, stdout=stdout, stderr=stderr, cmdline=cmdline): ret = ProcessResult(returncode=returncode, stdout=stdout, stderr=stderr, cmdline=cmdline) expected = ( textwrap.dedent( f"""\ ProcessResult Command Line: {cmdline!r} Returncode: {returncode} Process Output: >>>>> STDOUT >>>>> {stdout} <<<<< STDOUT <<<<< >>>>> STDERR >>>>> {stderr} <<<<< STDERR <<<<< Parsed JSON Data: """ ) + ProcessResult._to_printable_data(data) + "\n" ) assert str(ret) == expected data_key = "ret" with subtests.test( returncode=returncode, stdout=stdout, stderr=stderr, cmdline=cmdline, data_key=data_key ): ret = ProcessResult( returncode=returncode, stdout=stdout, stderr=stderr, cmdline=cmdline, data_key=data_key ) expected = ( textwrap.dedent( f"""\ ProcessResult Command Line: {cmdline!r} Returncode: {returncode} Process Output: >>>>> STDOUT >>>>> {stdout} <<<<< STDOUT <<<<< >>>>> STDERR >>>>> {stderr} <<<<< STDERR <<<<< Parsed JSON Data: """ ) + ProcessResult._to_printable_data(data[data_key]) + "\n" ) assert str(ret) == expected pytest-shell-utilities-1.9.7/tests/unit/utils/processes/test_processresult_matcher.py000066400000000000000000000022771470601045300315260ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 """ Test ``pytestshellutils.utils.processes.ProcessResult``. """ import pytest from pytestshellutils.utils.processes import ProcessResult MARY_HAD_A_LITTLE_LAMB = """\ Mary had a little lamb, Its fleece was white as snow; And everywhere that Mary went The lamb was sure to go. """ @pytest.fixture def process_result() -> ProcessResult: return ProcessResult(returncode=0, stdout=MARY_HAD_A_LITTLE_LAMB, stderr=None) def test_instance_types() -> None: ret = ProcessResult(returncode=0, stdout="STDOUT", stderr="STDERR") assert isinstance(ret.stdout, str) assert isinstance(ret.stderr, str) ret = ProcessResult(returncode=0, stdout=None, stderr=None) assert ret.stdout is None assert ret.stderr is None ret = ProcessResult(returncode=0, stdout=1, stderr=2, data=None) assert ret.stdout == 1 assert ret.stderr == 2 def test_matcher_attribute(process_result: ProcessResult) -> None: process_result.stdout.matcher.fnmatch_lines_random( [ "*had a little*", "Its fleece was white*", "*Mary went", "The lamb was sure to go.", ] ) pytest-shell-utilities-1.9.7/tests/unit/utils/test_format_callback_to_string.py000066400000000000000000000021741470601045300302700ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # from pytestshellutils.utils import format_callback_to_string def test_format_from_string() -> None: func = "the_function" args = ("one", "two") kwargs = {"three": 3} assert ( format_callback_to_string(func, args=args, kwargs=kwargs) == "the_function('one', 'two', three=3)" ) def test_format_just_args() -> None: func = "the_function" args = ("one", "two") assert format_callback_to_string(func, args=args) == "the_function('one', 'two')" def test_format_just_kwargs() -> None: func = "the_function" kwargs = {"three": 3} assert format_callback_to_string(func, kwargs=kwargs) == "the_function(three=3)" def test_format_no_args_nor_kwargs() -> None: func = "the_function" assert format_callback_to_string(func) == "the_function()" def test_format_from_function() -> None: func = format_callback_to_string args = ("one", "two") kwargs = {"three": 3} assert ( format_callback_to_string(func, args, kwargs) == "format_callback_to_string('one', 'two', three=3)" ) pytest-shell-utilities-1.9.7/tests/unit/utils/test_ports.py000066400000000000000000000056421470601045300242460ustar00rootroot00000000000000# Copyright 2021-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 """ Test the port related utilities. """ import functools from typing import Any from typing import List from typing import Tuple from unittest import mock import pytest import pytestshellutils.utils.ports as ports_utils class MockedSocket: """ This class is used so that we can return the known port in the getsockname call. """ def __init__(self, port: int): self.port = port def bind(self, *args: Any, **kwargs: Any) -> None: pass def getsockname(self) -> Tuple[None, int]: return None, self.port def close(self) -> None: pass class MockedCreateSocket: """ This class just mocks the `socket.socket(...)` call so that we return the ports we want. """ def __init__(self, ports: List[int]): self.ports = list(ports) + list(ports) def __call__(self, *args: Any, **kwargs: Any) -> MockedSocket: port = self.ports.pop(0) # Return a MockedSocket instance return MockedSocket(port) def test_get_unused_localhost_port_cached() -> None: """ Tests that test_get_unused_localhost_port only returns unique ports on consecutive calls. """ num_calls = 10 start_port = 1000 # The ports we're gonna get back ports = [] for port in range(start_port, start_port + num_calls): for _ in range(num_calls): # We make sure each port is repeated consecutively ports.append(port) # Hold a reference to the list of unique ports unique = set(ports) # This list will hold all ports that the function returns got_ports = [] # We'll get the unique ports with mock.patch( "pytestshellutils.utils.socket.socket", new_callable=functools.partial(MockedCreateSocket, ports), ): for _ in range(num_calls): got_ports.append(ports_utils.get_unused_localhost_port(use_cache=True)) assert len(got_ports) == num_calls assert set(got_ports) == unique with mock.patch( "pytestshellutils.utils.socket.socket", new_callable=functools.partial(MockedCreateSocket, ports + ports), ): for _ in range(num_calls): with pytest.raises(IndexError): # we won't have enough ports got_ports.append(ports_utils.get_unused_localhost_port(use_cache=True)) # Since we couldn't get repeated ports, got_ports remains as it was assert len(got_ports) == num_calls assert set(got_ports) == unique # If we don't cache the port, we'll get repeated ports with mock.patch( "pytestshellutils.utils.socket.socket", new_callable=functools.partial(MockedCreateSocket, ports), ): for _ in range(num_calls): got_ports.append(ports_utils.get_unused_localhost_port()) assert len(got_ports) == 2 * len(unique) assert set(got_ports) == unique pytest-shell-utilities-1.9.7/tests/unit/utils/test_time.py000066400000000000000000000007561470601045300240360ustar00rootroot00000000000000# Copyright 2022-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # import time from unittest import mock import pytestshellutils.utils.time def test_sleep() -> None: start = time.time() with mock.patch("time.sleep", return_value=None): time.sleep(1) pytestshellutils.utils.time.sleep(0.1) end = time.time() duration = end - start assert duration >= 0.1 # We did sleep 0.1 second assert duration < 0.5 # But the patched time.sleep was mocked pytest-shell-utilities-1.9.7/tools/000077500000000000000000000000001470601045300173365ustar00rootroot00000000000000pytest-shell-utilities-1.9.7/tools/__init__.py000066400000000000000000000002211470601045300214420ustar00rootroot00000000000000# Copyright 2023-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 # import ptscripts ptscripts.register_tools_module("tools.pre_commit") pytest-shell-utilities-1.9.7/tools/pre_commit.py000066400000000000000000000025711470601045300220530ustar00rootroot00000000000000# Copyright 2023-2024 VMware, Inc. # SPDX-License-Identifier: Apache-2.0 """ These commands are used by pre-commit. """ # pylint: disable=resource-leakage,broad-except,3rd-party-module-not-gated from __future__ import annotations import shutil from typing import NoReturn from ptscripts import command_group from ptscripts import Context # Define the command group cgroup = command_group(name="pre-commit", help="Pre-Commit Related Commands", description=__doc__) @cgroup.command( # type: ignore[misc] name="actionlint", arguments={ "files": { "help": "Files to run actionlint against", "nargs": "*", }, "no_color": { "help": "Disable colors in output", }, }, ) def actionlint(ctx: Context, files: list[str], no_color: bool = False) -> NoReturn: """ Run `actionlint`. """ actionlint = shutil.which("actionlint") if not actionlint: ctx.warn("Could not find the 'actionlint' binary") ctx.exit(0) cmdline = [actionlint] if no_color is False: cmdline.append("-color") shellcheck = shutil.which("shellcheck") if shellcheck: cmdline.append(f"-shellcheck={shellcheck}") pyflakes = shutil.which("pyflakes") if pyflakes: cmdline.append(f"-pyflakes={pyflakes}") ret = ctx.run(*cmdline, *files, check=False) ctx.exit(ret.returncode)