pytest-salt-factories-0.907.0/0000755000175000017500000000000014106144336016737 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/.github/0000755000175000017500000000000014106144336020277 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/.github/CODEOWNERS0000644000175000017500000000065514105115272021674 0ustar vampasvampas00000000000000# pytest-salt-factories 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-salt-factories-0.907.0/.gitignore0000644000175000017500000000262214033044065020726 0ustar vampasvampas00000000000000# 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/ *.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/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # 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/ # Local Vim file .lvimrc # Ignore Nox directory .nox/ # Ignore CI salt-checkout directory salt-checkout/ # Ignore CI artifacts artifacts/ # Ignore the setuptools_scm generated version file src/saltfactories/version.py pytest-salt-factories-0.907.0/.readthedocs.yaml0000644000175000017500000000104414033044065022162 0ustar vampasvampas00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py fail_on_warning: true # Optionally build your docs in additional formats such as PDF formats: - pdf # Optionally set the version of Python and requirements required to build your docs python: version: 3.8 install: - method: pip path: . - requirements: requirements/docs.txt pytest-salt-factories-0.907.0/CHANGELOG.rst0000644000175000017500000000113014035625615020760 0ustar vampasvampas00000000000000.. _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. .. admonition:: Previous Changelog Entries :class: attention Before the 1.0.0 release, due to the fast evolving pace and breakage introduced while developing the library, no changelog was kept. Please refer to the git history for details. .. towncrier-draft-entries:: .. towncrier release notes start pytest-salt-factories-0.907.0/LICENSE0000644000175000017500000002611313536136711017753 0ustar vampasvampas00000000000000 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 2019 SaltStack 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-salt-factories-0.907.0/MANIFEST.in0000644000175000017500000000052113746561526020510 0ustar vampasvampas00000000000000include requirements/base.txt include requirements/docs.txt include requirements/lint.txt include requirements/tests.txt recursive-include saltfactories *.py exclude .coveragerc exclude .codecov.yml exclude .pre-commit-config.yaml exclude .pylint* exclude noxfile.py recursive-exclude .pre-commit-hooks *.* recursive-exclude .github *.* pytest-salt-factories-0.907.0/PKG-INFO0000644000175000017500000000741514106144336020043 0ustar vampasvampas00000000000000Metadata-Version: 2.1 Name: pytest-salt-factories Version: 0.907.0 Summary: Pytest Salt Plugin Home-page: https://github.com/saltstack/pytest-salt-factories Author: Pedro Algarvio Author-email: pedro@algarvio.me License: Apache Software License 2.0 Project-URL: Source, https://github.com/saltstack/pytest-salt-factories Project-URL: Tracker, https://github.com/saltstack/pytest-salt-factories/issues Project-URL: Documentation, https://pytest-salt-factories.readthedocs.io Description: .. image:: https://img.shields.io/github/workflow/status/saltstack/pytest-salt-factories/Testing?style=plastic :target: https://github.com/saltstack/pytest-salt-factories/actions/workflows/testing.yml :alt: CI .. image:: https://readthedocs.org/projects/pytest-salt-factories/badge/?style=plastic :target: https://pytest-salt-factories.readthedocs.io :alt: Docs .. image:: https://img.shields.io/codecov/c/github/saltstack/pytest-salt-factories?style=plastic&token=ctdrjPj4mc :target: https://codecov.io/gh/saltstack/pytest-salt-factories :alt: Codecov .. image:: https://img.shields.io/pypi/pyversions/pytest-salt-factories?style=plastic :target: https://pypi.org/project/pytest-salt-factories :alt: Python Versions .. image:: https://img.shields.io/pypi/wheel/pytest-salt-factories?style=plastic :target: https://pypi.org/project/pytest-salt-factories :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-salt-factories?style=plastic :alt: PyPI - License .. include-starts-here ====================== What is Salt Factories ====================== It's a `pytest `_ plugin, and also the evolution of `pytest-salt `_, a great start, but ... .. image:: https://math.now.sh?from=%286t%5E3%20%2B%201%29%5E3%20-%20%286t%5E3%20-%201%29%5E3%20-%20%286t%5E2%29%5E3%20%3D%202&color=DodgerBlue :alt: Complicated way to get a result of ``2`` :target: https://arxiv.org/pdf/1802.06776.pdf over-engineered. From the ground up, pytest-salt-factories was created to be ... .. image:: https://math.now.sh?from=1%20%2B%201%20%3D%202&color=DodgerBlue :alt: Simpler way to get a result of 2 simple. .. include-ends-here Documentation ============= The full documentation can be seen `here `_. Platform: unix Platform: linux Platform: osx Platform: cygwin Platform: win32 Classifier: Programming Language :: Python Classifier: Programming Language :: Cython Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Requires-Python: >=3.5 Provides-Extra: docker Provides-Extra: salt Provides-Extra: docs Provides-Extra: lint Provides-Extra: tests pytest-salt-factories-0.907.0/README.rst0000644000175000017500000000375614033044065020436 0ustar vampasvampas00000000000000.. image:: https://img.shields.io/github/workflow/status/saltstack/pytest-salt-factories/Testing?style=plastic :target: https://github.com/saltstack/pytest-salt-factories/actions/workflows/testing.yml :alt: CI .. image:: https://readthedocs.org/projects/pytest-salt-factories/badge/?style=plastic :target: https://pytest-salt-factories.readthedocs.io :alt: Docs .. image:: https://img.shields.io/codecov/c/github/saltstack/pytest-salt-factories?style=plastic&token=ctdrjPj4mc :target: https://codecov.io/gh/saltstack/pytest-salt-factories :alt: Codecov .. image:: https://img.shields.io/pypi/pyversions/pytest-salt-factories?style=plastic :target: https://pypi.org/project/pytest-salt-factories :alt: Python Versions .. image:: https://img.shields.io/pypi/wheel/pytest-salt-factories?style=plastic :target: https://pypi.org/project/pytest-salt-factories :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-salt-factories?style=plastic :alt: PyPI - License .. include-starts-here ====================== What is Salt Factories ====================== It's a `pytest `_ plugin, and also the evolution of `pytest-salt `_, a great start, but ... .. image:: https://math.now.sh?from=%286t%5E3%20%2B%201%29%5E3%20-%20%286t%5E3%20-%201%29%5E3%20-%20%286t%5E2%29%5E3%20%3D%202&color=DodgerBlue :alt: Complicated way to get a result of ``2`` :target: https://arxiv.org/pdf/1802.06776.pdf over-engineered. From the ground up, pytest-salt-factories was created to be ... .. image:: https://math.now.sh?from=1%20%2B%201%20%3D%202&color=DodgerBlue :alt: Simpler way to get a result of 2 simple. .. include-ends-here Documentation ============= The full documentation can be seen `here `_. pytest-salt-factories-0.907.0/changelog/0000755000175000017500000000000014106144336020666 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/changelog/.gitignore0000644000175000017500000000001414033044065022646 0ustar vampasvampas00000000000000!.gitignore pytest-salt-factories-0.907.0/changelog/38.feature.rst0000644000175000017500000000026514042445405023307 0ustar vampasvampas00000000000000Temporary state tree management * Add ``temp_file`` and ``temp_directory`` support as pytest helpers * Add ``SaltStateTree`` and ``SaltPillarTree`` for easier temp files support pytest-salt-factories-0.907.0/changelog/39.trivial.rst0000644000175000017500000000030614042445405023323 0ustar vampasvampas00000000000000CI pileline adjustements * Bump salt testing requirement to 3002.6 * Drop testing of FreeBSD since it's too unreliable on Github Actions * Full clone when testing so that codecov does not complain pytest-salt-factories-0.907.0/changelog/40.feature.rst0000644000175000017500000000013414042445405023273 0ustar vampasvampas00000000000000Added skip markers for AArch64 platform, ``skip_on_aarch64`` and ``skip_unless_on_aarch64`` pytest-salt-factories-0.907.0/changelog/41.improvement.rst0000644000175000017500000000005314042445405024206 0ustar vampasvampas00000000000000Switch project to an ``src/`` based layout pytest-salt-factories-0.907.0/changelog/42.improvement.rst0000644000175000017500000000013114042445405024204 0ustar vampasvampas00000000000000Start using `towncrier `_ to maintain the changelog pytest-salt-factories-0.907.0/changelog/43.feature.rst0000644000175000017500000000012614042445405023277 0ustar vampasvampas00000000000000Added a ``VirtualEnv`` helper class to create and interact with a virtual environment pytest-salt-factories-0.907.0/changelog/49.improvement.rst0000644000175000017500000000032614042445405024221 0ustar vampasvampas00000000000000Forwarding logs, file and pillar roots fixes * Salt allows minions and proxy minions to also have file and pillar roots configured * All factories will now send logs of level ``debug`` or higher to the log server pytest-salt-factories-0.907.0/changelog/50.breaking.rst0000644000175000017500000000010314042445405023417 0ustar vampasvampas00000000000000`Name things once `_. pytest-salt-factories-0.907.0/changelog/51.breaking.rst0000644000175000017500000000011014042445405023416 0ustar vampasvampas00000000000000``get_unused_localhost_port`` no longer cached returned port by default pytest-salt-factories-0.907.0/changelog/52.improvement.rst0000644000175000017500000000002514042445405024207 0ustar vampasvampas00000000000000Log the test outcome pytest-salt-factories-0.907.0/changelog/56.trivial.rst0000644000175000017500000000003014042445620023313 0ustar vampasvampas00000000000000Upgrade to black 21.4b2 pytest-salt-factories-0.907.0/changelog/57.trivial.rst0000644000175000017500000000004114042476717023331 0ustar vampasvampas00000000000000Drop Pytest requirement to 6.0.0 pytest-salt-factories-0.907.0/changelog/62.improvement.rst0000644000175000017500000000014014061702507024206 0ustar vampasvampas00000000000000Take into account that ``SystemExit.code`` might not be an integer on the generated CLI scripts pytest-salt-factories-0.907.0/changelog/63.improvement.rst0000644000175000017500000000014414061662631024216 0ustar vampasvampas00000000000000Catch unhandled exceptions and write their traceback to ``sys.stderr`` in the generated CLI scripts pytest-salt-factories-0.907.0/changelog/64.improvement.rst0000644000175000017500000000011014061664460024212 0ustar vampasvampas00000000000000Several fixes/improvements to the ``ZMQHandler`` log forwarding handler pytest-salt-factories-0.907.0/changelog/64.trivial.rst0000644000175000017500000000012614062421132023312 0ustar vampasvampas00000000000000Increase and match CI system tests `timeout-minutes` to Linux tests `timeout-minutes` pytest-salt-factories-0.907.0/changelog/69.improvement.rst0000644000175000017500000000021714062622573024227 0ustar vampasvampas00000000000000ZMQ needs to reconnect on forked processes or else Salt's own multiprocessing log forwarding log records won't be logged by the ``ZMQHandler`` pytest-salt-factories-0.907.0/changelog/70.breaking.rst0000644000175000017500000000022614062623454023433 0ustar vampasvampas00000000000000Rename the ``SaltMaster.get_salt_cli`` to ``SaltMaster.salt_cli``, forgotten on `#50 `_ pytest-salt-factories-0.907.0/changelog/72.trivial.rst0000644000175000017500000000015014062655502023320 0ustar vampasvampas00000000000000Switch to the `new codecov uploader `_ pytest-salt-factories-0.907.0/changelog/73.trivial.rst0000644000175000017500000000005514063562564023333 0ustar vampasvampas00000000000000Fix codecov flags, report name, and coverage pytest-salt-factories-0.907.0/changelog/74.improvement.rst0000644000175000017500000000015014063563021024210 0ustar vampasvampas00000000000000Some more additional changes to the ZMQHandler to make sure it's resources are cleaned when terminating pytest-salt-factories-0.907.0/changelog/77.bugfix.rst0000644000175000017500000000005314105115734023135 0ustar vampasvampas00000000000000Adjust to the upcoming salt loader changes pytest-salt-factories-0.907.0/changelog/_template.rst0000644000175000017500000000167114033044065023374 0ustar vampasvampas00000000000000{% for section in sections %} {% set underline = "-" %} {% if section %} {{section}} {{ underline * section|length }}{% set underline = "~" %} {% endif %} {% if sections[section] %} {% for category, val in definitions.items() if category in sections[section] %} {{ definitions[category]['name'] }} {{ underline * definitions[category]['name']|length }} {% if definitions[category]['showcontent'] %} {% for text, values in sections[section][category]|dictsort(by='value') %} {% set issue_joiner = joiner(', ') %} - {% for value in values|sort %}{{ issue_joiner() }}`{{ value }} `_{% endfor %}: {{ text }} {% endfor %} {% else %} - {{ sections[section][category]['']|sort|join(', ') }} {% endif %} {% if sections[section][category]|length == 0 %} No significant changes. {% else %} {% endif %} {% endfor %} {% else %} No significant changes. {% endif %} {% endfor %} pytest-salt-factories-0.907.0/docs/0000755000175000017500000000000014106144336017667 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/docs/Makefile0000644000175000017500000000117213625456621021340 0ustar vampasvampas00000000000000# 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-salt-factories-0.907.0/docs/_static/0000755000175000017500000000000014106144336021315 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/docs/_static/.gitkeep0000644000175000017500000000000013633677574022757 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/docs/_static/css/0000755000175000017500000000000014106144336022105 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/docs/_static/css/inline-include.css0000644000175000017500000000065114033044065025515 0ustar vampasvampas00000000000000.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-salt-factories-0.907.0/docs/_static/img/0000755000175000017500000000000014106144336022071 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/docs/_static/img/SaltProject_Logomark_teal.png0000644000175000017500000002416714033044065027700 0ustar vampasvampas00000000000000PNG  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-salt-factories-0.907.0/docs/_static/img/SaltProject_altlogo_teal.png0000644000175000017500000002256714033044065027570 0ustar vampasvampas00000000000000PNG  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-salt-factories-0.907.0/docs/changelog.rst0000644000175000017500000000003614033044065022344 0ustar vampasvampas00000000000000.. include:: ../CHANGELOG.rst pytest-salt-factories-0.907.0/docs/conf.py0000644000175000017500000001745314033044065021175 0ustar vampasvampas00000000000000# 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 from sphinx.directives.code import LiteralInclude try: from importlib.metadata import version as pkg_version except ImportError: from importlib_metadata import version as pkg_version import pytest try: pytest.helpers except AttributeError: from pytest_helpers_namespace.plugin import HelpersRegistry pytest.helpers = HelpersRegistry() 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 = ( DOCS_BASEPATH / "_ext", # custom Sphinx extensions REPO_ROOT / "src", # saltfactories itself (for autodoc) ) for addtl_path in addtl_paths: sys.path.insert(0, addtl_path) # -- Project information ----------------------------------------------------- this_year = datetime.datetime.today().year if this_year == 2020: copyright_year = 2020 else: copyright_year = f"2020 - {this_year}" project = "PyTest Salt Factories" copyright = f"{copyright_year}, VMware, Inc." author = "VMware, Inc." # The full version, including alpha/beta/rc tags release = pkg_version("pytest-salt-factories") # Variables to pass into the docs from sitevars.rst for rst substitution with open("sitevars.rst") 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_copybutton", # "sphinx.ext.napoleon", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinx.ext.todo", "sphinx.ext.coverage", "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 = ["saltfactories."] 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 html_theme_options = { "light_logo": "img/SaltProject_altlogo_teal.png", "dark_logo": "img/SaltProject_altlogo_teal.png", "announcement": ( "" "Documentation is always evolving, but this one hasn't reached it's prime for a 1.0.0 release. " "Once we reach that stage, this warning will go away." "" ), } # 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"] # These paths are either relative to html_static_path # or fully qualified paths (eg. https://...) html_css_files = [ "css/inline-include.css", ] # 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 = False napoleon_use_admonition_for_notes = False napoleon_use_admonition_for_references = False napoleon_use_ivar = False napoleon_use_param = True napoleon_use_rtype = True # ----- Intersphinx Config ----------------------------------------------------------------------------------------> intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "pytest": ("https://docs.pytest.org/en/stable/", None), "salt": ("https://docs.saltproject.io/en/latest", None), "psutil": ("https://psutil.readthedocs.io/en/latest/", None), "coverage": ("https://coverage.readthedocs.io/en/latest/", None), } # <---- Intersphinx Config ----------------------------------------------------------------------------------------- # ----- Autodoc Config ----------------------------------------------------------------------------------------------> autodoc_default_options = {"member-order": "bysource"} autodoc_mock_imports = ["salt"] autodoc_typehints = "description" # <---- 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 -------------------------------------------------------------------------------------- # ----- Literal Include - Auto Caption ------------------------------------------------------------------------------> class IncludeExample(LiteralInclude): """ The first argument to the directive is the file path relative to the repository root .. code-block:: rst .. include-example:: relative/path/to/example.py """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.example_file = self.arguments[0] # Get the current doc path relative to the docs directory rel_current_source = os.path.relpath(DOCS_BASEPATH, self.state.document.current_source) # Now, the passed filename, relative to the current doc, so that including it actually works self.arguments[0] = os.path.join(rel_current_source, self.example_file) def run(self): if "caption" not in self.options: self.options["caption"] = self.example_file if "name" not in self.options: self.options["name"] = self.example_file return super().run() # <---- Literal Include - Auto Caption ------------------------------------------------------------------------------- def setup(app): app.add_directive("include-example", IncludeExample) 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-salt-factories-0.907.0/docs/contents.rst0000644000175000017500000000047214033044065022256 0ustar vampasvampas00000000000000.. _table-of-contents: ================= Table Of Contents ================= .. toctree:: :maxdepth: 3 topics/install topics/usage ref/saltfactories/plugins/markers topics/fixtures ref/saltfactories/index changelog GitHub Repository pytest-salt-factories-0.907.0/docs/index.rst0000644000175000017500000000106314033044065021525 0ustar vampasvampas00000000000000: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-salt-factories/issues pytest-salt-factories-0.907.0/docs/make.bat0000644000175000017500000000137013625456621021305 0ustar vampasvampas00000000000000@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-salt-factories-0.907.0/docs/ref/0000755000175000017500000000000014106144336020443 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/docs/ref/saltfactories/0000755000175000017500000000000014106144336023306 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/docs/ref/saltfactories/bases.rst0000644000175000017500000000021714036250542025134 0ustar vampasvampas00000000000000Base Classes ============ .. automodule:: saltfactories.bases :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/cli/0000755000175000017500000000000014106144336024055 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/docs/ref/saltfactories/cli/call.rst0000644000175000017500000000024214036250542025517 0ustar vampasvampas00000000000000============= ``salt-call`` ============= .. automodule:: saltfactories.cli.call :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/cli/cloud.rst0000644000175000017500000000024614036250542025716 0ustar vampasvampas00000000000000============== ``salt-cloud`` ============== .. automodule:: saltfactories.cli.cloud :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/cli/cp.rst0000644000175000017500000000023214036250542025205 0ustar vampasvampas00000000000000=========== ``salt-cp`` =========== .. automodule:: saltfactories.cli.cp :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/cli/key.rst0000644000175000017500000000023614036250542025377 0ustar vampasvampas00000000000000============ ``salt-key`` ============ .. automodule:: saltfactories.cli.key :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/cli/run.rst0000644000175000017500000000023614036250542025413 0ustar vampasvampas00000000000000============ ``salt-run`` ============ .. automodule:: saltfactories.cli.run :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/cli/salt.rst0000644000175000017500000000022314036250542025546 0ustar vampasvampas00000000000000======== ``salt`` ======== .. automodule:: saltfactories.cli.salt :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/cli/spm.rst0000644000175000017500000000021714036250542025405 0ustar vampasvampas00000000000000======= ``spm`` ======= .. automodule:: saltfactories.cli.spm :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/cli/ssh.rst0000644000175000017500000000023614036250542025404 0ustar vampasvampas00000000000000============ ``salt-ssh`` ============ .. automodule:: saltfactories.cli.ssh :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/cli.rst0000644000175000017500000000106014033044065024601 0ustar vampasvampas00000000000000============================= ``salt-factories`` CLI Script ============================= The ``salt-factories`` CLI script is meant to be used to get an absolute path to the directory containing ``sitecustomize.py`` so that it can be injected into ``PYTHONPATH`` when running tests to track subprocesses code coverage. Example: .. code-block:: bash export PYTHONPATH="$(salt-factories --coverage);$PYTHONPATH" Please check the :ref:`coverage documentation ` on the additional requirements to track code coverage on subprocesses. pytest-salt-factories-0.907.0/docs/ref/saltfactories/client.rst0000644000175000017500000000023514036250542025315 0ustar vampasvampas00000000000000============ Local Client ============ .. automodule:: saltfactories.client :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/daemons/0000755000175000017500000000000014106144336024734 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/docs/ref/saltfactories/daemons/api.rst0000644000175000017500000000024114036250542026233 0ustar vampasvampas00000000000000============ ``salt-api`` ============ .. automodule:: saltfactories.daemons.api :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/daemons/container.rst0000644000175000017500000000024214036250542027445 0ustar vampasvampas00000000000000========== Containers ========== .. automodule:: saltfactories.daemons.container :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/daemons/master.rst0000644000175000017500000000025614036250542026763 0ustar vampasvampas00000000000000=============== ``salt-master`` =============== .. automodule:: saltfactories.daemons.master :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/daemons/minion.rst0000644000175000017500000000025614036250542026761 0ustar vampasvampas00000000000000=============== ``salt-minion`` =============== .. automodule:: saltfactories.daemons.minion :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/daemons/proxy.rst0000644000175000017500000000025214036250542026645 0ustar vampasvampas00000000000000============== ``salt-proxy`` ============== .. automodule:: saltfactories.daemons.proxy :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/daemons/sshd.rst0000644000175000017500000000022614036250542026426 0ustar vampasvampas00000000000000======== ``sshd`` ======== .. automodule:: saltfactories.daemons.sshd :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/exceptions.rst0000644000175000017500000000023314033044065026214 0ustar vampasvampas00000000000000========== Exceptions ========== .. automodule:: saltfactories.exceptions :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/index.rst0000644000175000017500000000073014036250542025146 0ustar vampasvampas00000000000000====================== Salt Factories Package ====================== .. toctree:: :maxdepth: 2 cli plugins/index utils/index exceptions Factories ========= .. toctree:: bases manager Daemons ------- .. toctree:: daemons/master daemons/minion daemons/proxy daemons/api daemons/sshd daemons/container CLI --- .. toctree:: cli/key cli/salt cli/call cli/run cli/cp cli/cloud cli/spm cli/ssh client pytest-salt-factories-0.907.0/docs/ref/saltfactories/manager.rst0000644000175000017500000000027414036250542025454 0ustar vampasvampas00000000000000====================== Salt Factories Manager ====================== .. automodule:: saltfactories.manager :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/plugins/0000755000175000017500000000000014106144336024767 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/docs/ref/saltfactories/plugins/event_listener.rst0000644000175000017500000000020514033044065030541 0ustar vampasvampas00000000000000.. automodule:: saltfactories.plugins.event_listener :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/plugins/factories.rst0000644000175000017500000000020014033044065027465 0ustar vampasvampas00000000000000.. automodule:: saltfactories.plugins.factories :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/plugins/index.rst0000644000175000017500000000020114033044065026616 0ustar vampasvampas00000000000000======= Plugins ======= .. toctree:: markers event_listener loader sysinfo sysstats log_server factories pytest-salt-factories-0.907.0/docs/ref/saltfactories/plugins/loader.rst0000644000175000017500000000017514033044065026767 0ustar vampasvampas00000000000000.. automodule:: saltfactories.plugins.loader :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/plugins/log_server.rst0000644000175000017500000000020114033044065027656 0ustar vampasvampas00000000000000.. automodule:: saltfactories.plugins.log_server :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/plugins/markers.rst0000644000175000017500000003246014033044065027167 0ustar vampasvampas00000000000000======= Markers ======= Salt factories ships with quite a few markers, skip markers. .. _markers.destructive_test: ``destructive_test`` ==================== .. py:decorator:: pytest.mark.destructive_test Skip the test if ``--run-destructive`` is not passed to pytest on the CLI. Use this mark when the test does something destructive to the system where the tests are running, for example, adding or removing a user, changing a user password. .. admonition:: Note Do not use this marker if all the test does is add/remove/change files in the test suite temporary directory .. code-block:: python @pytest.mark.destructive_test def test_func(): assert True .. _markers.expensive_test: ``expensive_test`` ================== .. py:decorator:: pytest.mark.expensive_test Skip the test if ``--run-expensive`` is not passed to pytest on the CLI. Use this test when the test does something expensive(as in monetary expensive), like creating a virtual machine on a cloud provider, etc. .. code-block:: python @pytest.mark.expensive_test def test_func(): assert True .. _markers.skip_if_not_root: ``skip_if_not_root`` ==================== .. py:decorator:: pytest.mark.skip_if_not_root Skip the test if the user running the test suite is not ``root`` or ``Administrator`` on Windows. .. code-block:: python @pytest.mark.skip_if_not_root def test_func(): assert True Look :py:func:`here ` for the full function signature. .. _markers.skip_if_binaries_missing: ``skip_if_binaries_missing`` ============================ .. py:decorator:: pytest.mark.skip_if_binaries_missing(\*binaries, check_all=True, reason=None) :param str binaries: Any argument passed must be a :py:class:`str` which is the name of the binary check for presence in the path. Multiple arguments can be passed. :keyword bool check_all: If ``check_all`` is :py:const:`True`, the default, all binaries must exist. If ``check_all`` is :py:class:`False`, then only one the passed binaries needs to be found. Useful when, for example, passing a list of python interpreter names(python3.5, python3, python), where only one needs to exist. :keyword str reason: The skip reason. Skip tests if binaries are not found in path. .. code-block:: python @pytest.mark.skip_if_binaries_missing("sshd") def test_func(): assert True @pytest.mark.skip_if_binaries_missing("python3.7", "python3", "python", check_all=False) def test_func(): assert True Look :py:func:`here ` for the full function signature. .. _markers.requires_network: ``requires_network`` ==================== .. _markers.skip_on_windows: ``skip_on_windows`` =================== .. py:decorator:: pytest.mark.skip_on_windows(reason=None) :keyword str reason: The skip reason Skip test if test suite is running on windows. .. code-block:: python @pytest.mark.skip_on_windows def test_func(): assert True .. _markers.skip_unless_on_windows: ``skip_unless_on_windows`` ========================== .. py:decorator:: pytest.mark.skip_unless_on_windows(reason=None) :keyword str reason: The skip reason Skip test unless the test suite is running on windows. .. code-block:: python @pytest.mark.skip_unless_on_windows def test_func(): assert True .. _markers.skip_on_linux: ``skip_on_linux`` ================= .. py:decorator:: pytest.mark.skip_on_linux(reason=None) :keyword str reason: The skip reason Skip test if test suite is running on linux. .. code-block:: python @pytest.mark.skip_on_linux def test_func(): assert True .. _markers.skip_unless_on_linux: ``skip_unless_on_linux`` ======================== .. py:decorator:: pytest.mark.skip_unless_on_linux(reason=None) :keyword str reason: The skip reason Skip test unless the test suite is running on linux. .. code-block:: python @pytest.mark.skip_unless_on_linux def test_func(): assert True .. _markers.skip_on_darwin: ``skip_on_darwin`` ================== .. py:decorator:: pytest.mark.skip_on_darwin(reason=None) :keyword str reason: The skip reason Skip test if test suite is running on darwin. .. code-block:: python @pytest.mark.skip_on_darwin def test_func(): assert True .. _markers.skip_unless_on_darwin: ``skip_unless_on_darwin`` ========================= .. py:decorator:: pytest.mark.skip_unless_on_darwin(reason=None) :keyword str reason: The skip reason Skip test unless the test suite is running on darwin. .. code-block:: python @pytest.mark.skip_unless_on_darwin def test_func(): assert True .. _markers.skip_on_sunos: ``skip_on_sunos`` ================= .. py:decorator:: pytest.mark.skip_on_sunos(reason=None) :keyword str reason: The skip reason Skip test if test suite is running on sunos. .. code-block:: python @pytest.mark.skip_on_sunos def test_func(): assert True .. _markers.skip_unless_on_sunos: ``skip_unless_on_sunos`` ======================== .. py:decorator:: pytest.mark.skip_unless_on_sunos(reason=None) :keyword str reason: The skip reason Skip test unless the test suite is running on sunos. .. code-block:: python @pytest.mark.skip_unless_on_sunos def test_func(): assert True .. _markers.skip_on_smartos: ``skip_on_smartos`` =================== .. py:decorator:: pytest.mark.skip_on_smartos(reason=None) :keyword str reason: The skip reason Skip test if test suite is running on smartos. .. code-block:: python @pytest.mark.skip_on_smartos def test_func(): assert True .. _markers.skip_unless_on_smartos: ``skip_unless_on_smartos`` ========================== .. py:decorator:: pytest.mark.skip_unless_on_smartos(reason=None) :keyword str reason: The skip reason Skip test unless the test suite is running on smartos. .. code-block:: python @pytest.mark.skip_unless_on_smartos def test_func(): assert True .. _markers.skip_on_freebsd: ``skip_on_freebsd`` =================== .. py:decorator:: pytest.mark.skip_on_freebsd(reason=None) :keyword str reason: The skip reason Skip test if test suite is running on freebsd. .. code-block:: python @pytest.mark.skip_on_freebsd def test_func(): assert True .. _markers.skip_unless_on_freebsd: ``skip_unless_on_freebsd`` ========================== .. py:decorator:: pytest.mark.skip_unless_on_freebsd(reason=None) :keyword str reason: The skip reason Skip test unless the test suite is running on freebsd. .. code-block:: python @pytest.mark.skip_unless_on_freebsd def test_func(): assert True .. _markers.skip_on_netbsd: ``skip_on_netbsd`` ================== .. py:decorator:: pytest.mark.skip_on_netbsd(reason=None) :keyword str reason: The skip reason Skip test if test suite is running on netbsd. .. code-block:: python @pytest.mark.skip_on_netbsd def test_func(): assert True .. _markers.skip_unless_on_netbsd: ``skip_unless_on_netbsd`` ========================= .. py:decorator:: pytest.mark.skip_unless_on_netbsd(reason=None) :keyword str reason: The skip reason Skip test unless the test suite is running on netbsd. .. code-block:: python @pytest.mark.skip_unless_on_netbsd def test_func(): assert True .. _markers.skip_on_openbsd: ``skip_on_openbsd`` =================== .. py:decorator:: pytest.mark.skip_on_openbsd(reason=None) :keyword str reason: The skip reason Skip test if test suite is running on openbsd. .. code-block:: python @pytest.mark.skip_on_openbsd def test_func(): assert True .. _markers.skip_unless_on_openbsd: ``skip_unless_on_openbsd`` ========================== .. py:decorator:: pytest.mark.skip_unless_on_openbsd(reason=None) :keyword str reason: The skip reason Skip test unless the test suite is running on openbsd. .. code-block:: python @pytest.mark.skip_unless_on_openbsd def test_func(): assert True .. _markers.skip_on_aix: ``skip_on_aix`` =============== .. py:decorator:: pytest.mark.skip_on_aix(reason=None) :keyword str reason: The skip reason Skip test if test suite is running on aix. .. code-block:: python @pytest.mark.skip_on_aix def test_func(): assert True .. _markers.skip_unless_on_aix: ``skip_unless_on_aix`` ====================== .. py:decorator:: pytest.mark.skip_unless_on_aix(reason=None) :keyword str reason: The skip reason Skip test unless the test suite is running on aix. .. code-block:: python @pytest.mark.skip_unless_on_aix def test_func(): assert True .. _markers.skip_on_aarch64: ``skip_on_aarch64`` =================== .. py:decorator:: pytest.mark.skip_on_aarch64(reason=None) :keyword str reason: The skip reason Skip test if test suite is running on aarch64. .. code-block:: python @pytest.mark.skip_on_aarch64 def test_func(): assert True .. _markers.skip_unless_on_aarch64: ``skip_unless_on_aarch64`` ========================== .. py:decorator:: pytest.mark.skip_unless_on_aarch64(reason=None) :keyword str reason: The skip reason Skip test unless the test suite is running on aarch64. .. code-block:: python @pytest.mark.skip_unless_on_aarch64 def test_func(): assert True .. _markers.skip_on_platforms: ``skip_on_platforms`` ===================== .. py:decorator:: pytest.mark.skip_on_platforms(**platforms, reason=None) :keyword bool windows: Skip on windows if :py:const:`True` :keyword bool linux: Skip on linux if :py:const:`True` :keyword bool darwin: Skip on darwin if :py:const:`True` :keyword bool sunos: Skip on sunos if :py:const:`True` :keyword bool smartos: Skip on smartos if :py:const:`True` :keyword bool freebsd: Skip on freebsd if :py:const:`True` :keyword bool netbsd: Skip on netbsd if :py:const:`True` :keyword bool openbsd: Skip on openbsd if :py:const:`True` :keyword bool aix: Skip on aix if :py:const:`True` :keyword bool aarch64: Skip on aarch64 if :py:const:`True` :keyword str reason: The skip reason Pass :py:const:`True` to any of the platforms defined as keyword arguments to skip the test when running on that platform .. code-block:: python @pytest.mark.skip_on_platforms(windows=True, darwin=True) def test_func(): assert True .. _markers.skip_unless_on_platforms: ``skip_unless_on_platforms`` ============================ .. py:decorator:: pytest.mark.skip_unless_on_platforms(**platforms, reason=None) :keyword bool windows: Skip unless on windows if :py:const:`True` :keyword bool linux: Skip unless on linux if :py:const:`True` :keyword bool darwin: Skip unless on darwin if :py:const:`True` :keyword bool sunos: Skip unless on sunos if :py:const:`True` :keyword bool smartos: Skip unless on smartos if :py:const:`True` :keyword bool freebsd: Skip unless on freebsd if :py:const:`True` :keyword bool netbsd: Skip unless on netbsd if :py:const:`True` :keyword bool openbsd: Skip unless on openbsd if :py:const:`True` :keyword bool aix: Skip unless on aix if :py:const:`True` :keyword bool aarch64: Skip on aarch64 if :py:const:`True` :keyword str reason: The skip reason Pass :py:const:`True` to any of the platforms defined as keyword arguments to skip the test when not running on that platform .. code-block:: python @pytest.mark.skip_unless_on_platforms(windows=True, darwin=True) def test_func(): assert True .. _markers.requires_salt_modules: ``requires_salt_modules`` ========================= .. py:decorator:: pytest.mark.requires_salt_modules(*modules) :param str modules: Each argument passed to the marker should be a :ref:`salt execution module ` that will need to be loaded by salt, or the test will be skipped. Allowed values are the module name, for example ``cmd``, or the module name with the function name, ``cmd.run``. .. code-block:: python @pytest.mark.requires_salt_modules("cmd", "archive.tar") def test_func(): assert True .. _markers.requires_salt_states: ``requires_salt_states`` ======================== .. py:decorator:: pytest.mark.requires_salt_states(*modules) :param str modules: Each argument passed to the marker should be a :ref:`salt state module ` that will need to be loaded by salt, or the test will be skipped. Allowed values are the state module name, for example ``pkg``, or the state module name with the function name, ``pkg.installed``. .. code-block:: python @pytest.mark.requires_salt_states("pkg", "archive.extracted") def test_func(): assert True pytest-salt-factories-0.907.0/docs/ref/saltfactories/plugins/sysinfo.rst0000644000175000017500000000017614033044065027214 0ustar vampasvampas00000000000000.. automodule:: saltfactories.plugins.sysinfo :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/plugins/sysstats.rst0000644000175000017500000000017714033044065027420 0ustar vampasvampas00000000000000.. automodule:: saltfactories.plugins.sysstats :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/0000755000175000017500000000000014106144336024446 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/cli_scripts.rst0000644000175000017500000000020014033044065027503 0ustar vampasvampas00000000000000.. automodule:: saltfactories.utils.cli_scripts :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/functional.rst0000644000175000017500000000017714033044065027344 0ustar vampasvampas00000000000000.. automodule:: saltfactories.utils.functional :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/index.rst0000644000175000017500000000026614062431421026306 0ustar vampasvampas00000000000000===== Utils ===== .. automodule:: saltfactories.utils :members: :show-inheritance: :inherited-members: :no-undoc-members: .. toctree:: :glob: * saltext/index pytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/loader.rst0000644000175000017500000000017314033044065026444 0ustar vampasvampas00000000000000.. automodule:: saltfactories.utils.loader :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/markers.rst0000644000175000017500000000017414033044065026643 0ustar vampasvampas00000000000000.. automodule:: saltfactories.utils.markers :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/platform.rst0000644000175000017500000000017514033044065027024 0ustar vampasvampas00000000000000.. automodule:: saltfactories.utils.platform :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/ports.rst0000644000175000017500000000017214033044065026344 0ustar vampasvampas00000000000000.. automodule:: saltfactories.utils.ports :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/processes.rst0000644000175000017500000000017614033044065027207 0ustar vampasvampas00000000000000.. automodule:: saltfactories.utils.processes :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/saltext/0000755000175000017500000000000014106144336026132 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/saltext/engines/0000755000175000017500000000000014106144336027562 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/saltext/engines/pytest_engine.rst0000644000175000017500000000022214062431421033160 0ustar vampasvampas00000000000000.. automodule:: saltfactories.utils.saltext.engines.pytest_engine :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/saltext/index.rst0000644000175000017500000000030414062431421027763 0ustar vampasvampas00000000000000.. automodule:: saltfactories.utils.saltext :members: :show-inheritance: :inherited-members: :no-undoc-members: .. toctree:: :maxdepth: 1 :glob: engines/* log_handlers/* pytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/saltext/log_handlers/0000755000175000017500000000000014106144336030573 5ustar vampasvampas00000000000000././@LongLink0000000000000000000000000000014700000000000011217 Lustar 00000000000000pytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/saltext/log_handlers/pytest_log_handler.rstpytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/saltext/log_handlers/pytest_log_handler.r0000644000175000017500000000023414062431421034636 0ustar vampasvampas00000000000000.. automodule:: saltfactories.utils.saltext.log_handlers.pytest_log_handler :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/ref/saltfactories/utils/tempfiles.rst0000644000175000017500000000017614033044065027171 0ustar vampasvampas00000000000000.. automodule:: saltfactories.utils.tempfiles :members: :show-inheritance: :inherited-members: :no-undoc-members: pytest-salt-factories-0.907.0/docs/sitevars.rst0000644000175000017500000000042114033044065022253 0ustar vampasvampas00000000000000.. |salt| replace:: `Salt`_ .. _salt: https://saltproject.io .. |saltrepo| replace:: `Salt repository`_ .. _Salt repository: https://github.com/saltstack/salt .. |salt-extension| replace:: `salt-extension`_ .. _salt-extension: https://www.youtube.com/watch?v=hhomJkwxK3Q pytest-salt-factories-0.907.0/docs/topics/0000755000175000017500000000000014106144336021170 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/docs/topics/fixtures.rst0000644000175000017500000000141714033044065023573 0ustar vampasvampas00000000000000Fixtures ======== .. autofunction:: saltfactories.plugins.event_listener::event_listener :noindex: .. _configure-loader-modules-fixture: ``configure_loader_modules`` ---------------------------- .. admonition:: Note The ``configure_loader_modules`` fixture is meant to be used on unit-tests, the ``pytest-salt-factories`` plugin does not define it anywhere. Instead, the user must define it on the test module. The fixture **must** return a dictionary, where the keys are the salt modules that need to be patched, and the values are dictionaries. These dictionaries should have the :py:data:`salt dunders ` as keys. These dunders are dictionaries that the salt loader injects at runtime, so, they are not available outside of Salt's runtime. pytest-salt-factories-0.907.0/docs/topics/install.rst0000644000175000017500000000201014033044065023356 0ustar vampasvampas00000000000000====================== Install Salt Factories ====================== Installing Salt Factories is as simple as: .. code-block:: bash python -m pip install pytest-salt-factories And, that's honestly it. Salt ==== Salt factories does not define |salt| as a hard requirement because that would create a chicken and egg problem while testing Salt itself. This is not a problem while testing code outside of the |saltrepo|. To install |salt| along with Salt Factories: .. code-block:: bash python -m pip install 'pytest-salt-factories[salt]' Docker ====== Salt factories also supports container factories using docker containers. To have container support enabled, install Salt Factories along with docker: .. code-block:: bash python -m pip install 'pytest-salt-factories[docker]' Multiple Optional Dependencies ============================== Installing salt-factories with multiple optional dependencies is also simple. .. code-block:: bash python -m pip install 'pytest-salt-factories[salt,docker]' pytest-salt-factories-0.907.0/docs/topics/usage.rst0000644000175000017500000000300414033044065023020 0ustar vampasvampas00000000000000==================== Using Salt Factories ==================== Salt factories simplifies testing |salt| related code outside of Salt's source tree. A great example is a |salt-extension|. Let's consider this ``echo-extension`` example. The ``echo-extension`` provides an execution module: .. include-example:: examples/echo-extension/src/echoext/modules/echo_mod.py And also a state module: .. include-example:: examples/echo-extension/src/echoext/states/echo_mod.py One could start off with something simple like unit testing the extension's code. Unit Tests ========== .. include-example:: examples/echo-extension/tests/unit/modules/test_echo.py .. include-example:: examples/echo-extension/tests/unit/states/test_echo.py The *magical* piece of code in the above example is the :ref:`configure-loader-modules-fixture` fixture. Integration Tests ================= .. include-example:: examples/echo-extension/tests/conftest.py .. include-example:: examples/echo-extension/tests/integration/conftest.py .. include-example:: examples/echo-extension/tests/integration/modules/test_echo.py .. include-example:: examples/echo-extension/tests/integration/states/test_echo.py What happened above? 1. We started a salt master 2. We started a salt minion 3. The minion connects to the master 4. The master accepted the minion's key automatically 5. We pinged the minion .. admonition:: A litle suggestion Not all tests should be integration tests, in fact, only a small set of the test suite should be an integration test. pytest-salt-factories-0.907.0/examples/0000755000175000017500000000000014106144336020555 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/0000755000175000017500000000000014106144336023505 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/LICENSE0000644000175000017500000002612014033044065024510 0ustar vampasvampas00000000000000 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 2021 Pedro Algarvio 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-salt-factories-0.907.0/examples/echo-extension/README.md0000644000175000017500000000016514033044065024763 0ustar vampasvampas00000000000000# Echo Extension This echo extension serves the purpose of showing how to test salt extensions using salt-factories pytest-salt-factories-0.907.0/examples/echo-extension/pyproject.toml0000644000175000017500000000046314033044065026421 0ustar vampasvampas00000000000000[build-system] requires = ["setuptools>=50.3.2", "wheel", "setuptools-declarative-requirements", "setuptools_scm[toml]>=3.4"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "src/echoext/version.py" write_to_template = "__version__ = \"{version}\"" [tool.black] line-length = 100 pytest-salt-factories-0.907.0/examples/echo-extension/requirements/0000755000175000017500000000000014106144336026230 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/requirements/base.txt0000644000175000017500000000001514033044065027674 0ustar vampasvampas00000000000000salt>=3002.6 pytest-salt-factories-0.907.0/examples/echo-extension/requirements/tests.txt0000644000175000017500000000006114033044065030125 0ustar vampasvampas00000000000000pytest >= 6.1.0 pytest-salt-factories >= 0.130.0 pytest-salt-factories-0.907.0/examples/echo-extension/setup.cfg0000644000175000017500000000317614033044065025332 0ustar vampasvampas00000000000000[metadata] name = echoext description = Echo Extension long_description = file: README.md long_description_content_type = text/markdown author = Pedro Algarvio author_email = pedro@algarvio.me keywords = salt-extension url = https://github.com/saltstack/pytest-salt-factories project_urls = Source=https://github.com/saltstack/pytest-salt-factories Tracker=https://github.com/saltstack/pytest-salt-factories/issues license = Apache Software License classifiers = Programming Language :: Python Programming Language :: Cython Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Apache Software License platforms = any [options] zip_safe = False include_package_data = True package_dir = =src packages = find_namespace: python_requires = >= 3.5 setup_requires = wheel setuptools>=50.3.2 setuptools_scm[toml]>=3.4 setuptools-declarative-requirements [options.packages.find] where = src exclude = tests [options.entry_points] salt.loader= module_dirs = echoext.loader:get_module_dirs states_dirs = echoext.loader:get_states_dirs [requirements-files] install_requires = requirements/base.txt extras_require = tests = requirements/tests.txt [bdist_wheel] # Use this option if your package is pure-python universal = 1 [build_sphinx] source_dir = docs build_dir = build/sphinx pytest-salt-factories-0.907.0/examples/echo-extension/setup.py0000644000175000017500000000020414033044065025210 0ustar vampasvampas00000000000000# pylint: disable=missing-module-docstring import setuptools if __name__ == "__main__": setuptools.setup(use_scm_version=True) pytest-salt-factories-0.907.0/examples/echo-extension/src/0000755000175000017500000000000014106144336024274 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/src/echoext/0000755000175000017500000000000014106144336025733 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/src/echoext/__init__.py0000644000175000017500000000153714033044065030047 0ustar vampasvampas00000000000000# pylint: disable=missing-module-docstring import pathlib PACKAGE_ROOT = pathlib.Path(__file__).resolve().parent 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(__name__) except PackageNotFoundError: # package is not installed pass except ImportError: try: from pkg_resources import get_distribution, DistributionNotFound try: __version__ = get_distribution(__name__).version except DistributionNotFound: # package is not installed pass except ImportError: # pkg resources isn't even available?! pass pytest-salt-factories-0.907.0/examples/echo-extension/src/echoext/loader.py0000644000175000017500000000072614033044065027555 0ustar vampasvampas00000000000000""" Define the required entry-points functions in order for Salt to know what and from where it should load this extension's loaders """ from . import PACKAGE_ROOT def get_module_dirs(): """ Return a list of paths from where salt should load module modules """ return [str(PACKAGE_ROOT / "modules")] def get_states_dirs(): """ Return a list of paths from where salt should load state modules """ return [str(PACKAGE_ROOT / "states")] pytest-salt-factories-0.907.0/examples/echo-extension/src/echoext/modules/0000755000175000017500000000000014106144336027403 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/src/echoext/modules/__init__.py0000644000175000017500000000000014033044065031477 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/src/echoext/modules/echo_mod.py0000644000175000017500000000104714033044065031531 0ustar vampasvampas00000000000000__virtualname__ = "echo" def __virtual__(): return __virtualname__ def text(string): """ This function just returns any text that it's given. CLI Example: .. code-block:: bash salt '*' echo.text 'foo bar baz quo qux' """ return __salt__["test.echo"](string) def reverse(string): """ This function just returns any text that it's given, reversed. CLI Example: .. code-block:: bash salt '*' echo.reverse 'foo bar baz quo qux' """ return __salt__["test.echo"](string)[::-1] pytest-salt-factories-0.907.0/examples/echo-extension/src/echoext/states/0000755000175000017500000000000014106144336027236 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/src/echoext/states/__init__.py0000644000175000017500000000000014033044065031332 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/src/echoext/states/echo_mod.py0000644000175000017500000000143414033044065031364 0ustar vampasvampas00000000000000__virtualname__ = "echo" def __virtual__(): if "echo.text" not in __salt__: return False, "The 'echo' execution module is not available" return __virtualname__ def echoed(name): ret = {"name": name, "changes": {}, "result": False, "comment": ""} value = __salt__["echo.text"](name) if value == name: ret["result"] = True ret["comment"] = "The 'echo.echoed' returned: '{}'".format(value) return ret def reversed(name): """ This example function should be replaced """ ret = {"name": name, "changes": {}, "result": False, "comment": ""} value = __salt__["echo.reverse"](name) if value == name[::-1]: ret["result"] = True ret["comment"] = "The 'echo.reversed' returned: '{}'".format(value) return ret pytest-salt-factories-0.907.0/examples/echo-extension/tests/0000755000175000017500000000000014106144336024647 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/tests/__init__.py0000644000175000017500000000000014033044065026743 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/tests/conftest.py0000644000175000017500000000140014036250542027040 0ustar vampasvampas00000000000000import os import pytest from echoext import PACKAGE_ROOT from saltfactories.utils import random_string @pytest.fixture(scope="session") def salt_factories_config(): """ Return a dictionary with the keyword arguments for FactoriesManager """ return { "code_dir": str(PACKAGE_ROOT), "inject_coverage": "COVERAGE_PROCESS_START" in os.environ, "inject_sitecustomize": "COVERAGE_PROCESS_START" in os.environ, "start_timeout": 120 if os.environ.get("CI") else 60, } @pytest.fixture(scope="package") def master(salt_factories): return salt_factories.salt_master_daemon(random_string("master-")) @pytest.fixture(scope="package") def minion(master): return master.salt_minion_daemon(random_string("minion-")) pytest-salt-factories-0.907.0/examples/echo-extension/tests/integration/0000755000175000017500000000000014106144336027172 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/tests/integration/__init__.py0000644000175000017500000000000014033044065031266 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/tests/integration/conftest.py0000644000175000017500000000067514062623454031405 0ustar vampasvampas00000000000000import pytest @pytest.fixture(scope="package") def master(master): with master.started(): yield master @pytest.fixture(scope="package") def minion(minion): with minion.started(): yield minion @pytest.fixture def salt_run_cli(master): return master.salt_run_cli() @pytest.fixture def salt_cli(master): return master.salt_cli() @pytest.fixture def salt_call_cli(minion): return minion.salt_call_cli() pytest-salt-factories-0.907.0/examples/echo-extension/tests/integration/modules/0000755000175000017500000000000014106144336030642 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/tests/integration/modules/__init__.py0000644000175000017500000000000014033044065032736 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/tests/integration/modules/test_echo.py0000644000175000017500000000076214033044065033173 0ustar vampasvampas00000000000000import pytest pytestmark = [ pytest.mark.requires_salt_modules("echo.text"), ] def test_text(salt_call_cli): echo_str = "Echoed!" ret = salt_call_cli.run("echo.text", echo_str) assert ret.exitcode == 0 assert ret.json assert ret.json == echo_str def test_reverse(salt_call_cli): echo_str = "Echoed!" expected = echo_str[::-1] ret = salt_call_cli.run("echo.reverse", echo_str) assert ret.exitcode == 0 assert ret.json assert ret.json == expected pytest-salt-factories-0.907.0/examples/echo-extension/tests/integration/states/0000755000175000017500000000000014106144336030475 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/tests/integration/states/__init__.py0000644000175000017500000000000014033044065032571 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/tests/integration/states/test_echo.py0000644000175000017500000000102714033044065033021 0ustar vampasvampas00000000000000import pytest pytestmark = [ pytest.mark.requires_salt_states("echo.text"), ] def test_echoed(salt_call_cli): echo_str = "Echoed!" ret = salt_call_cli.run("state.single", "echo.echoed", echo_str) assert ret.exitcode == 0 assert ret.json assert ret.json == echo_str def test_reversed(salt_call_cli): echo_str = "Echoed!" expected = echo_str[::-1] ret = salt_call_cli.run("state.single", "echo.reversed", echo_str) assert ret.exitcode == 0 assert ret.json assert ret.json == expected pytest-salt-factories-0.907.0/examples/echo-extension/tests/unit/0000755000175000017500000000000014106144336025626 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/tests/unit/__init__.py0000644000175000017500000000000014033044065027722 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/tests/unit/modules/0000755000175000017500000000000014106144336027276 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/tests/unit/modules/__init__.py0000644000175000017500000000000014033044065031372 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/tests/unit/modules/test_echo.py0000644000175000017500000000077214033044065031630 0ustar vampasvampas00000000000000import pytest import salt.modules.test as testmod import echoext.modules.echo_mod as echo_module @pytest.fixture def configure_loader_modules(): module_globals = { "__salt__": {"test.echo": testmod.echo}, } return { echo_module: module_globals, } def test_text(): echo_str = "Echoed!" assert echo_module.text(echo_str) == echo_str def test_reverse(): echo_str = "Echoed!" expected = echo_str[::-1] assert echo_module.reverse(echo_str) == expected pytest-salt-factories-0.907.0/examples/echo-extension/tests/unit/states/0000755000175000017500000000000014106144336027131 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/tests/unit/states/__init__.py0000644000175000017500000000000014033044065031225 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/examples/echo-extension/tests/unit/states/test_echo.py0000644000175000017500000000206614033044065031461 0ustar vampasvampas00000000000000import pytest import salt.modules.test as testmod import echoext.modules.echo_mod as echo_module import echoext.states.echo_mod as echo_state @pytest.fixture def configure_loader_modules(): return { echo_module: { "__salt__": { "test.echo": testmod.echo, }, }, echo_state: { "__salt__": { "echo.text": echo_module.text, "echo.reverse": echo_module.reverse, }, }, } def test_echoed(): echo_str = "Echoed!" expected = { "name": echo_str, "changes": {}, "result": True, "comment": "The 'echo.echoed' returned: '{}'".format(echo_str), } assert echo_state.echoed(echo_str) == expected def test_reversed(): echo_str = "Echoed!" expected_str = echo_str[::-1] expected = { "name": echo_str, "changes": {}, "result": True, "comment": "The 'echo.reversed' returned: '{}'".format(expected_str), } assert echo_state.reversed(echo_str) == expected pytest-salt-factories-0.907.0/pyproject.toml0000644000175000017500000000225114035547045021660 0ustar vampasvampas00000000000000[build-system] requires = ["setuptools>=50.3.2", "wheel", "setuptools-declarative-requirements", "setuptools_scm[toml]>=3.4"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "src/saltfactories/version.py" write_to_template = "# pylint: skip-file\n\n__version__ = \"{version}\"\n" [tool.towncrier] package = "saltfactories" filename = "CHANGELOG.rst" directory = "changelog/" title_format = "salt-factories {version} ({project_date})" template = "changelog/_template.rst" [[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-salt-factories-0.907.0/pytest.ini0000644000175000017500000000053413720712732020774 0ustar vampasvampas00000000000000[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 testpaths=tests/ pytest-salt-factories-0.907.0/requirements/0000755000175000017500000000000014106144336021462 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/requirements/base.txt0000644000175000017500000000017214044531362023135 0ustar vampasvampas00000000000000pytest>=6.0.0 attrs>=19.2.0 pytest-tempdir>=2019.9.16 pytest-helpers-namespace>=2021.4.29 psutil pyzmq msgpack virtualenv pytest-salt-factories-0.907.0/requirements/changelog.txt0000644000175000017500000000001214033044065024140 0ustar vampasvampas00000000000000towncrier pytest-salt-factories-0.907.0/requirements/docs.txt0000644000175000017500000000021114035616767023162 0ustar vampasvampas00000000000000-r base.txt -r tests.txt furo sphinx sphinx-copybutton sphinx-prompt sphinxcontrib-spelling sphinxcontrib-towncrier towncrier >= 0.2.0a0 pytest-salt-factories-0.907.0/requirements/lint.txt0000644000175000017500000000022614035547046023177 0ustar vampasvampas00000000000000-r base.txt -r tests.txt pylint==2.7.4 saltpylint==2020.9.28 pyenchant black; python_version >= '3.7' reorder-python-imports; python_version >= '3.7' pytest-salt-factories-0.907.0/requirements/tests.txt0000644000175000017500000000004314062427562023370 0ustar vampasvampas00000000000000-r base.txt docker pytest-subtests pytest-salt-factories-0.907.0/setup.cfg0000644000175000017500000000412014106144336020555 0ustar vampasvampas00000000000000[metadata] name = pytest-salt-factories description = Pytest Salt Plugin long_description = file: README.rst author = Pedro Algarvio author_email = pedro@algarvio.me url = https://github.com/saltstack/pytest-salt-factories project_urls = Source=https://github.com/saltstack/pytest-salt-factories Tracker=https://github.com/saltstack/pytest-salt-factories/issues Documentation=https://pytest-salt-factories.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.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Development Status :: 4 - Beta 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.5 setup_requires = setuptools>=50.3.2 setuptools_scm[toml]>=3.4 setuptools-declarative-requirements [options.packages.find] where = src exclude = tests* [options.extras_require] docker = docker salt = salt>=3001 [requirements-files] install_requires = requirements/base.txt extras_require = docs = requirements/docs.txt lint = requirements/lint.txt tests = requirements/tests.txt [options.entry_points] console_scripts = salt-factories = saltfactories.__main__:main pytest11 = salt-factories = saltfactories.plugins salt-factories-factories = saltfactories.plugins.factories salt-factories-markers = saltfactories.plugins.markers salt-factories-sysinfo = saltfactories.plugins.sysinfo salt-factories-sysstats = saltfactories.plugins.sysstats salt-factories-event-listener = saltfactories.plugins.event_listener salt-factories-log-server = saltfactories.plugins.log_server salt-factories-loader-mock = saltfactories.plugins.loader [bdist_wheel] universal = false [egg_info] tag_build = tag_date = 0 pytest-salt-factories-0.907.0/setup.py0000644000175000017500000000015713746333313020460 0ustar vampasvampas00000000000000#!/usr/bin/env python import setuptools if __name__ == "__main__": setuptools.setup(use_scm_version=True) pytest-salt-factories-0.907.0/src/0000755000175000017500000000000014106144336017526 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/src/pytest_salt_factories.egg-info/0000755000175000017500000000000014106144336025632 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/src/pytest_salt_factories.egg-info/PKG-INFO0000644000175000017500000000741514106144335026735 0ustar vampasvampas00000000000000Metadata-Version: 2.1 Name: pytest-salt-factories Version: 0.907.0 Summary: Pytest Salt Plugin Home-page: https://github.com/saltstack/pytest-salt-factories Author: Pedro Algarvio Author-email: pedro@algarvio.me License: Apache Software License 2.0 Project-URL: Source, https://github.com/saltstack/pytest-salt-factories Project-URL: Tracker, https://github.com/saltstack/pytest-salt-factories/issues Project-URL: Documentation, https://pytest-salt-factories.readthedocs.io Description: .. image:: https://img.shields.io/github/workflow/status/saltstack/pytest-salt-factories/Testing?style=plastic :target: https://github.com/saltstack/pytest-salt-factories/actions/workflows/testing.yml :alt: CI .. image:: https://readthedocs.org/projects/pytest-salt-factories/badge/?style=plastic :target: https://pytest-salt-factories.readthedocs.io :alt: Docs .. image:: https://img.shields.io/codecov/c/github/saltstack/pytest-salt-factories?style=plastic&token=ctdrjPj4mc :target: https://codecov.io/gh/saltstack/pytest-salt-factories :alt: Codecov .. image:: https://img.shields.io/pypi/pyversions/pytest-salt-factories?style=plastic :target: https://pypi.org/project/pytest-salt-factories :alt: Python Versions .. image:: https://img.shields.io/pypi/wheel/pytest-salt-factories?style=plastic :target: https://pypi.org/project/pytest-salt-factories :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-salt-factories?style=plastic :alt: PyPI - License .. include-starts-here ====================== What is Salt Factories ====================== It's a `pytest `_ plugin, and also the evolution of `pytest-salt `_, a great start, but ... .. image:: https://math.now.sh?from=%286t%5E3%20%2B%201%29%5E3%20-%20%286t%5E3%20-%201%29%5E3%20-%20%286t%5E2%29%5E3%20%3D%202&color=DodgerBlue :alt: Complicated way to get a result of ``2`` :target: https://arxiv.org/pdf/1802.06776.pdf over-engineered. From the ground up, pytest-salt-factories was created to be ... .. image:: https://math.now.sh?from=1%20%2B%201%20%3D%202&color=DodgerBlue :alt: Simpler way to get a result of 2 simple. .. include-ends-here Documentation ============= The full documentation can be seen `here `_. Platform: unix Platform: linux Platform: osx Platform: cygwin Platform: win32 Classifier: Programming Language :: Python Classifier: Programming Language :: Cython Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Requires-Python: >=3.5 Provides-Extra: docker Provides-Extra: salt Provides-Extra: docs Provides-Extra: lint Provides-Extra: tests pytest-salt-factories-0.907.0/src/pytest_salt_factories.egg-info/SOURCES.txt0000644000175000017500000002764214106144336027531 0ustar vampasvampas00000000000000.gitignore .readthedocs.yaml CHANGELOG.rst LICENSE MANIFEST.in README.rst pyproject.toml pytest.ini setup.cfg setup.py .github/CODEOWNERS changelog/.gitignore changelog/38.feature.rst changelog/39.trivial.rst changelog/40.feature.rst changelog/41.improvement.rst changelog/42.improvement.rst changelog/43.feature.rst changelog/49.improvement.rst changelog/50.breaking.rst changelog/51.breaking.rst changelog/52.improvement.rst changelog/56.trivial.rst changelog/57.trivial.rst changelog/62.improvement.rst changelog/63.improvement.rst changelog/64.improvement.rst changelog/64.trivial.rst changelog/69.improvement.rst changelog/70.breaking.rst changelog/72.trivial.rst changelog/73.trivial.rst changelog/74.improvement.rst changelog/77.bugfix.rst changelog/_template.rst docs/Makefile docs/changelog.rst docs/conf.py docs/contents.rst docs/index.rst docs/make.bat docs/sitevars.rst docs/_static/.gitkeep docs/_static/css/inline-include.css docs/_static/img/SaltProject_Logomark_teal.png docs/_static/img/SaltProject_altlogo_teal.png docs/ref/saltfactories/bases.rst docs/ref/saltfactories/cli.rst docs/ref/saltfactories/client.rst docs/ref/saltfactories/exceptions.rst docs/ref/saltfactories/index.rst docs/ref/saltfactories/manager.rst docs/ref/saltfactories/cli/call.rst docs/ref/saltfactories/cli/cloud.rst docs/ref/saltfactories/cli/cp.rst docs/ref/saltfactories/cli/key.rst docs/ref/saltfactories/cli/run.rst docs/ref/saltfactories/cli/salt.rst docs/ref/saltfactories/cli/spm.rst docs/ref/saltfactories/cli/ssh.rst docs/ref/saltfactories/daemons/api.rst docs/ref/saltfactories/daemons/container.rst docs/ref/saltfactories/daemons/master.rst docs/ref/saltfactories/daemons/minion.rst docs/ref/saltfactories/daemons/proxy.rst docs/ref/saltfactories/daemons/sshd.rst docs/ref/saltfactories/plugins/event_listener.rst docs/ref/saltfactories/plugins/factories.rst docs/ref/saltfactories/plugins/index.rst docs/ref/saltfactories/plugins/loader.rst docs/ref/saltfactories/plugins/log_server.rst docs/ref/saltfactories/plugins/markers.rst docs/ref/saltfactories/plugins/sysinfo.rst docs/ref/saltfactories/plugins/sysstats.rst docs/ref/saltfactories/utils/cli_scripts.rst docs/ref/saltfactories/utils/functional.rst docs/ref/saltfactories/utils/index.rst docs/ref/saltfactories/utils/loader.rst docs/ref/saltfactories/utils/markers.rst docs/ref/saltfactories/utils/platform.rst docs/ref/saltfactories/utils/ports.rst docs/ref/saltfactories/utils/processes.rst docs/ref/saltfactories/utils/tempfiles.rst docs/ref/saltfactories/utils/saltext/index.rst docs/ref/saltfactories/utils/saltext/engines/pytest_engine.rst docs/ref/saltfactories/utils/saltext/log_handlers/pytest_log_handler.rst docs/topics/fixtures.rst docs/topics/install.rst docs/topics/usage.rst examples/echo-extension/LICENSE examples/echo-extension/README.md examples/echo-extension/pyproject.toml examples/echo-extension/setup.cfg examples/echo-extension/setup.py examples/echo-extension/requirements/base.txt examples/echo-extension/requirements/tests.txt examples/echo-extension/src/echoext/__init__.py examples/echo-extension/src/echoext/loader.py examples/echo-extension/src/echoext/modules/__init__.py examples/echo-extension/src/echoext/modules/echo_mod.py examples/echo-extension/src/echoext/states/__init__.py examples/echo-extension/src/echoext/states/echo_mod.py examples/echo-extension/tests/__init__.py examples/echo-extension/tests/conftest.py examples/echo-extension/tests/integration/__init__.py examples/echo-extension/tests/integration/conftest.py examples/echo-extension/tests/integration/modules/__init__.py examples/echo-extension/tests/integration/modules/test_echo.py examples/echo-extension/tests/integration/states/__init__.py examples/echo-extension/tests/integration/states/test_echo.py examples/echo-extension/tests/unit/__init__.py examples/echo-extension/tests/unit/modules/__init__.py examples/echo-extension/tests/unit/modules/test_echo.py examples/echo-extension/tests/unit/states/__init__.py examples/echo-extension/tests/unit/states/test_echo.py requirements/base.txt requirements/changelog.txt requirements/docs.txt requirements/lint.txt requirements/tests.txt src/pytest_salt_factories.egg-info/PKG-INFO src/pytest_salt_factories.egg-info/SOURCES.txt src/pytest_salt_factories.egg-info/dependency_links.txt src/pytest_salt_factories.egg-info/entry_points.txt src/pytest_salt_factories.egg-info/not-zip-safe src/pytest_salt_factories.egg-info/requires.txt src/pytest_salt_factories.egg-info/top_level.txt src/saltfactories/__init__.py src/saltfactories/__main__.py src/saltfactories/bases.py src/saltfactories/client.py src/saltfactories/exceptions.py src/saltfactories/manager.py src/saltfactories/version.py src/saltfactories/cli/__init__.py src/saltfactories/cli/call.py src/saltfactories/cli/cloud.py src/saltfactories/cli/cp.py src/saltfactories/cli/key.py src/saltfactories/cli/run.py src/saltfactories/cli/salt.py src/saltfactories/cli/spm.py src/saltfactories/cli/ssh.py src/saltfactories/daemons/__init__.py src/saltfactories/daemons/api.py src/saltfactories/daemons/container.py src/saltfactories/daemons/master.py src/saltfactories/daemons/minion.py src/saltfactories/daemons/proxy.py src/saltfactories/daemons/sshd.py src/saltfactories/daemons/syndic.py src/saltfactories/plugins/__init__.py src/saltfactories/plugins/event_listener.py src/saltfactories/plugins/factories.py src/saltfactories/plugins/loader.py src/saltfactories/plugins/log_server.py src/saltfactories/plugins/markers.py src/saltfactories/plugins/sysinfo.py src/saltfactories/plugins/sysstats.py src/saltfactories/utils/__init__.py src/saltfactories/utils/cli_scripts.py src/saltfactories/utils/functional.py src/saltfactories/utils/loader.py src/saltfactories/utils/markers.py src/saltfactories/utils/platform.py src/saltfactories/utils/ports.py src/saltfactories/utils/processes.py src/saltfactories/utils/socket.py src/saltfactories/utils/tempfiles.py src/saltfactories/utils/time.py src/saltfactories/utils/virtualenv.py src/saltfactories/utils/coverage/sitecustomize.py src/saltfactories/utils/saltext/__init__.py src/saltfactories/utils/saltext/engines/pytest_engine.py src/saltfactories/utils/saltext/log_handlers/__init__.py src/saltfactories/utils/saltext/log_handlers/pytest_log_handler.py tests/__init__.py tests/conftest.py tests/functional/__init__.py tests/functional/test_cli.py tests/functional/test_sys_info.py tests/functional/test_sys_stats.py tests/functional/factories/__init__.py tests/functional/factories/base/__init__.py tests/functional/factories/base/test_daemon_factory.py tests/functional/factories/base/test_process_factory.py tests/functional/factories/base/test_salt_daemon_factory.py tests/functional/factories/cli/__init__.py tests/functional/factories/cli/conftest.py tests/functional/factories/cli/test_call.py tests/functional/factories/cli/test_cloud.py tests/functional/factories/cli/test_cp.py tests/functional/factories/cli/test_key.py tests/functional/factories/cli/test_run.py tests/functional/factories/cli/test_salt.py tests/functional/factories/cli/test_spm.py tests/functional/factories/cli/test_ssh.py tests/functional/factories/daemons/__init__.py tests/functional/factories/daemons/test_master_factory.py tests/functional/factories/daemons/test_minion_factory.py tests/functional/factories/daemons/test_proxy_minion_factory.py tests/functional/factories/daemons/test_syndic_factory.py tests/functional/loader/__init__.py tests/functional/loader/test_fixture_deps.py tests/functional/loader/test_loader.py tests/functional/markers/__init__.py tests/functional/markers/test_destructive_test.py tests/functional/markers/test_expensive_test.py tests/functional/markers/test_requires_network.py tests/functional/markers/test_requires_salt_modules.py tests/functional/markers/test_requires_salt_states.py tests/functional/markers/test_skip_if_binaries_missing.py tests/functional/markers/test_skip_if_not_root.py tests/functional/markers/test_skip_on_aarch64.py tests/functional/markers/test_skip_on_aix.py tests/functional/markers/test_skip_on_darwin.py tests/functional/markers/test_skip_on_freebsd.py tests/functional/markers/test_skip_on_linux.py tests/functional/markers/test_skip_on_netbsd.py tests/functional/markers/test_skip_on_openbsd.py tests/functional/markers/test_skip_on_platforms.py tests/functional/markers/test_skip_on_smartos.py tests/functional/markers/test_skip_on_sunos.py tests/functional/markers/test_skip_on_windows.py tests/functional/markers/test_skip_unless_on_aarch64.py tests/functional/markers/test_skip_unless_on_aix.py tests/functional/markers/test_skip_unless_on_darwin.py tests/functional/markers/test_skip_unless_on_freebsd.py tests/functional/markers/test_skip_unless_on_linux.py tests/functional/markers/test_skip_unless_on_netbsd.py tests/functional/markers/test_skip_unless_on_openbsd.py tests/functional/markers/test_skip_unless_on_platforms.py tests/functional/markers/test_skip_unless_on_smartos.py tests/functional/markers/test_skip_unless_on_sunos.py tests/functional/markers/test_skip_unless_on_windows.py tests/functional/utils/__init__.py tests/functional/utils/test_tempfiles.py tests/functional/utils/processes/__init__.py tests/functional/utils/processes/bases/__init__.py tests/functional/utils/saltext/__init__.py tests/functional/utils/saltext/test_log_handlers.py tests/integration/__init__.py tests/integration/factories/__init__.py tests/integration/factories/cli/__init__.py tests/integration/factories/cli/conftest.py tests/integration/factories/cli/test_salt.py tests/integration/factories/daemons/__init__.py tests/integration/factories/daemons/api/__init__.py tests/integration/factories/daemons/api/test_api.py tests/integration/factories/daemons/api/test_restarts.py tests/integration/factories/daemons/container/__init__.py tests/integration/factories/daemons/container/test_container_factory.py tests/integration/factories/daemons/master/__init__.py tests/integration/factories/daemons/master/test_master.py tests/integration/factories/daemons/master/test_restarts.py tests/integration/factories/daemons/minion/__init__.py tests/integration/factories/daemons/minion/test_event_forwarder_engine.py tests/integration/factories/daemons/minion/test_minion.py tests/integration/factories/daemons/minion/test_restarts.py tests/integration/factories/daemons/proxy/__init__.py tests/integration/factories/daemons/proxy/test_proxy_minion.py tests/integration/factories/daemons/proxy/test_restarts.py tests/integration/factories/daemons/ssh/__init__.py tests/integration/factories/daemons/ssh/test_salt_ssh.py tests/integration/factories/daemons/sshd/__init__.py tests/integration/factories/daemons/sshd/test_sshd.py tests/integration/factories/daemons/syndic/__init__.py tests/integration/factories/daemons/syndic/test_syndic.py tests/integration/utils/__init__.py tests/integration/utils/saltext/__init__.py tests/integration/utils/saltext/test_log_handlers.py tests/scenarios/__init__.py tests/scenarios/examples/__init__.py tests/scenarios/examples/conftest.py tests/scenarios/examples/test_echoext.py tests/unit/__init__.py tests/unit/test_exceptions.py tests/unit/factories/__init__.py tests/unit/factories/base/__init__.py tests/unit/factories/base/test_salt_cli_factory.py tests/unit/factories/base/test_salt_daemon_factory.py tests/unit/factories/cli/__init__.py tests/unit/factories/cli/test_salt.py tests/unit/factories/daemons/__init__.py tests/unit/factories/daemons/test_api.py tests/unit/factories/daemons/test_container.py tests/unit/utils/__init__.py tests/unit/utils/test_cli_scripts.py tests/unit/utils/test_platforms.py tests/unit/utils/test_ports.py tests/unit/utils/test_random_string.py tests/unit/utils/test_time.py tests/unit/utils/markers/__init__.py tests/unit/utils/markers/test_skip_if_binaries_missing.py tests/unit/utils/markers/test_skip_if_no_local_network.py tests/unit/utils/markers/test_skip_if_no_remote_network.py tests/unit/utils/markers/test_skip_if_not_root.py tests/unit/utils/processes/__init__.py tests/unit/utils/processes/test_processresult.py tests/unit/utils/processes/test_shellresult.py tests/unit/utils/saltext/__init__.py tests/unit/utils/saltext/test_log_handlers.pypytest-salt-factories-0.907.0/src/pytest_salt_factories.egg-info/dependency_links.txt0000644000175000017500000000000114106144335031677 0ustar vampasvampas00000000000000 pytest-salt-factories-0.907.0/src/pytest_salt_factories.egg-info/entry_points.txt0000644000175000017500000000102114106144335031121 0ustar vampasvampas00000000000000[console_scripts] salt-factories = saltfactories.__main__:main [pytest11] salt-factories = saltfactories.plugins salt-factories-event-listener = saltfactories.plugins.event_listener salt-factories-factories = saltfactories.plugins.factories salt-factories-loader-mock = saltfactories.plugins.loader salt-factories-log-server = saltfactories.plugins.log_server salt-factories-markers = saltfactories.plugins.markers salt-factories-sysinfo = saltfactories.plugins.sysinfo salt-factories-sysstats = saltfactories.plugins.sysstats pytest-salt-factories-0.907.0/src/pytest_salt_factories.egg-info/not-zip-safe0000644000175000017500000000000114062411254030055 0ustar vampasvampas00000000000000 pytest-salt-factories-0.907.0/src/pytest_salt_factories.egg-info/requires.txt0000644000175000017500000000064714106144335030240 0ustar vampasvampas00000000000000pytest>=6.0.0 attrs>=19.2.0 pytest-tempdir>=2019.9.16 pytest-helpers-namespace>=2021.4.29 psutil pyzmq msgpack virtualenv [docker] docker [docs] furo sphinx sphinx-copybutton sphinx-prompt sphinxcontrib-spelling sphinxcontrib-towncrier towncrier>=0.2.0a0 [lint] pylint==2.7.4 saltpylint==2020.9.28 pyenchant [lint:python_version >= "3.7"] black reorder-python-imports [salt] salt>=3001 [tests] docker pytest-subtests pytest-salt-factories-0.907.0/src/pytest_salt_factories.egg-info/top_level.txt0000644000175000017500000000001614106144335030360 0ustar vampasvampas00000000000000saltfactories pytest-salt-factories-0.907.0/src/saltfactories/0000755000175000017500000000000014106144336022371 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/src/saltfactories/__init__.py0000644000175000017500000000341114033044065024476 0ustar vampasvampas00000000000000import 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-salt-factories") except PackageNotFoundError: # package is not installed pass except ImportError: try: from importlib_metadata import version, PackageNotFoundError try: __version__ = version("pytest-salt-factories") except PackageNotFoundError: # package is not installed pass except ImportError: try: from pkg_resources import get_distribution, DistributionNotFound try: __version__ = get_distribution("pytest-salt-factories").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: __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-salt-factories-0.907.0/src/saltfactories/__main__.py0000644000175000017500000000152714036250542024467 0ustar vampasvampas00000000000000""" The ``salt-factories`` CLI script is meant to be used to get an absolute path to the directory containing ``sitecustomize.py`` so that it can be injected into ``PYTHONPATH`` when running tests to track subprocesses code coverage. """ import argparse import sys import saltfactories def main(): parser = argparse.ArgumentParser(description="PyTest Salt Factories") parser.add_argument( "--coverage", action="store_true", help="Prints the path to where the sitecustomize.py is to trigger coverage tracking on sub-processes.", ) options = parser.parse_args() if options.coverage: print(str(saltfactories.CODE_ROOT_DIR / "utils" / "coverage"), file=sys.stdout, flush=True) parser.exit(status=0) parser.exit(status=1, message=parser.format_usage()) if __name__ == "__main__": main() pytest-salt-factories-0.907.0/src/saltfactories/bases.py0000644000175000017500000016407314062063412024047 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE Factories base classes """ import atexit import contextlib import json import logging import os import pprint import subprocess import sys import tempfile import attr import psutil import pytest import salt.utils.files import salt.utils.path import salt.utils.verify import salt.utils.yaml from salt.utils.immutabletypes import freeze from saltfactories.exceptions import FactoryNotRunning from saltfactories.exceptions import FactoryNotStarted from saltfactories.exceptions import FactoryTimeout from saltfactories.utils import format_callback_to_string from saltfactories.utils import platform from saltfactories.utils import ports from saltfactories.utils import running_username from saltfactories.utils import time from saltfactories.utils.processes import ProcessResult from saltfactories.utils.processes import ShellResult from saltfactories.utils.processes import terminate_process from saltfactories.utils.processes import terminate_process_list log = logging.getLogger(__name__) @attr.s(kw_only=True) class Factory: """ Base factory class :keyword str display_name: Human readable name for the factory :keyword dict environ: A dictionary of ``key``, ``value`` pairs to add to the environment. :keyword str cwd: The path to the current working directory """ display_name = attr.ib(default=None) cwd = attr.ib(default=None) environ = attr.ib(repr=False, default=None) def __attrs_post_init__(self): if self.environ is None: self.environ = os.environ.copy() if self.cwd is None: self.cwd = os.getcwd() def get_display_name(self): """ Returns a human readable name for the factory """ if self.display_name: return "{}({})".format(self.__class__.__name__, self.display_name) return self.__class__.__name__ @attr.s(kw_only=True) class SubprocessImpl: """ Subprocess interaction implementation :keyword ~saltfactories.bases.Factory factory: The factory instance, either :py:class:`~saltfactories.bases.Factory` or a sub-class of it. """ factory = attr.ib() _terminal = attr.ib(repr=False, init=False, default=None) _terminal_stdout = attr.ib(repr=False, init=False, default=None) _terminal_stderr = attr.ib(repr=False, init=False, default=None) _terminal_result = attr.ib(repr=False, init=False, default=None) _terminal_timeout = attr.ib(repr=False, init=False, default=None) _children = attr.ib(repr=False, init=False, default=attr.Factory(list)) def cmdline(self, *args): """ Construct a list of arguments to use when starting the subprocess :param str 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, as is the case of :py:class:`saltfactories.base.SystemdSaltDaemonImpl`. """ return self.factory.cmdline(*args) def init_terminal(self, cmdline, env=None): """ 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 :param list,tuple cmdline: List of strings to pass as ``args`` to :py:class:`~subprocess.Popen` :keyword dict environ: A dictionary of ``key``, ``value`` pairs to add to the py:attr:`saltfactories.base.Factory.environ`. """ environ = self.factory.environ.copy() if env is not None: environ.update(env) self._terminal_stdout = tempfile.SpooledTemporaryFile(512000, buffering=0) self._terminal_stderr = tempfile.SpooledTemporaryFile(512000, buffering=0) if platform.is_windows(): # Windows does not support closing FDs close_fds = False elif platform.is_freebsd() and sys.version_info < (3, 9): # 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=False, cwd=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): 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): """ :return: Returns true if the sub-process is alive :rtype: bool """ if not self._terminal: return False return self._terminal.poll() is None def terminate(self): """ Terminate the started daemon :rtype: ~saltfactories.utils.processes.ProcessResult """ return self._terminate() def _terminate(self): """ This method actually terminates the started daemon """ if self._terminal is None: 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): for child in psutil.Process(self._terminal.pid).children(recursive=True): if child not in self._children: self._children.append(child) with self._terminal: 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 # 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() self._terminal_stdout.flush() self._terminal_stdout.seek(0) if sys.version_info < (3, 6): # pragma: no cover stdout = self._terminal._translate_newlines( self._terminal_stdout.read(), __salt_system_encoding__ ) else: stdout = self._terminal._translate_newlines( self._terminal_stdout.read(), __salt_system_encoding__, sys.stdout.errors ) self._terminal_stdout.close() self._terminal_stderr.flush() self._terminal_stderr.seek(0) if sys.version_info < (3, 6): # pragma: no cover stderr = self._terminal._translate_newlines( self._terminal_stderr.read(), __salt_system_encoding__ ) else: stderr = self._terminal._translate_newlines( self._terminal_stderr.read(), __salt_system_encoding__, sys.stderr.errors ) self._terminal_stderr.close() try: self._terminal_result = ProcessResult( self._terminal.returncode, stdout, stderr, cmdline=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._terminal_timeout = None self._children = [] @property def pid(self): """ The pid of the running process. None if not running. """ if not self._terminal: # pragma: no cover return return self._terminal.pid def run(self, *args, **kwargs): """ Run the given command synchronously """ cmdline = self.cmdline(*args, **kwargs) log.info("%s is running %r in CWD: %s ...", self.factory, cmdline, self.factory.cwd) return self.init_terminal(cmdline) @attr.s(kw_only=True) class Subprocess(Factory): """ Base CLI script/binary class :keyword str 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. :keyword list,tuple base_script_args: An list or tuple iterable of the base arguments to use when building the command line to launch the process :keyword bool 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. Please look at :py:class:`~saltfactories.bases.Factory` for the additional supported keyword arguments documentation. """ script_name = attr.ib() base_script_args = attr.ib(default=attr.Factory(list)) slow_stop = attr.ib(default=True) impl = attr.ib(repr=False, init=False) @impl.default def _set_impl_default(self): impl_class = self._get_impl_class() return impl_class(factory=self) def _get_impl_class(self): return SubprocessImpl def get_display_name(self): """ Returns a human readable name for the factory """ return self.display_name or self.script_name def get_script_path(self): """ Returns the path to the script to run """ if os.path.isabs(self.script_name): script_path = self.script_name else: script_path = salt.utils.path.which(self.script_name) if not script_path or not os.path.exists(script_path): pytest.fail("The CLI script {!r} does not exist".format(script_path)) return script_path def get_base_script_args(self): """ Returns any additional arguments to pass to the CLI script """ return list(self.base_script_args) def get_script_args(self): # pylint: disable=no-self-use """ Returns any additional arguments to pass to the CLI script """ return [] def cmdline(self, *args): """ Construct a list of arguments to use when starting the subprocess :param str args: Additional arguments to use when starting the subprocess :rtype: list """ return ( [self.get_script_path()] + self.get_base_script_args() + self.get_script_args() + list(args) ) def is_running(self): """ Returns true if the sub-process is alive """ return self.impl.is_running() def terminate(self): """ Terminate the started daemon """ return self.impl.terminate() @property def pid(self): """ The pid of the running process. None if not running. """ return self.impl.pid @attr.s(kw_only=True) class Process(Subprocess): """ Base process factory :keyword int timeout: The default maximum amount of seconds that a script should run. This value can be overridden when calling :py:meth:`~saltfactories.bases.Process.run` through the ``_timeout`` keyword argument, and, in that case, the timeout value applied would be that of ``_timeout`` instead of ``self.timeout``. Please look at :py:class:`~saltfactories.bases.Subprocess` for the additional supported keyword arguments documentation. """ timeout = attr.ib() @timeout.default def _set_timeout(self): if not sys.platform.startswith(("win", "darwin")): return 60 # Windows and macOS are just slower. return 120 def run(self, *args, _timeout=None, **kwargs): """ Run the given command synchronously :keyword list,tuple args: The list of arguments to pass to :py:meth:`~saltfactories.bases.Process.cmdline` to construct the command to run :keyword int _timeout: The timeout value for this particular ``run()`` call. If this value is not ``None``, it will be used instead of :py:attr:`~saltfactories.bases.Process.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, **kwargs) self.impl._terminal.communicate(timeout=self.impl._terminal_timeout) except subprocess.TimeoutExpired: timmed_out = True result = self.terminate() cmdline = result.cmdline exitcode = result.exitcode if timmed_out: raise FactoryTimeout( "{} Failed to run: {}; Error: Timed out after {:.2f} seconds!".format( self, cmdline, time.time() - start_time ), stdout=result.stdout, stderr=result.stderr, cmdline=cmdline, exitcode=exitcode, ) 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 ShellResult(exitcode, stdout, stderr, json=json_out, cmdline=cmdline) def process_output(self, stdout, stderr, cmdline=None): """ Process the output. When possible JSON is loaded from the output. :return: Returns a tuple in the form of ``(stdout, stderr, loaded_json)`` :rtype: tuple """ 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(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 list,tuple args: List of arguments :keyword dict kwargs: Dictionary of keyword arguments """ args = attr.ib() kwargs = attr.ib() @attr.s(kw_only=True) class DaemonImpl(SubprocessImpl): """ Daemon subprocess interaction implementation Please look at :py:class:`~saltfactories.bases.SubprocessImpl` for the additional supported keyword arguments documentation. """ _before_start_callbacks = attr.ib(repr=False, hash=False, default=attr.Factory(list)) _after_start_callbacks = attr.ib(repr=False, hash=False, default=attr.Factory(list)) _before_terminate_callbacks = attr.ib(repr=False, hash=False, default=attr.Factory(list)) _after_terminate_callbacks = attr.ib(repr=False, hash=False, default=attr.Factory(list)) _start_args_and_kwargs = attr.ib(init=False, repr=False, hash=False) def before_start(self, callback, *args, **kwargs): """ Register a function callback to run before the daemon starts :param ~collections.abc.Callable callback: The function to call back :keyword args: The arguments to pass to the callback :keyword kwargs: The keyword arguments to pass to the callback """ self._before_start_callbacks.append((callback, args, kwargs)) def after_start(self, callback, *args, **kwargs): """ Register a function callback to run after the daemon starts :param ~collections.abc.Callable callback: The function to call back :keyword args: The arguments to pass to the callback :keyword kwargs: The keyword arguments to pass to the callback """ self._after_start_callbacks.append((callback, args, kwargs)) def before_terminate(self, callback, *args, **kwargs): """ Register a function callback to run before the daemon terminates :param ~collections.abc.Callable callback: The function to call back :keyword args: The arguments to pass to the callback :keyword kwargs: The keyword arguments to pass to the callback """ self._before_terminate_callbacks.append((callback, args, kwargs)) def after_terminate(self, callback, *args, **kwargs): """ Register a function callback to run after the daemon terminates :param ~collections.abc.Callable callback: The function to call back :keyword args: The arguments to pass to the callback :keyword kwargs: The keyword arguments to pass to the callback """ self._after_terminate_callbacks.append((callback, args, kwargs)) def start(self, *extra_cli_arguments, max_start_attempts=None, start_timeout=None): """ Start the daemon :keyword tuple, extra_cli_arguments: Extra arguments to pass to the CLI that starts the daemon :keyword int max_start_attempts: Maximum number of attempts to try and start the daemon in case of failures :keyword int start_timeout: The maximum number of seconds to wait before considering that the daemon did not start """ 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, args, kwargs in self._before_start_callbacks: try: callback(*args, **kwargs) except Exception as exc: # pragma: no cover pylint: disable=broad-except log.info( "Exception raised when running %s: %s", format_callback_to_string(callback, args, kwargs), 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, args, kwargs in self._after_start_callbacks: try: callback(*args, **kwargs) except Exception as exc: # pragma: no cover pylint: disable=broad-except log.info( "Exception raised when running %s: %s", format_callback_to_string(callback, args, kwargs), 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, ), stdout=result.stdout, stderr=result.stderr, exitcode=result.exitcode, cmdline=result.cmdline, ) def terminate(self): """ Terminate the daemon """ if self._terminal_result is not None: # This factory has already been terminated return self._terminal_result for callback, args, kwargs in self._before_terminate_callbacks: try: callback(*args, **kwargs) except Exception as exc: # pragma: no cover pylint: disable=broad-except log.info( "Exception raised when running %s: %s", format_callback_to_string(callback, args, kwargs), exc, exc_info=True, ) try: return super().terminate() finally: for callback, args, kwargs in self._after_terminate_callbacks: try: callback(*args, **kwargs) except Exception as exc: # pragma: no cover pylint: disable=broad-except log.info( "Exception raised when running %s: %s", format_callback_to_string(callback, args, kwargs), exc, exc_info=True, ) def get_start_arguments(self): """ Return the arguments and keyword arguments used when starting the daemon: :rtype: StartDaemonCallArguments """ return self._start_args_and_kwargs @attr.s(kw_only=True) class Daemon(Subprocess): """ Base daemon factory :keyword list check_ports: List of ports to try and connect to while confirming that the daemon is up and running :keyword tuple, extra_cli_arguments_after_first_start_failure: Extra arguments to pass to the CLI that starts the daemon after the first failure :keyword int max_start_attempts: Maximum number of attempts to try and start the daemon in case of failures :keyword int start_timeout: The maximum number of seconds to wait before considering that the daemon did not start Please look at :py:class:`~saltfactories.bases.Subprocess` for the additional supported keyword arguments documentation. """ check_ports = attr.ib(default=None) factories_manager = attr.ib(repr=False, hash=False, default=None) start_timeout = attr.ib(repr=False) max_start_attempts = attr.ib(repr=False, default=3) extra_cli_arguments_after_first_start_failure = attr.ib(hash=False, default=attr.Factory(list)) listen_ports = attr.ib(init=False, repr=False, hash=False, default=attr.Factory(list)) def _get_impl_class(self): return DaemonImpl def __attrs_post_init__(self): 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) def before_start(self, callback, *args, **kwargs): """ Register a function callback to run before the daemon starts :param ~collections.abc.Callable callback: The function to call back :keyword args: The arguments to pass to the callback :keyword kwargs: The keyword arguments to pass to the callback """ self.impl._before_start_callbacks.append((callback, args, kwargs)) def after_start(self, callback, *args, **kwargs): """ Register a function callback to run after the daemon starts :param ~collections.abc.Callable callback: The function to call back :keyword args: The arguments to pass to the callback :keyword kwargs: The keyword arguments to pass to the callback """ self.impl._after_start_callbacks.append((callback, args, kwargs)) def before_terminate(self, callback, *args, **kwargs): """ Register a function callback to run before the daemon terminates :param ~collections.abc.Callable callback: The function to call back :keyword args: The arguments to pass to the callback :keyword kwargs: The keyword arguments to pass to the callback """ self.impl._before_terminate_callbacks.append((callback, args, kwargs)) def after_terminate(self, callback, *args, **kwargs): """ Register a function callback to run after the daemon terminates :param ~collections.abc.Callable callback: The function to call back :keyword args: The arguments to pass to the callback :keyword kwargs: The keyword arguments to pass to the callback """ self.impl._after_terminate_callbacks.append((callback, args, kwargs)) def get_check_ports(self): """ Return a list of ports to check against to ensure the daemon is running """ return self.check_ports or [] def start(self, *extra_cli_arguments, max_start_attempts=None, start_timeout=None): """ Start the daemon """ return self.impl.start( *extra_cli_arguments, max_start_attempts=max_start_attempts, start_timeout=start_timeout ) def started(self, *extra_cli_arguments, max_start_attempts=None, start_timeout=None): """ Start the daemon and return it's instance so it can be used as a context manager """ self.start( *extra_cli_arguments, max_start_attempts=max_start_attempts, start_timeout=start_timeout ) return self @contextlib.contextmanager def stopped( self, before_stop_callback=None, after_stop_callback=None, before_start_callback=None, after_start_callback=None, ): """ :keyword ~collections.abc.Callable before_stop_callback: A callable to run before stopping the daemon. The callback must accept one argument, the daemon instance. :keyword ~collections.abc.Callable after_stop_callback: A callable to run after stopping the daemon. The callback must accept one argument, the daemon instance. :keyword ~collections.abc.Callable before_start_callback: A callable to run before starting the daemon. The callback must accept one argument, the daemon instance. :keyword ~collections.abc.Callable 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. For 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("{} is not running ".format(self)) start_arguments = self.impl.get_start_arguments() try: if before_stop_callback: try: before_stop_callback(self) except Exception as exc: # pragma: no cover pylint: disable=broad-except 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 Exception as exc: # pragma: no cover pylint: disable=broad-except log.info( "Exception raised when running %s: %s", format_callback_to_string(after_stop_callback), exc, exc_info=True, ) yield except Exception: # pragma: no cover pylint: disable=broad-except,try-except-raise raise else: if before_start_callback: try: before_start_callback(self) except Exception as exc: # pragma: no cover pylint: disable=broad-except log.info( "Exception raised when running %s: %s", format_callback_to_string(before_start_callback), exc, exc_info=True, ) _started = self.started(*start_arguments.args, **start_arguments.kwargs) if _started: if after_start_callback: try: after_start_callback(self) except Exception as exc: # pragma: no cover pylint: disable=broad-except log.info( "Exception raised when running %s: %s", format_callback_to_string(after_start_callback), exc, exc_info=True, ) return _started def run_start_checks(self, started_at, timeout_at): """ Run checks to confirm that the daemon has started """ log.debug("%s is running start checks", self) 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("{} is no longer running".format(self)) 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): if self.factories_manager and self.factories_manager.stats_processes is not None: display_name = self.get_display_name() self.factories_manager.stats_processes.add(display_name, self.pid) def _remove_factory_from_stats_processes(self): if self.factories_manager and self.factories_manager.stats_processes is not None: display_name = self.get_display_name() self.factories_manager.stats_processes.remove(display_name) def _terminate_processes_matching_listen_ports(self): 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 = [] 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: # We've been denied access to this process connections. Carry on. 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]), 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]), ) def __enter__(self): 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, *_): self.terminate() @attr.s(kw_only=True) class Salt: """ Base factory for salt cli's and daemon's :param dict config: The Salt config dictionary :param str python_executable: The path to the python executable to use :param bool system_install: If true, the daemons and CLI's are run against a system installed salt setup, ie, the default salt system paths apply. """ id = attr.ib(default=None, init=False) config = attr.ib(repr=False) config_dir = attr.ib(init=False, default=None) config_file = attr.ib(init=False, default=None) python_executable = attr.ib(default=None) system_install = attr.ib(repr=False, default=False) display_name = attr.ib(init=False, default=None) def __attrs_post_init__(self): if self.python_executable is None and self.system_install is False: self.python_executable = sys.executable # We really do not want buffered output self.environ.setdefault("PYTHONUNBUFFERED", "1") # Don't write .pyc files or create them in __pycache__ directories self.environ.setdefault("PYTHONDONTWRITEBYTECODE", "1") self.config_file = self.config["conf_file"] self.config_dir = os.path.dirname(self.config_file) self.id = self.config["id"] self.config = freeze(self.config) def get_display_name(self): """ Returns a human readable name for the factory """ if self.display_name is None: self.display_name = "{}(id={!r})".format(self.__class__.__name__, self.id) return super().get_display_name() @attr.s(kw_only=True) class SaltCliImpl(SubprocessImpl): """ Salt CLI's subprocess interaction implementation Please look at :py:class:`~saltfactories.bases.SubprocessImpl` for the additional supported keyword arguments documentation. """ def cmdline(self, *args, minion_tgt=None, **kwargs): # pylint: disable=arguments-differ """ Construct a list of arguments to use when starting the subprocess :param str args: Additional arguments to use when starting the subprocess :keyword str minion_tgt: The minion ID to target :keyword kwargs: Additional keyword arguments will be converted into ``key=value`` pairs to be consumed by the salt CLI's """ return self.factory.cmdline(*args, minion_tgt=minion_tgt, **kwargs) @attr.s(kw_only=True) class SaltCli(Salt, Process): """ Base factory for salt cli's :param bool hard_crash: Pass ``--hard-crash`` to Salt's CLI's Please look at :py:class:`~saltfactories.bases.Salt` and :py:class:`~saltfactories.bases.Process` for the additional supported keyword arguments documentation. """ hard_crash = attr.ib(repr=False, default=False) # Override the following to default to non-mandatory and to None display_name = attr.ib(init=False, default=None) _minion_tgt = attr.ib(repr=False, init=False, default=None) merge_json_output = attr.ib(repr=False, default=True) __cli_timeout_supported__ = attr.ib(repr=False, init=False, default=False) __cli_log_level_supported__ = attr.ib(repr=False, init=False, default=True) __cli_output_supported__ = attr.ib(repr=False, init=False, default=True) __json_output__ = attr.ib(repr=False, init=False, default=False) __merge_json_output__ = attr.ib(repr=False, init=False, default=True) def _get_impl_class(self): return SaltCliImpl def __attrs_post_init__(self): Process.__attrs_post_init__(self) Salt.__attrs_post_init__(self) def get_script_args(self): """ Returns any additional arguments to pass to the CLI script """ if not self.hard_crash: return super().get_script_args() return ["--hard-crash"] def get_minion_tgt(self, minion_tgt=None): return minion_tgt def cmdline( self, *args, minion_tgt=None, merge_json_output=None, **kwargs ): # pylint: disable=arguments-differ """ Construct a list of arguments to use when starting the subprocess :param str args: Additional arguments to use when starting the subprocess :keyword str minion_tgt: The minion ID to target :keyword bool merge_json_output: The default behavior of salt outputters is to print one line per minion return, which makes parsing the whole output as JSON impossible when targeting multiple minions. If this value is ``True``, an attempt is made to merge each JSON line into a single dictionary. :keyword kwargs: Additional keyword arguments will be converted into ``key=value`` pairs to be consumed by the salt CLI's """ log.debug( "Building cmdline. Minion target: %s; Input args: %s; Input kwargs: %s;", minion_tgt, args, kwargs, ) minion_tgt = self._minion_tgt = self.get_minion_tgt(minion_tgt=minion_tgt) if merge_json_output is None: self.__merge_json_output__ = self.merge_json_output else: self.__merge_json_output__ = merge_json_output cmdline = [] # Convert all passed in arguments to strings args = [str(arg) for arg in args] # Handle the config directory flag for arg in args: if arg.startswith("--config-dir="): break if arg in ("-c", "--config-dir"): break else: cmdline.append("--config-dir={}".format(self.config_dir)) # Handle the timeout CLI flag, if supported if self.__cli_timeout_supported__: salt_cli_timeout_next = False for arg in args: if arg.startswith("--timeout="): # Let's actually change the _terminal_timeout value which is used to # calculate when the run() method should actually timeout try: salt_cli_timeout = int(arg.split("--timeout=")[-1]) except ValueError: # Not a number? Let salt do it's error handling break if salt_cli_timeout >= self.impl._terminal_timeout: self.impl._terminal_timeout = int(salt_cli_timeout) + 10 break if salt_cli_timeout_next: try: salt_cli_timeout = int(arg) except ValueError: # Not a number? Let salt do it's error handling break if salt_cli_timeout >= self.impl._terminal_timeout: self.impl._terminal_timeout = int(salt_cli_timeout) + 10 break if arg == "-t" or arg.startswith("--timeout"): salt_cli_timeout_next = True continue else: # Pass the default timeout to salt and increase the internal timeout by 10 seconds to # allow salt to exit cleanly. salt_cli_timeout = self.impl._terminal_timeout if salt_cli_timeout: self.impl._terminal_timeout = salt_cli_timeout + 10 # Add it to the salt command CLI flags cmdline.append("--timeout={}".format(salt_cli_timeout)) # Handle the output flag if self.__cli_output_supported__: for idx, arg in enumerate(args): if arg in ("--out", "--output"): self.__json_output__ = args[idx + 1] == "json" break if arg.startswith(("--out=", "--output=")): self.__json_output__ = arg.split("=")[-1].strip() == "json" break else: # No output was passed, the default output is JSON cmdline.append("--out=json") self.__json_output__ = True if self.__json_output__: for arg in args: if arg in ("--out-indent", "--output-indent"): break if arg.startswith(("--out-indent=", "--output-indent=")): break else: # Default to one line per output cmdline.append("--out-indent=0") if self.__cli_log_level_supported__: # Handle the logging flag for arg in args: if arg in ("-l", "--log-level"): break if arg.startswith("--log-level="): break else: # Default to being almost quiet on console output cmdline.append("--log-level=critical") if minion_tgt: cmdline.append(minion_tgt) # Add the remaining args cmdline.extend(args) # Keyword arguments get passed as KEY=VALUE pairs to the CLI for key in kwargs: value = kwargs[key] if not isinstance(value, str): value = json.dumps(value) cmdline.append("{}={}".format(key, value)) cmdline = super().cmdline(*cmdline) if self.python_executable: if cmdline[0] != self.python_executable: cmdline.insert(0, self.python_executable) log.debug("Built cmdline: %s", cmdline) return cmdline def process_output(self, stdout, stderr, cmdline=None): """ Process the output. When possible JSON is loaded from the output. :return: Returns a tuple in the form of ``(stdout, stderr, loaded_json)`` :rtype: tuple """ json_out = None if stdout and self.__json_output__: try: json_out = json.loads(stdout) except ValueError: if self.__merge_json_output__: try: json_out = json.loads(stdout.replace("}\n{", ", ")) except ValueError: pass if json_out is None: log.debug("%s failed to load JSON from the following output:\n%r", self, stdout) if ( self.__cli_output_supported__ and json_out and isinstance(json_out, str) and self.__json_output__ ): # Sometimes the parsed JSON is just a string, for example: # OUTPUT: '"The salt master could not be contacted. Is master running?"\n' # LOADED JSON: 'The salt master could not be contacted. Is master running?' # # In this case, we assign the loaded JSON to stdout and reset json_out stdout = json_out json_out = None if ( self.__cli_output_supported__ and json_out and self._minion_tgt and self._minion_tgt != "*" ): try: json_out = json_out[self._minion_tgt] except KeyError: # pragma: no cover pass return stdout, stderr, json_out @attr.s(kw_only=True) class SystemdSaltDaemonImpl(DaemonImpl): """ Daemon systemd interaction implementation Please look at :py:class:`~saltfactories.bases.DaemonImpl` for the additional supported keyword arguments documentation. """ _process = attr.ib(init=False, repr=False, default=None) _service_name = attr.ib(init=False, repr=False, default=None) def cmdline(self, *args): """ Construct a list of arguments to use when starting the subprocess :param str args: Additional arguments to use when starting the subprocess """ if args: # pragma: no cover log.debug( "%s.run() is ignoring the passed in arguments: %r", self.__class__.__name__, args ) return ("systemctl", "start", self.get_service_name()) def get_service_name(self): """ Return the systemd service name """ if self._service_name is None: script_path = self.factory.get_script_path() if os.path.isabs(script_path): script_path = os.path.basename(script_path) self._service_name = script_path return self._service_name def is_running(self): """ Returns true if the sub-process is alive """ if self._process is None: ret = self.internal_run("systemctl", "show", "-p", "MainPID", self.get_service_name()) _, mainpid = ret.stdout.split("=") if mainpid == "0": return False self._process = psutil.Process(int(mainpid)) return self._process.is_running() def internal_run(self, *args, **kwargs): """ Run the given command synchronously """ result = subprocess.run( args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, check=False, **kwargs ) stdout = result.stdout.strip() stderr = result.stderr.strip() process_result = ProcessResult(result.returncode, stdout, stderr, cmdline=result.args) log.info("%s %s", self.factory.__class__.__name__, process_result) return process_result def start(self, *extra_cli_arguments, max_start_attempts=None, start_timeout=None): started = super().start( *extra_cli_arguments, max_start_attempts=max_start_attempts, start_timeout=start_timeout ) atexit.register(self.terminate) return started def _terminate(self): """ This method actually terminates the started daemon """ if self._process is None: # pragma: no cover 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): for child in psutil.Process(self.pid).children(recursive=True): if child not in self._children: self._children.append(child) pid = self.pid cmdline = self._process.cmdline() self.internal_run("systemctl", "stop", self.get_service_name()) if self._process.is_running(): # pragma: no cover try: self._process.wait() except psutil.TimeoutExpired: self._process.terminate() try: self._process.wait() except psutil.TimeoutExpired: pass exitcode = self._process.wait() or 0 self._process = None # Lets log and kill any child processes left behind, including the main subprocess # if it failed to properly stop terminate_process( pid=pid, kill_children=True, children=self._children, slow_stop=self.factory.slow_stop, ) self._terminal_stdout.close() self._terminal_stderr.close() stdout = "" ret = self.internal_run("journalctl", "--no-pager", "-u", self.get_service_name()) stderr = ret.stdout try: self._terminal_result = ProcessResult(exitcode, stdout, stderr, cmdline=cmdline) 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._terminal_timeout = None self._children = [] @property def pid(self): if self.is_running(): return self._process.pid @attr.s(kw_only=True) class SaltDaemon(Salt, Daemon): """ Base factory for salt daemon's Please look at :py:class:`~saltfactories.bases.Salt` and :py:class:`~saltfactories.bases.Daemon` for the additional supported keyword arguments documentation. """ display_name = attr.ib(init=False, default=None) event_listener = attr.ib(repr=False, default=None) started_at = attr.ib(repr=False, default=None) def __attrs_post_init__(self): Daemon.__attrs_post_init__(self) Salt.__attrs_post_init__(self) if self.system_install is True and self.extra_cli_arguments_after_first_start_failure: raise pytest.UsageError( "You cannot pass `extra_cli_arguments_after_first_start_failure` to a salt " "system installation setup." ) elif self.system_install is False: for arg in self.extra_cli_arguments_after_first_start_failure: if arg in ("-l", "--log-level"): break if arg.startswith("--log-level="): break else: self.extra_cli_arguments_after_first_start_failure.append("--log-level=debug") def _get_impl_class(self): if self.system_install: return SystemdSaltDaemonImpl return super()._get_impl_class() @classmethod def configure( cls, factories_manager, daemon_id, root_dir=None, defaults=None, overrides=None, **configure_kwargs ): """ Configure the salt daemon """ return cls._configure( factories_manager, daemon_id, root_dir=root_dir, defaults=defaults, overrides=overrides, **configure_kwargs ) @classmethod def _configure( cls, factories_manager, daemon_id, root_dir=None, defaults=None, overrides=None, ): raise NotImplementedError @classmethod def verify_config(cls, config): salt.utils.verify.verify_env( cls._get_verify_config_entries(config), running_username(), pki_dir=config.get("pki_dir") or "", root_dir=config["root_dir"], ) @classmethod def _get_verify_config_entries(cls, config): raise NotImplementedError @classmethod def write_config(cls, config): """ Write the configuration to file """ config_file = config.pop("conf_file") log.debug( "Writing to configuration file %s. Configuration:\n%s", config_file, pprint.pformat(config), ) # Write down the computed configuration into the config file with salt.utils.files.fopen(config_file, "w") as wfh: salt.utils.yaml.safe_dump(config, wfh, default_flow_style=False) loaded_config = cls.load_config(config_file, config) cls.verify_config(loaded_config) return loaded_config @classmethod def load_config(cls, config_file, config): """ Should return the configuration as the daemon would have loaded after parsing the CLI """ raise NotImplementedError def get_check_events(self): """ Return a list of tuples in the form of `(master_id, event_tag)` check against to ensure the daemon is running """ raise NotImplementedError def cmdline(self, *args): """ Construct a list of arguments to use when starting the subprocess :param str args: Additional arguments to use when starting the subprocess """ _args = [] # Handle the config directory flag for arg in args: if not isinstance(arg, str): continue if arg.startswith("--config-dir="): break if arg in ("-c", "--config-dir"): break else: _args.append("--config-dir={}".format(self.config_dir)) # Handle the logging flag for arg in args: if not isinstance(arg, str): continue if arg in ("-l", "--log-level"): break if arg.startswith("--log-level="): break else: # Default to being almost quiet on console output _args.append("--log-level=critical") cmdline = super().cmdline(*(_args + list(args))) if self.python_executable: if cmdline[0] != self.python_executable: cmdline.insert(0, self.python_executable) return cmdline def run_start_checks(self, started_at, timeout_at): """ Run checks to confirm that the daemon has started """ if not super().run_start_checks(started_at, timeout_at): return False if not self.event_listener: # pragma: no cover log.debug("The 'event_listener' attribute is not set. Not checking events...") return True check_events = set(self.get_check_events()) if not check_events: log.debug("No events to listen to for %s", self) return True log.debug("Events to check for %s: %s", self, set(self.get_check_events())) checks_start_time = time.time() while time.time() <= timeout_at: if not self.is_running(): raise FactoryNotStarted("{} is no longer running".format(self)) if not check_events: break check_events -= { (event.daemon_id, event.tag) for event in self.event_listener.get_events(check_events, after_time=started_at) } if check_events: time.sleep(1.5) else: log.error( "Failed to check events after %1.2f seconds for %s. Remaining events to check: %s", time.time() - checks_start_time, self, check_events, ) return False log.debug("All events checked for %s: %s", self, set(self.get_check_events())) return True pytest-salt-factories-0.907.0/src/saltfactories/cli/0000755000175000017500000000000014106144336023140 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/src/saltfactories/cli/__init__.py0000644000175000017500000000024314036250542025247 0ustar vampasvampas00000000000000""" CLI === """ from . import call from . import cloud from . import cp from . import key from . import run from . import salt from . import spm from . import ssh pytest-salt-factories-0.907.0/src/saltfactories/cli/call.py0000644000175000017500000000106714036250542024430 0ustar vampasvampas00000000000000""" ``salt-call`` CLI factory """ import attr from saltfactories.bases import SaltCli @attr.s(kw_only=True, slots=True) class SaltCall(SaltCli): """ salt-call CLI factory """ __cli_timeout_supported__ = attr.ib(repr=False, init=False, default=True) def get_minion_tgt(self, minion_tgt=None): return None def process_output(self, stdout, stderr, cmdline=None): # Under salt-call, the minion target is always "local" self._minion_tgt = "local" return super().process_output(stdout, stderr, cmdline=cmdline) pytest-salt-factories-0.907.0/src/saltfactories/cli/cloud.py0000644000175000017500000000573714036250542024633 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE ``salt-cloud`` CLI factory """ import logging import pathlib import pprint import urllib.parse import attr import salt.config import salt.utils.dictupdate import salt.utils.files import salt.utils.yaml from saltfactories.bases import SaltCli from saltfactories.utils import running_username log = logging.getLogger(__name__) @attr.s(kw_only=True, slots=True) class SaltCloud(SaltCli): """ salt-cloud CLI factory """ @staticmethod def default_config(root_dir, master_id, defaults=None, overrides=None): if defaults is None: defaults = {} conf_dir = root_dir / "conf" conf_dir.mkdir(parents=True, exist_ok=True) for confd in ("cloud.conf.d", "cloud.providers.d", "cloud.profiles.d"): dpath = conf_dir / confd dpath.mkdir(exist_ok=True) conf_file = str(conf_dir / "cloud") _defaults = { "conf_file": conf_file, "root_dir": str(root_dir), "log_file": "logs/cloud.log", "log_level_logfile": "debug", "pytest-cloud": { "master-id": master_id, "log": {"prefix": "{{cli_name}}({})".format(master_id)}, }, } # Merge in the initial default options with the internal _defaults salt.utils.dictupdate.update(defaults, _defaults, merge_lists=True) if overrides: # Merge in the default options with the master_overrides salt.utils.dictupdate.update(defaults, overrides, merge_lists=True) return defaults @classmethod def configure( cls, factories_manager, daemon_id, root_dir=None, defaults=None, overrides=None, **configure_kwargs ): return cls.default_config(root_dir, daemon_id, defaults=defaults, overrides=overrides) @classmethod def verify_config(cls, config): prepend_root_dirs = [] for config_key in ("log_file",): if urllib.parse.urlparse(config.get(config_key, "")).scheme == "": prepend_root_dirs.append(config_key) if prepend_root_dirs: salt.config.prepend_root_dir(config, prepend_root_dirs) salt.utils.verify.verify_env( [str(pathlib.Path(config["log_file"]).parent)], running_username(), pki_dir=config.get("pki_dir") or "", root_dir=config["root_dir"], ) @classmethod def write_config(cls, config): cls.verify_config(config) config_file = config.pop("conf_file") log.debug( "Writing to configuration file %s. Configuration:\n%s", config_file, pprint.pformat(config), ) # Write down the computed configuration into the config file with salt.utils.files.fopen(config_file, "w") as wfh: salt.utils.yaml.safe_dump(config, wfh, default_flow_style=False) return salt.config.cloud_config(config_file) pytest-salt-factories-0.907.0/src/saltfactories/cli/cp.py0000644000175000017500000000104014036250542024106 0ustar vampasvampas00000000000000""" ``salt-cp`` CLI factory """ import attr from saltfactories.bases import SaltCli @attr.s(kw_only=True, slots=True) class SaltCp(SaltCli): """ salt-cp CLI factory """ __cli_timeout_supported__ = attr.ib(repr=False, init=False, default=True) def process_output(self, stdout, stderr, cmdline=None): if "No minions matched the target. No command was sent, no jid was assigned.\n" in stdout: stdout = stdout.split("\n", 1)[1:][0] return super().process_output(stdout, stderr, cmdline=cmdline) pytest-salt-factories-0.907.0/src/saltfactories/cli/key.py0000644000175000017500000000235314036250542024304 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE ``salt-key`` CLI factory """ import re import attr from salt.utils.parsers import SaltKeyOptionParser from saltfactories.bases import SaltCli try: SALT_KEY_LOG_LEVEL_SUPPORTED = SaltKeyOptionParser._skip_console_logging_config_ is False except AttributeError: # pragma: no cover # New logging is in place SALT_KEY_LOG_LEVEL_SUPPORTED = True @attr.s(kw_only=True, slots=True) class SaltKey(SaltCli): """ salt-key CLI factory """ _output_replace_re = attr.ib( init=False, repr=False, default=re.compile(r"((The following keys are going to be.*:|Key for minion.*)\n)"), ) # As of Neon, salt-key still does not support --log-level # Only when we get the new logging merged in will we get that, so remove that CLI flag __cli_log_level_supported__ = attr.ib( repr=False, init=False, default=SALT_KEY_LOG_LEVEL_SUPPORTED ) def get_minion_tgt(self, minion_tgt=None): return None def process_output(self, stdout, stderr, cmdline=None): # salt-key print()s to stdout regardless of output chosen stdout = self._output_replace_re.sub("", stdout) return super().process_output(stdout, stderr, cmdline=cmdline) pytest-salt-factories-0.907.0/src/saltfactories/cli/run.py0000644000175000017500000000114714036250542024320 0ustar vampasvampas00000000000000""" ``salt-run`` CLI factory """ import attr from saltfactories.bases import SaltCli @attr.s(kw_only=True, slots=True) class SaltRun(SaltCli): """ salt-run CLI factory """ __cli_timeout_supported__ = attr.ib(repr=False, init=False, default=True) def get_minion_tgt(self, minion_tgt=None): return None def process_output(self, stdout, stderr, cmdline=None): if "No minions matched the target. No command was sent, no jid was assigned.\n" in stdout: stdout = stdout.split("\n", 1)[1:][0] return super().process_output(stdout, stderr, cmdline=cmdline) pytest-salt-factories-0.907.0/src/saltfactories/cli/salt.py0000644000175000017500000000221614036250542024455 0ustar vampasvampas00000000000000""" ``salt`` CLI factory """ import attr import pytest from saltfactories.bases import SaltCli @attr.s(kw_only=True, slots=True) class Salt(SaltCli): """ salt CLI factory """ __cli_timeout_supported__ = attr.ib(repr=False, init=False, default=True) def cmdline(self, *args, minion_tgt=None, **kwargs): # pylint: disable=arguments-differ skip_raise_exception_args = {"-V", "--version", "--versions-report", "--help"} if minion_tgt is None and not set(args).intersection(skip_raise_exception_args): raise pytest.UsageError( "The `minion_tgt` keyword argument is mandatory for the salt CLI factory" ) return super().cmdline(*args, minion_tgt=minion_tgt, **kwargs) def process_output(self, stdout, stderr, cmdline=None): if "No minions matched the target. No command was sent, no jid was assigned.\n" in stdout: stdout = stdout.split("\n", 1)[1:][0] if cmdline and "--show-jid" in cmdline and stdout.startswith("jid: "): stdout = stdout.split("\n", 1)[-1].strip() return super().process_output(stdout, stderr, cmdline=cmdline) pytest-salt-factories-0.907.0/src/saltfactories/cli/spm.py0000644000175000017500000000050414036250542024307 0ustar vampasvampas00000000000000""" ``spm`` CLI factory """ import attr from saltfactories.bases import SaltCli @attr.s(kw_only=True, slots=True) class Spm(SaltCli): """ ``spm`` CLI factory """ __cli_output_supported__ = attr.ib(repr=False, init=False, default=False) def get_minion_tgt(self, minion_tgt=None): return None pytest-salt-factories-0.907.0/src/saltfactories/cli/ssh.py0000644000175000017500000000204214036250542024304 0ustar vampasvampas00000000000000""" salt-ssh CLI factory """ import attr from saltfactories.bases import SaltCli @attr.s(kw_only=True, slots=True) class SaltSsh(SaltCli): """ salt CLI factory """ roster_file = attr.ib(default=None) client_key = attr.ib(default=None) target_host = attr.ib(default=None) ssh_user = attr.ib(default=None) def __attrs_post_init__(self): super().__attrs_post_init__() if self.target_host is None: self.target_host = "127.0.0.1" def get_script_args(self): script_args = super().get_script_args() if self.roster_file: script_args.append("--roster-file={}".format(self.roster_file)) if self.client_key: script_args.append("--priv={}".format(self.client_key)) if self.ssh_user: script_args.append("--user={}".format(self.ssh_user)) return script_args def get_minion_tgt(self, minion_tgt=None): if minion_tgt is None and self.target_host: minion_tgt = self.target_host return minion_tgt pytest-salt-factories-0.907.0/src/saltfactories/client.py0000644000175000017500000000653514105112520024217 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE Salt Client in-process implementation """ import logging import re import attr import pytest import salt.client log = logging.getLogger(__name__) @attr.s(kw_only=True, slots=True) class LocalClient: """ Wrapper class around Salt's local client """ STATE_FUNCTION_RUNNING_RE = re.compile( r"""The function (?:"|')(?P.*)(?:"|') is running as PID """ r"(?P[\d]+) and was started at (?P.*) with jid (?P[\d]+)" ) master_config = attr.ib(repr=False) functions_known_to_return_none = attr.ib(repr=False) __client = attr.ib(init=False, repr=False) @functions_known_to_return_none.default def _set_functions_known_to_return_none(self): return ( "data.get", "file.chown", "file.chgrp", "pkg.refresh_db", "ssh.recv_known_host_entries", "time.sleep", ) @__client.default def _set_client(self): return salt.client.get_local_client(mopts=self.master_config) def run(self, function, *args, minion_tgt="minion", timeout=300, **kwargs): """ Run a single salt function and condition the return down to match the behavior of the raw function call """ if "f_arg" in kwargs: kwargs["arg"] = kwargs.pop("f_arg") if "f_timeout" in kwargs: kwargs["timeout"] = kwargs.pop("f_timeout") ret = self.__client.cmd(minion_tgt, function, args, timeout=timeout, kwarg=kwargs) if minion_tgt not in ret: pytest.fail( "WARNING(SHOULD NOT HAPPEN #1935): Failed to get a reply " "from the minion '{}'. Command output: {}".format(minion_tgt, ret) ) elif ret[minion_tgt] is None and function not in self.__functions_known_to_return_none: pytest.fail( "WARNING(SHOULD NOT HAPPEN #1935): Failed to get '{}' from " "the minion '{}'. Command output: {}".format(function, minion_tgt, ret) ) # Try to match stalled state functions ret[minion_tgt] = self._check_state_return(ret[minion_tgt]) return ret[minion_tgt] def _check_state_return(self, ret): if isinstance(ret, dict): # This is the supposed return format for state calls return ret if isinstance(ret, list): jids = [] # These are usually errors for item in ret[:]: if not isinstance(item, str): # We don't know how to handle this continue match = self.STATE_FUNCTION_RUNNING_RE.match(item) if not match: # We don't know how to handle this continue jid = match.group("jid") if jid in jids: continue jids.append(jid) job_data = self.run("saltutil.find_job", jid) job_kill = self.run("saltutil.kill_job", jid) msg = ( "A running state.single was found causing a state lock. " "Job details: '{}' Killing Job Returned: '{}'".format(job_data, job_kill) ) ret.append("[TEST SUITE ENFORCED]{}[/TEST SUITE ENFORCED]".format(msg)) return ret pytest-salt-factories-0.907.0/src/saltfactories/daemons/0000755000175000017500000000000014106144336024017 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/src/saltfactories/daemons/__init__.py0000644000175000017500000000025014036250542026124 0ustar vampasvampas00000000000000""" Daemons ======= """ from . import api from . import container from . import master from . import minion from . import proxy from . import sshd from . import syndic pytest-salt-factories-0.907.0/src/saltfactories/daemons/api.py0000644000175000017500000000303214036250542025137 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE Salt API Factory """ import attr import pytest from saltfactories.bases import SaltDaemon @attr.s(kw_only=True, slots=True) class SaltApi(SaltDaemon): def __attrs_post_init__(self): if "rest_cherrypy" in self.config: self.check_ports = [self.config["rest_cherrypy"]["port"]] elif "rest_tornado" in self.config: self.check_ports = [self.config["rest_tornado"]["port"]] else: raise pytest.UsageError( "The salt-master configuration for this salt-api instance does not seem to have " "any api properly configured." ) super().__attrs_post_init__() @classmethod def _configure( cls, factories_manager, daemon_id, root_dir=None, defaults=None, overrides=None, ): raise pytest.UsageError( "The salt-api daemon is not configurable. It uses the salt-master config that " "it's attached to." ) @classmethod def _get_verify_config_entries(cls, config): return [] @classmethod def load_config(cls, config_file, config): raise pytest.UsageError( "The salt-api daemon does not have it's own config file. It uses the salt-master config that " "it's attached to." ) def get_check_events(self): """ Return a list of tuples in the form of `(master_id, event_tag)` check against to ensure the daemon is running """ return [] pytest-salt-factories-0.907.0/src/saltfactories/daemons/container.py0000644000175000017500000005364014036250542026362 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE Container based factories """ import atexit import logging import os import attr from saltfactories import bases from saltfactories import CODE_ROOT_DIR from saltfactories.daemons import minion from saltfactories.exceptions import FactoryNotStarted from saltfactories.utils import format_callback_to_string from saltfactories.utils import ports from saltfactories.utils import random_string from saltfactories.utils import time from saltfactories.utils.processes import ProcessResult try: import docker from docker.errors import APIError HAS_DOCKER = True except ImportError: # pragma: no cover HAS_DOCKER = False class APIError(Exception): pass try: from requests.exceptions import ConnectionError as RequestsConnectionError HAS_REQUESTS = True except ImportError: # pragma: no cover HAS_REQUESTS = False class RequestsConnectionError(ConnectionError): pass try: import pywintypes PyWinTypesError = pywintypes.error # pragma: no cover except ImportError: class PyWinTypesError(Exception): pass log = logging.getLogger(__name__) @attr.s(kw_only=True) class Container(bases.Factory): image = attr.ib() name = attr.ib(default=None) check_ports = attr.ib(default=None) docker_client = attr.ib(repr=False, default=None) container_run_kwargs = attr.ib(repr=False, default=attr.Factory(dict)) container = attr.ib(init=False, default=None, repr=False) start_timeout = attr.ib(repr=False, default=30) max_start_attempts = attr.ib(repr=False, default=3) _before_start_callbacks = attr.ib(repr=False, hash=False, default=attr.Factory(list)) _before_terminate_callbacks = attr.ib(repr=False, hash=False, default=attr.Factory(list)) _after_start_callbacks = attr.ib(repr=False, hash=False, default=attr.Factory(list)) _after_terminate_callbacks = attr.ib(repr=False, hash=False, default=attr.Factory(list)) _terminate_result = attr.ib(repr=False, hash=False, init=False, default=None) def __attrs_post_init__(self): super().__attrs_post_init__() if self.name is None: self.name = random_string("factories-") if self.docker_client is None: if not HAS_DOCKER: raise RuntimeError("The docker python library was not found installed") if not HAS_REQUESTS: raise RuntimeError("The requests python library was not found installed") self.docker_client = docker.from_env() def before_start(self, callback, *args, **kwargs): """ Register a function callback to run before the container starts :param ~collections.abc.Callable callback: The function to call back :keyword args: The arguments to pass to the callback :keyword kwargs: The keyword arguments to pass to the callback """ self._before_start_callbacks.append((callback, args, kwargs)) def after_start(self, callback, *args, **kwargs): """ Register a function callback to run after the container starts :param ~collections.abc.Callable callback: The function to call back :keyword args: The arguments to pass to the callback :keyword kwargs: The keyword arguments to pass to the callback """ self._after_start_callbacks.append((callback, args, kwargs)) def before_terminate(self, callback, *args, **kwargs): """ Register a function callback to run before the container terminates :param ~collections.abc.Callable callback: The function to call back :keyword args: The arguments to pass to the callback :keyword kwargs: The keyword arguments to pass to the callback """ self._before_terminate_callbacks.append((callback, args, kwargs)) def after_terminate(self, callback, *args, **kwargs): """ Register a function callback to run after the container terminates :param ~collections.abc.Callable callback: The function to call back :keyword args: The arguments to pass to the callback :keyword kwargs: The keyword arguments to pass to the callback """ self._after_terminate_callbacks.append((callback, args, kwargs)) def start(self, *command, max_start_attempts=None, start_timeout=None): if self.is_running(): log.warning("%s is already running.", self) return True connectable = Container.client_connectable(self.docker_client) if connectable is not True: self.terminate() raise RuntimeError(connectable) self._terminate_result = None atexit.register(self.terminate) factory_started = False for callback, args, kwargs in self._before_start_callbacks: try: callback(*args, **kwargs) except Exception as exc: # pragma: no cover pylint: disable=broad-except log.info( "Exception raised when running %s: %s", format_callback_to_string(callback, args, kwargs), exc, exc_info=True, ) start_time = time.time() start_attempts = max_start_attempts or self.max_start_attempts current_attempt = 0 while current_attempt <= start_attempts: current_attempt += 1 if factory_started: break log.info("Starting %s. Attempt: %d of %d", self, current_attempt, start_attempts) current_start_time = time.time() start_running_timeout = current_start_time + (start_timeout or self.start_timeout) # Start the container self.container = self.docker_client.containers.run( self.image, name=self.name, detach=True, stdin_open=True, command=list(command) or None, **self.container_run_kwargs ) while time.time() <= start_running_timeout: # Don't know why, but if self.container wasn't previously in a running # state, and now it is, we have to re-set the self.container attribute # so that it gives valid status information self.container = self.docker_client.containers.get(self.name) if self.container.status != "running": time.sleep(0.25) continue self.container = self.docker_client.containers.get(self.name) logs = self.container.logs(stdout=True, stderr=True, stream=False) if isinstance(logs, bytes): stdout = logs.decode() stderr = None else: stdout = logs[0].decode() stderr = logs[1].decode() if stdout and stderr: log.info("Running Container Logs:\n%s\n%s", stdout, stderr) elif stdout: log.info("Running Container Logs:\n%s", stdout) # If we reached this far it means that we got the running status above, and # now that the container has started, run start checks try: if ( self.run_container_start_checks(current_start_time, start_running_timeout) is False ): time.sleep(0.5) continue except FactoryNotStarted: self.terminate() break log.info( "The %s factory is running after %d attempts. Took %1.2f seconds", self, current_attempt, time.time() - start_time, ) factory_started = True break else: # We reached start_running_timeout, re-try try: self.container.remove(force=True) self.container.wait() except docker.errors.NotFound: pass self.container = None else: # The factory failed to confirm it's running status self.terminate() if factory_started: for callback, args, kwargs in self._after_start_callbacks: try: callback(*args, **kwargs) except Exception as exc: # pragma: no cover pylint: disable=broad-except log.info( "Exception raised when running %s: %s", format_callback_to_string(callback, args, kwargs), exc, exc_info=True, ) # TODO: Add containers to the processes stats?! # if self.factories_manager and self.factories_manager.stats_processes is not None: # self.factories_manager.stats_processes[self.get_display_name()] = psutil.Process( # self.pid # ) return factory_started result = self.terminate() raise FactoryNotStarted( "The {} factory has failed to confirm running status after {} attempts, which " "took {:.2f} seconds({:.2f} seconds each)".format( self, current_attempt - 1, time.time() - start_time, start_timeout or self.start_timeout, ), stdout=result.stdout, stderr=result.stderr, exitcode=result.exitcode, ) def started(self, *command, max_start_attempts=None, start_timeout=None): """ Start the container and return it's instance so it can be used as a context manager """ self.start(*command, max_start_attempts=max_start_attempts, start_timeout=start_timeout) return self def terminate(self): if self._terminate_result is not None: # The factory is already terminated return self._terminate_result atexit.unregister(self.terminate) for callback, args, kwargs in self._before_terminate_callbacks: try: callback(*args, **kwargs) except Exception as exc: # pragma: no cover pylint: disable=broad-except log.info( "Exception raised when running %s: %s", format_callback_to_string(callback, args, kwargs), exc, exc_info=True, ) stdout = stderr = None try: if self.container is not None: container = self.docker_client.containers.get(self.name) logs = container.logs(stdout=True, stderr=True, stream=False) if isinstance(logs, bytes): stdout = logs.decode() else: stdout = logs[0].decode() stderr = logs[1].decode() if stdout and stderr: log.info("Stopped Container Logs:\n%s\n%s", stdout, stderr) elif stdout: log.info("Stopped Container Logs:\n%s", stdout) if container.status == "running": container.remove(force=True) container.wait() self.container = None except docker.errors.NotFound: pass finally: for callback, args, kwargs in self._after_terminate_callbacks: try: callback(*args, **kwargs) except Exception as exc: # pragma: no cover pylint: disable=broad-except log.info( "Exception raised when running %s: %s", format_callback_to_string(callback, args, kwargs), exc, exc_info=True, ) self._terminate_result = ProcessResult(exitcode=0, stdout=stdout, stderr=stderr) return self._terminate_result def get_check_ports(self): """ Return a list of ports to check against to ensure the daemon is running """ return self.check_ports or [] def is_running(self): if self.container is None: return False self.container = self.docker_client.containers.get(self.name) return self.container.status == "running" def run(self, *cmd, **kwargs): if len(cmd) == 1: cmd = cmd[0] log.info("%s is running %r ...", self, cmd) # We force dmux to True so that we always get back both stdout and stderr container = self.docker_client.containers.get(self.name) ret = container.exec_run(cmd, demux=True, **kwargs) exitcode = ret.exit_code stdout = stderr = None if ret.output: stdout, stderr = ret.output if stdout is not None: stdout = stdout.decode() if stderr is not None: stderr = stderr.decode() return ProcessResult(exitcode=exitcode, stdout=stdout, stderr=stderr, cmdline=cmd) @staticmethod def client_connectable(docker_client): try: if not docker_client.ping(): return "The docker client failed to get a ping response from the docker daemon" return True except (APIError, RequestsConnectionError, PyWinTypesError) as exc: return "The docker client failed to ping the docker server: {}".format(exc) def run_container_start_checks(self, started_at, timeout_at): checks_start_time = time.time() while time.time() <= timeout_at: if not self.is_running(): raise FactoryNotStarted("{} is no longer running".format(self)) if self._container_start_checks(): break else: log.error( "Failed to run container start checks after %1.2f seconds", time.time() - checks_start_time, ) return False check_ports = set(self.get_check_ports()) if not check_ports: return True while time.time() <= timeout_at: if not self.is_running(): raise FactoryNotStarted("{} is no longer running".format(self)) if not check_ports: break check_ports -= ports.get_connectable_ports(check_ports) if check_ports: time.sleep(0.5) else: log.error("Failed to check ports after %1.2f seconds", time.time() - checks_start_time) return False return True def _container_start_checks(self): return True def __enter__(self): 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, *_): self.terminate() @attr.s(kw_only=True) class SaltDaemon(bases.SaltDaemon, Container): def __attrs_post_init__(self): self.daemon_started = self.daemon_starting = False if self.python_executable is None: # Default to whatever is the default python in the container self.python_executable = "python" bases.SaltDaemon.__attrs_post_init__(self) Container.__attrs_post_init__(self) # There are some volumes which NEED to exist on the container so # that configs are in the right place and also our custom salt # plugins along with the custom scripts to start the daemons. root_dir = os.path.dirname(self.config["root_dir"]) config_dir = str(self.config_dir) scripts_dir = str(self.factories_manager.scripts_dir) volumes = { root_dir: {"bind": root_dir, "mode": "z"}, scripts_dir: {"bind": scripts_dir, "mode": "z"}, config_dir: {"bind": self.config_dir, "mode": "z"}, str(CODE_ROOT_DIR): {"bind": str(CODE_ROOT_DIR), "mode": "z"}, } if "volumes" not in self.container_run_kwargs: self.container_run_kwargs["volumes"] = {} self.container_run_kwargs["volumes"].update(volumes) self.container_run_kwargs.setdefault("hostname", self.name) self.container_run_kwargs.setdefault("auto_remove", True) def cmdline(self, *args): return ["docker", "exec", "-i", self.name] + super().cmdline(*args) def start(self, *extra_cli_arguments, max_start_attempts=None, start_timeout=None): # Start the container Container.start(self, max_start_attempts=max_start_attempts, start_timeout=start_timeout) self.daemon_starting = True # Now that the container is up, let's start the daemon self.daemon_started = bases.SaltDaemon.start( self, *extra_cli_arguments, max_start_attempts=max_start_attempts, start_timeout=start_timeout ) return self.daemon_started def terminate(self): self.daemon_started = self.daemon_starting = False ret = bases.SaltDaemon.terminate(self) Container.terminate(self) return ret def is_running(self): running = Container.is_running(self) if running is False: return running if self.daemon_starting or self.daemon_started: return bases.SaltDaemon.is_running(self) return running def get_check_ports(self): """ Return a list of ports to check against to ensure the daemon is running """ return Container.get_check_ports(self) + bases.SaltDaemon.get_check_ports(self) def before_start( self, callback, *args, on_container=False, **kwargs ): # pylint: disable=arguments-differ """ Register a function callback to run before the daemon starts :param ~collections.abc.Callable callback: The function to call back :keyword bool on_container: If true, the callback will be registered on the container and not the daemon. :keyword args: The arguments to pass to the callback :keyword kwargs: The keyword arguments to pass to the callback """ if on_container: Container.before_start(self, callback, *args, **kwargs) else: bases.SaltDaemon.before_start(self, callback, *args, **kwargs) def after_start( self, callback, *args, on_container=False, **kwargs ): # pylint: disable=arguments-differ """ Register a function callback to run after the daemon starts :param ~collections.abc.Callable callback: The function to call back :keyword bool on_container: If true, the callback will be registered on the container and not the daemon. :keyword args: The arguments to pass to the callback :keyword kwargs: The keyword arguments to pass to the callback """ if on_container: Container.after_start(self, callback, *args, **kwargs) else: bases.SaltDaemon.after_start(self, callback, *args, **kwargs) def before_terminate( self, callback, *args, on_container=False, **kwargs ): # pylint: disable=arguments-differ """ Register a function callback to run before the daemon terminates :param ~collections.abc.Callable callback: The function to call back :keyword bool on_container: If true, the callback will be registered on the container and not the daemon. :keyword args: The arguments to pass to the callback :keyword kwargs: The keyword arguments to pass to the callback """ if on_container: Container.before_terminate(self, callback, *args, **kwargs) else: bases.SaltDaemon.before_terminate(self, callback, *args, **kwargs) def after_terminate( self, callback, *args, on_container=False, **kwargs ): # pylint: disable=arguments-differ """ Register a function callback to run after the daemon terminates :param ~collections.abc.Callable callback: The function to call back :keyword bool on_container: If true, the callback will be registered on the container and not the daemon. :keyword args: The arguments to pass to the callback :keyword kwargs: The keyword arguments to pass to the callback """ if on_container: Container.after_terminate(self, callback, *args, **kwargs) else: bases.SaltDaemon.after_terminate(self, callback, *args, **kwargs) def started(self, *extra_cli_arguments, max_start_attempts=None, start_timeout=None): """ Start the daemon and return it's instance so it can be used as a context manager """ return bases.SaltDaemon.started( self, *extra_cli_arguments, max_start_attempts=max_start_attempts, start_timeout=start_timeout ) def get_check_events(self): """ Return a list of tuples in the form of `(master_id, event_tag)` check against to ensure the daemon is running """ raise NotImplementedError @attr.s(kw_only=True, slots=True) class SaltMinion(SaltDaemon, minion.SaltMinion): """ Salt minion daemon implementation running in a docker container """ def get_check_events(self): """ Return a list of tuples in the form of `(master_id, event_tag)` check against to ensure the daemon is running """ return minion.SaltMinion.get_check_events(self) def run_start_checks(self, started_at, timeout_at): return minion.SaltMinion.run_start_checks(self, started_at, timeout_at) pytest-salt-factories-0.907.0/src/saltfactories/daemons/master.py0000644000175000017500000005046614062623454025703 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE Salt Master Factory """ import copy import pathlib import shutil from functools import partial import attr import salt.config import salt.utils.dictupdate from saltfactories import cli from saltfactories import client from saltfactories.bases import SaltDaemon from saltfactories.utils import cli_scripts from saltfactories.utils import ports from saltfactories.utils import running_username from saltfactories.utils.tempfiles import SaltPillarTree from saltfactories.utils.tempfiles import SaltStateTree @attr.s(kw_only=True, slots=True) class SaltMaster(SaltDaemon): on_auth_event_callback = attr.ib(repr=False, default=None) state_tree = attr.ib(init=False, hash=False, repr=False) pillar_tree = attr.ib(init=False, hash=False, repr=False) def __attrs_post_init__(self): super().__attrs_post_init__() if self.config.get("open_mode", False) is False: # If the master is not configured to be in open mode, register an auth event callback # If we were passed an auth event callback, it needs to get this master as the first # argument if self.on_auth_event_callback: auth_event_callback = partial(self.on_auth_event_callback, self) else: auth_event_callback = self._on_auth_event self.before_start( self.event_listener.register_auth_event_handler, self.id, auth_event_callback ) self.after_terminate(self.event_listener.unregister_auth_event_handler, self.id) @state_tree.default def __setup_state_tree(self): if "file_roots" in self.config: return SaltStateTree(envs=copy.deepcopy(self.config.get("file_roots") or {})) @pillar_tree.default def __setup_pillar_tree(self): if "pillar_roots" in self.config: return SaltPillarTree(envs=copy.deepcopy(self.config.get("pillar_roots") or {})) @classmethod def default_config( cls, root_dir, master_id, defaults=None, overrides=None, order_masters=False, master_of_masters=None, system_install=False, ): if defaults is None: defaults = {} if overrides is None: overrides = {} else: overrides = overrides.copy() master_of_masters_id = None if master_of_masters: master_of_masters_id = master_of_masters.id overrides["syndic_master"] = master_of_masters.config["interface"] overrides["syndic_master_port"] = master_of_masters.config["ret_port"] # Match transport if not set defaults.setdefault("transport", master_of_masters.config["transport"]) if system_install is True: conf_dir = root_dir / "etc" / "salt" conf_dir.mkdir(parents=True, exist_ok=True) conf_file = str(conf_dir / "master") pki_dir = conf_dir / "pki" / "master" logs_dir = root_dir / "var" / "log" / "salt" logs_dir.mkdir(parents=True, exist_ok=True) pidfile_dir = root_dir / "var" / "run" state_tree_root = root_dir / "srv" / "salt" state_tree_root.mkdir(parents=True, exist_ok=True) pillar_tree_root = root_dir / "srv" / "pillar" pillar_tree_root.mkdir(parents=True, exist_ok=True) _defaults = { "id": master_id, "conf_file": conf_file, "root_dir": str(root_dir), "interface": "127.0.0.1", "publish_port": salt.config.DEFAULT_MASTER_OPTS["publish_port"], "ret_port": salt.config.DEFAULT_MASTER_OPTS["ret_port"], "tcp_master_pub_port": salt.config.DEFAULT_MASTER_OPTS["tcp_master_pub_port"], "tcp_master_pull_port": salt.config.DEFAULT_MASTER_OPTS["tcp_master_pull_port"], "tcp_master_publish_pull": salt.config.DEFAULT_MASTER_OPTS[ "tcp_master_publish_pull" ], "tcp_master_workers": salt.config.DEFAULT_MASTER_OPTS["tcp_master_workers"], "api_pidfile": str(pidfile_dir / "api.pid"), "pki_dir": str(pki_dir), "fileserver_backend": ["roots"], "log_file": str(logs_dir / "master.log"), "log_level_logfile": "debug", "api_logfile": str(logs_dir / "api.log"), "key_logfile": str(logs_dir / "key.log"), "log_fmt_console": "%(asctime)s,%(msecs)03.0f [%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", "log_fmt_logfile": "[%(asctime)s,%(msecs)03.0f][%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", "file_roots": { "base": [str(state_tree_root)], }, "pillar_roots": { "base": [str(pillar_tree_root)], }, "order_masters": order_masters, "max_open_files": 10240, "pytest-master": { "master-id": master_of_masters_id, "log": {"prefix": "{}(id={!r})".format(cls.__name__, master_id)}, }, } else: conf_dir = root_dir / "conf" conf_dir.mkdir(parents=True, exist_ok=True) conf_file = str(conf_dir / "master") state_tree_root = root_dir / "state-tree" state_tree_root.mkdir(exist_ok=True) state_tree_root_base = state_tree_root / "base" state_tree_root_base.mkdir(exist_ok=True) state_tree_root_prod = state_tree_root / "prod" state_tree_root_prod.mkdir(exist_ok=True) pillar_tree_root = root_dir / "pillar-tree" pillar_tree_root.mkdir(exist_ok=True) pillar_tree_root_base = pillar_tree_root / "base" pillar_tree_root_base.mkdir(exist_ok=True) pillar_tree_root_prod = pillar_tree_root / "prod" pillar_tree_root_prod.mkdir(exist_ok=True) _defaults = { "id": master_id, "conf_file": conf_file, "root_dir": str(root_dir), "interface": "127.0.0.1", "publish_port": ports.get_unused_localhost_port(), "ret_port": ports.get_unused_localhost_port(), "tcp_master_pub_port": ports.get_unused_localhost_port(), "tcp_master_pull_port": ports.get_unused_localhost_port(), "tcp_master_publish_pull": ports.get_unused_localhost_port(), "tcp_master_workers": ports.get_unused_localhost_port(), "pidfile": "run/master.pid", "api_pidfile": "run/api.pid", "pki_dir": "pki", "cachedir": "cache", "sock_dir": "run/master", "fileserver_list_cache_time": 0, "fileserver_backend": ["roots"], "pillar_opts": False, "peer": {".*": ["test.*"]}, "log_file": "logs/master.log", "log_level_logfile": "debug", "api_logfile": "logs/api.log", "key_logfile": "logs/key.log", "token_dir": "tokens", "token_file": str(root_dir / "ksfjhdgiuebfgnkefvsikhfjdgvkjahcsidk"), "file_buffer_size": 8192, "log_fmt_console": "%(asctime)s,%(msecs)03.0f [%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", "log_fmt_logfile": "[%(asctime)s,%(msecs)03.0f][%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", "file_roots": { "base": [str(state_tree_root_base)], "prod": [str(state_tree_root_prod)], }, "pillar_roots": { "base": [str(pillar_tree_root_base)], "prod": [str(pillar_tree_root_prod)], }, "order_masters": order_masters, "max_open_files": 10240, "enable_legacy_startup_events": False, "pytest-master": { "master-id": master_of_masters_id, "log": {"prefix": "{}(id={!r})".format(cls.__name__, master_id)}, }, } # Merge in the initial default options with the internal _defaults salt.utils.dictupdate.update(defaults, _defaults, merge_lists=True) if overrides: # Merge in the default options with the master_overrides salt.utils.dictupdate.update(defaults, overrides, merge_lists=True) return defaults @classmethod def _configure( # pylint: disable=arguments-differ cls, factories_manager, daemon_id, master_of_masters=None, root_dir=None, defaults=None, overrides=None, order_masters=False, ): return cls.default_config( root_dir, daemon_id, master_of_masters=master_of_masters, defaults=defaults, overrides=overrides, order_masters=order_masters, system_install=factories_manager.system_install, ) @classmethod def _get_verify_config_entries(cls, config): # verify env to make sure all required directories are created and have the # right permissions pki_dir = pathlib.Path(config["pki_dir"]) verify_env_entries = [ str(pki_dir / "minions"), str(pki_dir / "minions_pre"), str(pki_dir / "minions_rejected"), str(pki_dir / "accepted"), str(pki_dir / "rejected"), str(pki_dir / "pending"), str(pathlib.Path(config["log_file"]).parent), str(pathlib.Path(config["cachedir"]) / "proc"), str(pathlib.Path(config["cachedir"]) / "jobs"), # config['extension_modules'], config["sock_dir"], ] verify_env_entries.extend(config["file_roots"]["base"]) if "prod" in config["file_roots"]: verify_env_entries.extend(config["file_roots"]["prod"]) verify_env_entries.extend(config["pillar_roots"]["base"]) if "prod" in config["pillar_roots"]: verify_env_entries.extend(config["pillar_roots"]["prod"]) return verify_env_entries @classmethod def load_config(cls, config_file, config): return salt.config.master_config(config_file) def _on_auth_event(self, payload): if self.config["open_mode"]: return minion_id = payload["id"] keystate = payload["act"] salt_key_cli = self.salt_key_cli() if keystate == "pend": ret = salt_key_cli.run("--yes", "--accept", minion_id) assert ret.exitcode == 0 def get_check_events(self): """ Return a list of tuples in the form of `(master_id, event_tag)` check against to ensure the daemon is running """ yield self.config["id"], "salt/master/{id}/start".format(**self.config) # The following methods just route the calls to the right method in the factories manager # while making sure the CLI tools are referring to this daemon def salt_master_daemon(self, master_id, **kwargs): """ This method will configure a master under a master-of-masters. Please see the documentation in :py:class:`~saltfactories.manager.FactoriesManager.salt_master_daemon` """ return self.factories_manager.salt_master_daemon( master_id, master_of_masters=self, **kwargs ) def salt_minion_daemon(self, minion_id, **kwargs): """ Please see the documentation in :py:class:`~saltfactories.manager.FactoriesManager.configure_salt_minion` """ return self.factories_manager.salt_minion_daemon(minion_id, master=self, **kwargs) def salt_proxy_minion_daemon(self, minion_id, **kwargs): """ Please see the documentation in :py:class:`~saltfactories.manager.FactoriesManager.salt_proxy_minion_daemon` """ return self.factories_manager.salt_proxy_minion_daemon(minion_id, master=self, **kwargs) def salt_api_daemon(self, **kwargs): """ Please see the documentation in :py:class:`~saltfactories.manager.FactoriesManager.salt_api_daemon` """ return self.factories_manager.salt_api_daemon(self, **kwargs) def salt_syndic_daemon(self, syndic_id, **kwargs): """ Please see the documentation in :py:class:`~saltfactories.manager.FactoriesManager.salt_syndic_daemon` """ return self.factories_manager.salt_syndic_daemon( syndic_id, master_of_masters=self, **kwargs ) def salt_cloud_cli( self, defaults=None, overrides=None, factory_class=cli.cloud.SaltCloud, **factory_class_kwargs ): """ Return a salt-cloud CLI instance Args: defaults(dict): A dictionary of default configuration to use when configuring the minion overrides(dict): A dictionary of configuration overrides to use when configuring the minion Returns: :py:class:`~saltfactories.cli.cloud.SaltCloud`: The salt-cloud CLI script process class instance """ root_dir = pathlib.Path(self.config["root_dir"]) config = factory_class.configure( self, self.id, root_dir=root_dir, defaults=defaults, overrides=overrides, ) self.factories_manager.final_cloud_config_tweaks(config) config = factory_class.write_config(config) if self.system_install is False: script_path = cli_scripts.generate_script( self.factories_manager.scripts_dir, "salt-cloud", code_dir=self.factories_manager.code_dir, inject_coverage=self.factories_manager.inject_coverage, inject_sitecustomize=self.factories_manager.inject_sitecustomize, ) else: script_path = shutil.which("salt-cloud") return factory_class( script_name=script_path, config=config, system_install=self.factories_manager.system_install, **factory_class_kwargs ) def salt_cli(self, factory_class=cli.salt.Salt, **factory_class_kwargs): """ Return a `salt` CLI process for this master instance """ if self.system_install is False: script_path = cli_scripts.generate_script( self.factories_manager.scripts_dir, "salt", code_dir=self.factories_manager.code_dir, inject_coverage=self.factories_manager.inject_coverage, inject_sitecustomize=self.factories_manager.inject_sitecustomize, ) else: script_path = shutil.which("salt") return factory_class( script_name=script_path, config=self.config.copy(), system_install=self.factories_manager.system_install, **factory_class_kwargs ) def salt_cp_cli(self, factory_class=cli.cp.SaltCp, **factory_class_kwargs): """ Return a `salt-cp` CLI process for this master instance """ if self.system_install is False: script_path = cli_scripts.generate_script( self.factories_manager.scripts_dir, "salt-cp", code_dir=self.factories_manager.code_dir, inject_coverage=self.factories_manager.inject_coverage, inject_sitecustomize=self.factories_manager.inject_sitecustomize, ) else: script_path = shutil.which("salt-cp") return factory_class( script_name=script_path, config=self.config.copy(), system_install=self.factories_manager.system_install, **factory_class_kwargs ) def salt_key_cli(self, factory_class=cli.key.SaltKey, **factory_class_kwargs): """ Return a `salt-key` CLI process for this master instance """ if self.system_install is False: script_path = cli_scripts.generate_script( self.factories_manager.scripts_dir, "salt-key", code_dir=self.factories_manager.code_dir, inject_coverage=self.factories_manager.inject_coverage, inject_sitecustomize=self.factories_manager.inject_sitecustomize, ) else: script_path = shutil.which("salt-key") return factory_class( script_name=script_path, config=self.config.copy(), system_install=self.factories_manager.system_install, **factory_class_kwargs ) def salt_run_cli(self, factory_class=cli.run.SaltRun, **factory_class_kwargs): """ Return a `salt-run` CLI process for this master instance """ if self.system_install is False: script_path = cli_scripts.generate_script( self.factories_manager.scripts_dir, "salt-run", code_dir=self.factories_manager.code_dir, inject_coverage=self.factories_manager.inject_coverage, inject_sitecustomize=self.factories_manager.inject_sitecustomize, ) else: script_path = shutil.which("salt-run") return factory_class( script_name=script_path, config=self.config.copy(), system_install=self.factories_manager.system_install, **factory_class_kwargs ) def salt_spm_cli(self, factory_class=cli.spm.Spm, **factory_class_kwargs): """ Return a `spm` CLI process for this master instance """ if self.system_install is False: script_path = cli_scripts.generate_script( self.factories_manager.scripts_dir, "spm", code_dir=self.factories_manager.code_dir, inject_coverage=self.factories_manager.inject_coverage, inject_sitecustomize=self.factories_manager.inject_sitecustomize, ) else: script_path = shutil.which("spm") return factory_class( script_name=script_path, config=self.config.copy(), system_install=self.factories_manager.system_install, **factory_class_kwargs ) def salt_ssh_cli( self, factory_class=cli.ssh.SaltSsh, roster_file=None, target_host=None, client_key=None, ssh_user=None, **factory_class_kwargs ): """ Return a `salt-ssh` CLI process for this master instance Args: roster_file(str): The roster file to use target_host(str): The target host address to connect to client_key(str): The path to the private ssh key to use to connect ssh_user(str): The remote username to connect as """ if self.system_install is False: script_path = cli_scripts.generate_script( self.factories_manager.scripts_dir, "salt-ssh", code_dir=self.factories_manager.code_dir, inject_coverage=self.factories_manager.inject_coverage, inject_sitecustomize=self.factories_manager.inject_sitecustomize, ) else: script_path = shutil.which("salt-ssh") return factory_class( script_name=script_path, config=self.config.copy(), roster_file=roster_file, target_host=target_host, client_key=client_key, ssh_user=ssh_user or running_username(), system_install=self.factories_manager.system_install, **factory_class_kwargs ) def salt_client( self, functions_known_to_return_none=None, factory_class=client.LocalClient, ): """ Return a local salt client object """ return factory_class( master_config=self.config.copy(), functions_known_to_return_none=functions_known_to_return_none, ) pytest-salt-factories-0.907.0/src/saltfactories/daemons/minion.py0000644000175000017500000002333714036250542025671 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE Salt Minion Factory """ import copy import logging import pathlib import shutil import sys import attr import salt.config import salt.utils.dictupdate from saltfactories import cli from saltfactories.bases import SaltDaemon from saltfactories.utils import cli_scripts from saltfactories.utils import ports from saltfactories.utils.tempfiles import SaltPillarTree from saltfactories.utils.tempfiles import SaltStateTree log = logging.getLogger(__name__) @attr.s(kw_only=True, slots=True) class SaltMinion(SaltDaemon): state_tree = attr.ib(init=False, hash=False, repr=False) pillar_tree = attr.ib(init=False, hash=False, repr=False) @state_tree.default def __setup_state_tree(self): if "file_roots" in self.config: return SaltStateTree(envs=copy.deepcopy(self.config.get("file_roots") or {})) @pillar_tree.default def __setup_pillar_tree(self): if "pillar_roots" in self.config: return SaltPillarTree(envs=copy.deepcopy(self.config.get("pillar_roots") or {})) @classmethod def default_config( cls, root_dir, minion_id, defaults=None, overrides=None, master=None, system_install=False, ): if defaults is None: defaults = {} master_id = master_port = None if master is not None: master_id = master.id master_port = master.config["ret_port"] # Match transport if not set defaults.setdefault("transport", master.config["transport"]) if system_install is True: conf_dir = root_dir / "etc" / "salt" conf_dir.mkdir(parents=True, exist_ok=True) conf_file = str(conf_dir / "minion") pki_dir = conf_dir / "pki" / "minion" logs_dir = root_dir / "var" / "log" / "salt" logs_dir.mkdir(parents=True, exist_ok=True) state_tree_root = root_dir / "srv" / "salt" state_tree_root.mkdir(parents=True, exist_ok=True) pillar_tree_root = root_dir / "srv" / "pillar" pillar_tree_root.mkdir(parents=True, exist_ok=True) _defaults = { "id": master_id, "conf_file": conf_file, "root_dir": str(root_dir), "interface": "127.0.0.1", "master": "127.0.0.1", "master_port": master_port or salt.config.DEFAULT_MINION_OPTS["master_port"], "tcp_pub_port": salt.config.DEFAULT_MINION_OPTS["tcp_pub_port"], "tcp_pull_port": salt.config.DEFAULT_MINION_OPTS["tcp_pull_port"], "pki_dir": str(pki_dir), "log_file": str(logs_dir / "minion.log"), "log_level_logfile": "debug", "log_fmt_console": "%(asctime)s,%(msecs)03.0f [%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", "log_fmt_logfile": "[%(asctime)s,%(msecs)03.0f][%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", "file_roots": { "base": [str(state_tree_root)], }, "pillar_roots": { "base": [str(pillar_tree_root)], }, "pytest-minion": { "master-id": master_id, "log": {"prefix": "{}(id={!r})".format(cls.__name__, minion_id)}, }, } else: conf_dir = root_dir / "conf" conf_dir.mkdir(parents=True, exist_ok=True) conf_file = str(conf_dir / "minion") state_tree_root = root_dir / "state-tree" state_tree_root.mkdir(exist_ok=True) state_tree_root_base = state_tree_root / "base" state_tree_root_base.mkdir(exist_ok=True) state_tree_root_prod = state_tree_root / "prod" state_tree_root_prod.mkdir(exist_ok=True) pillar_tree_root = root_dir / "pillar-tree" pillar_tree_root.mkdir(exist_ok=True) pillar_tree_root_base = pillar_tree_root / "base" pillar_tree_root_base.mkdir(exist_ok=True) pillar_tree_root_prod = pillar_tree_root / "prod" pillar_tree_root_prod.mkdir(exist_ok=True) _defaults = { "id": minion_id, "conf_file": conf_file, "root_dir": str(root_dir), "interface": "127.0.0.1", "master": "127.0.0.1", "master_port": master_port or ports.get_unused_localhost_port(), "tcp_pub_port": ports.get_unused_localhost_port(), "tcp_pull_port": ports.get_unused_localhost_port(), "pidfile": "run/minion.pid", "pki_dir": "pki", "cachedir": "cache", "sock_dir": "run/minion", "log_file": "logs/minion.log", "log_level_logfile": "debug", "loop_interval": 0.05, "log_fmt_console": "%(asctime)s,%(msecs)03.0f [%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", "log_fmt_logfile": "[%(asctime)s,%(msecs)03.0f][%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", "file_roots": { "base": [str(state_tree_root_base)], "prod": [str(state_tree_root_prod)], }, "pillar_roots": { "base": [str(pillar_tree_root_base)], "prod": [str(pillar_tree_root_prod)], }, "enable_legacy_startup_events": False, "acceptance_wait_time": 0.5, "acceptance_wait_time_max": 5, "pytest-minion": { "master-id": master_id, "log": {"prefix": "{}(id={!r})".format(cls.__name__, minion_id)}, }, } # Merge in the initial default options with the internal _defaults salt.utils.dictupdate.update(defaults, _defaults, merge_lists=True) if overrides: # Merge in the default options with the minion_overrides salt.utils.dictupdate.update(defaults, overrides, merge_lists=True) return defaults @classmethod def _configure( # pylint: disable=arguments-differ cls, factories_manager, daemon_id, root_dir=None, defaults=None, overrides=None, master=None, ): return cls.default_config( root_dir, daemon_id, defaults=defaults, overrides=overrides, master=master, system_install=factories_manager.system_install, ) @classmethod def _get_verify_config_entries(cls, config): # verify env to make sure all required directories are created and have the # right permissions pki_dir = pathlib.Path(config["pki_dir"]) verify_env_entries = [ str(pki_dir / "minions"), str(pki_dir / "minions_pre"), str(pki_dir / "minions_rejected"), str(pki_dir / "accepted"), str(pki_dir / "rejected"), str(pki_dir / "pending"), str(pathlib.Path(config["log_file"]).parent), str(pathlib.Path(config["cachedir"]) / "proc"), # config['extension_modules'], config["sock_dir"], ] verify_env_entries.extend(config["file_roots"]["base"]) if "prod" in config["file_roots"]: verify_env_entries.extend(config["file_roots"]["prod"]) verify_env_entries.extend(config["pillar_roots"]["base"]) if "prod" in config["pillar_roots"]: verify_env_entries.extend(config["pillar_roots"]["prod"]) return verify_env_entries @classmethod def load_config(cls, config_file, config): return salt.config.minion_config(config_file, minion_id=config["id"], cache_minion_id=True) def get_script_args(self): args = super().get_script_args() if sys.platform.startswith("win") is False: args.append("--disable-keepalive") return args def get_check_events(self): """ Return a list of tuples in the form of `(master_id, event_tag)` check against to ensure the daemon is running """ pytest_config = self.config["pytest-{}".format(self.config["__role"])] if not pytest_config.get("master-id"): log.warning( "Will not be able to check for start events for %s since it's missing the 'master-id' key " "in the 'pytest-%s' dictionary, or it's value is None.", self, self.config["__role"], ) else: yield pytest_config["master-id"], "salt/{role}/{id}/start".format( role=self.config["__role"], id=self.id ) def salt_call_cli(self, factory_class=cli.call.SaltCall, **factory_class_kwargs): """ Return a `salt-call` CLI process for this minion instance """ if self.system_install is False: script_path = cli_scripts.generate_script( self.factories_manager.scripts_dir, "salt-call", code_dir=self.factories_manager.code_dir, inject_coverage=self.factories_manager.inject_coverage, inject_sitecustomize=self.factories_manager.inject_sitecustomize, ) else: script_path = shutil.which("salt-call") return factory_class( script_name=script_path, config=self.config.copy(), system_install=self.factories_manager.system_install, **factory_class_kwargs ) pytest-salt-factories-0.907.0/src/saltfactories/daemons/proxy.py0000644000175000017500000002476014036250542025562 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE Salt Proxy Minion Factory """ import copy import logging import pathlib import shutil import sys import attr import salt.config import salt.utils.dictupdate from saltfactories import cli from saltfactories.bases import SaltDaemon from saltfactories.bases import SystemdSaltDaemonImpl from saltfactories.utils import cli_scripts from saltfactories.utils import ports from saltfactories.utils.tempfiles import SaltPillarTree from saltfactories.utils.tempfiles import SaltStateTree log = logging.getLogger(__name__) class SystemdSaltProxyImpl(SystemdSaltDaemonImpl): def get_service_name(self): if self._service_name is None: self._service_name = "{}@{}".format(super().get_service_name(), self.factory.id) return self._service_name @attr.s(kw_only=True, slots=True) class SaltProxyMinion(SaltDaemon): include_proxyid_cli_flag = attr.ib(default=True, repr=False) state_tree = attr.ib(init=False, hash=False, repr=False) pillar_tree = attr.ib(init=False, hash=False, repr=False) def _get_impl_class(self): if self.system_install: return SystemdSaltProxyImpl return super()._get_impl_class() @state_tree.default def __setup_state_tree(self): if "file_roots" in self.config: return SaltStateTree(envs=copy.deepcopy(self.config.get("file_roots") or {})) @pillar_tree.default def __setup_pillar_tree(self): if "pillar_roots" in self.config: return SaltPillarTree(envs=copy.deepcopy(self.config.get("pillar_roots") or {})) @classmethod def default_config( cls, root_dir, proxy_minion_id, defaults=None, overrides=None, master=None, system_install=False, ): if defaults is None: defaults = {} master_id = master_port = None if master is not None: master_id = master.id master_port = master.config["ret_port"] # Match transport if not set defaults.setdefault("transport", master.config["transport"]) if system_install is True: conf_dir = root_dir / "etc" / "salt" conf_dir.mkdir(parents=True, exist_ok=True) conf_file = str(conf_dir / "proxy") pki_dir = conf_dir / "pki" / "minion" logs_dir = root_dir / "var" / "log" / "salt" logs_dir.mkdir(parents=True, exist_ok=True) state_tree_root = root_dir / "srv" / "salt" state_tree_root.mkdir(parents=True, exist_ok=True) pillar_tree_root = root_dir / "srv" / "pillar" pillar_tree_root.mkdir(parents=True, exist_ok=True) _defaults = { "id": proxy_minion_id, "conf_file": conf_file, "root_dir": str(root_dir), "interface": "127.0.0.1", "master": "127.0.0.1", "master_port": master_port or salt.config.DEFAULT_MINION_OPTS["master_port"], "tcp_pub_port": salt.config.DEFAULT_MINION_OPTS["tcp_pub_port"], "tcp_pull_port": salt.config.DEFAULT_MINION_OPTS["tcp_pull_port"], "pki_dir": str(pki_dir), "log_file": str(logs_dir / "proxy.log"), "log_level_logfile": "debug", "loop_interval": 0.05, "log_fmt_console": "%(asctime)s,%(msecs)03.0f [%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", "log_fmt_logfile": "[%(asctime)s,%(msecs)03.0f][%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", "file_roots": { "base": [str(state_tree_root)], }, "pillar_roots": { "base": [str(pillar_tree_root)], }, "proxy": {"proxytype": "dummy"}, "pytest-minion": { "master-id": master_id, "log": {"prefix": "{}(id={!r})".format(cls.__name__, proxy_minion_id)}, }, } else: conf_dir = root_dir / "conf" conf_dir.mkdir(parents=True, exist_ok=True) conf_file = str(conf_dir / "proxy") state_tree_root = root_dir / "state-tree" state_tree_root.mkdir(exist_ok=True) state_tree_root_base = state_tree_root / "base" state_tree_root_base.mkdir(exist_ok=True) state_tree_root_prod = state_tree_root / "prod" state_tree_root_prod.mkdir(exist_ok=True) pillar_tree_root = root_dir / "pillar-tree" pillar_tree_root.mkdir(exist_ok=True) pillar_tree_root_base = pillar_tree_root / "base" pillar_tree_root_base.mkdir(exist_ok=True) pillar_tree_root_prod = pillar_tree_root / "prod" pillar_tree_root_prod.mkdir(exist_ok=True) _defaults = { "id": proxy_minion_id, "conf_file": conf_file, "root_dir": str(root_dir), "interface": "127.0.0.1", "master": "127.0.0.1", "master_port": master_port or ports.get_unused_localhost_port(), "tcp_pub_port": ports.get_unused_localhost_port(), "tcp_pull_port": ports.get_unused_localhost_port(), "pidfile": "run/proxy.pid", "pki_dir": "pki", "cachedir": "cache", "sock_dir": "run/proxy", "log_file": "logs/proxy.log", "log_level_logfile": "debug", "loop_interval": 0.05, "log_fmt_console": "%(asctime)s,%(msecs)03.0f [%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", "log_fmt_logfile": "[%(asctime)s,%(msecs)03.0f][%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", "file_roots": { "base": [str(state_tree_root_base)], "prod": [str(state_tree_root_prod)], }, "pillar_roots": { "base": [str(pillar_tree_root_base)], "prod": [str(pillar_tree_root_prod)], }, "proxy": {"proxytype": "dummy"}, "enable_legacy_startup_events": False, "acceptance_wait_time": 0.5, "acceptance_wait_time_max": 5, "pytest-minion": { "master-id": master_id, "log": {"prefix": "{}(id={!r})".format(cls.__name__, proxy_minion_id)}, }, } # Merge in the initial default options with the internal _defaults salt.utils.dictupdate.update(defaults, _defaults, merge_lists=True) if overrides: # Merge in the default options with the proxy_overrides salt.utils.dictupdate.update(defaults, overrides, merge_lists=True) return defaults @classmethod def _configure( # pylint: disable=arguments-differ cls, factories_manager, daemon_id, root_dir=None, defaults=None, overrides=None, master=None, ): return cls.default_config( root_dir, daemon_id, defaults=defaults, overrides=overrides, master=master, system_install=factories_manager.system_install, ) @classmethod def _get_verify_config_entries(cls, config): # verify env to make sure all required directories are created and have the # right permissions verify_env_entries = [ str(pathlib.Path(config["log_file"]).parent), # config['extension_modules'], config["sock_dir"], ] verify_env_entries.extend(config["file_roots"]["base"]) if "prod" in config["file_roots"]: verify_env_entries.extend(config["file_roots"]["prod"]) verify_env_entries.extend(config["pillar_roots"]["base"]) if "prod" in config["pillar_roots"]: verify_env_entries.extend(config["pillar_roots"]["prod"]) return verify_env_entries @classmethod def load_config(cls, config_file, config): return salt.config.proxy_config(config_file, minion_id=config["id"], cache_minion_id=True) def get_base_script_args(self): script_args = super().get_base_script_args() if sys.platform.startswith("win") is False: script_args.append("--disable-keepalive") return script_args def cmdline(self, *args): if self.include_proxyid_cli_flag is False: return super().cmdline(*args) _args = [] for arg in args: if arg.startswith("--proxyid"): break else: _args.append("--proxyid={}".format(self.id)) return super().cmdline(*(_args + list(args))) def get_check_events(self): """ Return a list of tuples in the form of `(master_id, event_tag)` check against to ensure the daemon is running """ pytest_config = self.config["pytest-{}".format(self.config["__role"])] if not pytest_config.get("master-id"): log.warning( "Will not be able to check for start events for %s since it's missing the 'master-id' key " "in the 'pytest-%s' dictionary, or it's value is None.", self, self.config["__role"], ) else: yield pytest_config["master-id"], "salt/{role}/{id}/start".format( role=self.config["__role"], id=self.id ) def salt_call_cli(self, factory_class=cli.call.SaltCall, **factory_class_kwargs): """ Return a `salt-call` CLI process for this minion instance """ if self.system_install is False: script_path = cli_scripts.generate_script( self.factories_manager.scripts_dir, "salt-call", code_dir=self.factories_manager.code_dir, inject_coverage=self.factories_manager.inject_coverage, inject_sitecustomize=self.factories_manager.inject_sitecustomize, ) else: script_path = shutil.which("salt-call") return factory_class( script_name=script_path, config=self.config.copy(), base_script_args=["--proxyid={}".format(self.id)], system_install=self.factories_manager.system_install, **factory_class_kwargs ) pytest-salt-factories-0.907.0/src/saltfactories/daemons/sshd.py0000644000175000017500000001657114036250542025343 0ustar vampasvampas00000000000000""" SSHD daemon factory implementation """ import logging import pathlib import shutil import subprocess from datetime import datetime import attr from saltfactories.bases import Daemon from saltfactories.exceptions import FactoryFailure from saltfactories.utils import ports from saltfactories.utils import running_username from saltfactories.utils import socket log = logging.getLogger(__name__) @attr.s(kw_only=True, slots=True) class Sshd(Daemon): config_dir = attr.ib() listen_address = attr.ib(default=None) listen_port = attr.ib(default=None) authorized_keys = attr.ib(default=None) sshd_config_dict = attr.ib(default=None, repr=False) client_key = attr.ib(default=None, init=False, repr=False) sshd_config = attr.ib(default=None, init=False) def __attrs_post_init__(self): if self.authorized_keys is None: self.authorized_keys = [] if self.sshd_config_dict is None: self.sshd_config_dict = {} if self.listen_address is None: self.listen_address = "127.0.0.1" if self.listen_port is None: self.listen_port = ports.get_unused_localhost_port() self.check_ports = [self.listen_port] if isinstance(self.config_dir, str): self.config_dir = pathlib.Path(self.config_dir) elif not isinstance(self.config_dir, pathlib.Path): # A py local path? self.config_dir = pathlib.Path(self.config_dir.strpath) self.config_dir.chmod(0o0700) authorized_keys_file = self.config_dir / "authorized_keys" # Let's generate the client key self.client_key = self._generate_client_ecdsa_key() with open("{}.pub".format(self.client_key)) as rfh: pubkey = rfh.read().strip() log.debug("SSH client pub key: %r", pubkey) self.authorized_keys.append(pubkey) # Write the authorized pub keys to file with open(str(authorized_keys_file), "w") as wfh: wfh.write("\n".join(self.authorized_keys)) authorized_keys_file.chmod(0o0600) with open(str(authorized_keys_file)) as rfh: log.debug("AuthorizedKeysFile contents:\n%s", rfh.read()) _default_config = { "ListenAddress": self.listen_address, "PermitRootLogin": "yes" if running_username() == "root" else "no", "ChallengeResponseAuthentication": "no", "PasswordAuthentication": "no", "PubkeyAuthentication": "yes", "PrintMotd": "no", "PidFile": self.config_dir / "sshd.pid", "AuthorizedKeysFile": authorized_keys_file, } if self.sshd_config_dict: _default_config.update(self.sshd_config_dict) self.sshd_config = _default_config self._write_config() super().__attrs_post_init__() def get_base_script_args(self): """ Returns any additional arguments to pass to the CLI script """ return ["-D", "-e", "-f", str(self.config_dir / "sshd_config"), "-p", str(self.listen_port)] def _write_config(self): sshd_config_file = self.config_dir / "sshd_config" if not sshd_config_file.exists(): # Let's write a default config file config_lines = [] for key, value in self.sshd_config.items(): if isinstance(value, list): for item in value: config_lines.append("{} {}\n".format(key, item)) continue config_lines.append("{} {}\n".format(key, value)) # Let's generate the host keys self._generate_server_dsa_key() self._generate_server_ecdsa_key() self._generate_server_ed25519_key() for host_key in pathlib.Path(self.config_dir).glob("ssh_host_*_key"): config_lines.append("HostKey {}\n".format(host_key)) with open(str(sshd_config_file), "w") as wfh: wfh.write("".join(sorted(config_lines))) sshd_config_file.chmod(0o0600) with open(str(sshd_config_file)) as wfh: log.debug( "Wrote to configuration file %s. Configuration:\n%s", sshd_config_file, wfh.read(), ) def _generate_client_ecdsa_key(self): key_filename = "client_key" key_path_prv = self.config_dir / key_filename key_path_pub = self.config_dir / "{}.pub".format(key_filename) if key_path_prv.exists() and key_path_pub.exists(): return key_path_prv self._ssh_keygen(key_filename, "ecdsa", "521") for key_path in (key_path_prv, key_path_pub): key_path.chmod(0o0400) return key_path_prv def _generate_server_dsa_key(self): key_filename = "ssh_host_dsa_key" key_path_prv = self.config_dir / key_filename key_path_pub = self.config_dir / "{}.pub".format(key_filename) if key_path_prv.exists() and key_path_pub.exists(): return key_path_prv self._ssh_keygen(key_filename, "dsa", "1024") for key_path in (key_path_prv, key_path_pub): key_path.chmod(0o0400) return key_path_prv def _generate_server_ecdsa_key(self): key_filename = "ssh_host_ecdsa_key" key_path_prv = self.config_dir / key_filename key_path_pub = self.config_dir / "{}.pub".format(key_filename) if key_path_prv.exists() and key_path_pub.exists(): return key_path_prv self._ssh_keygen(key_filename, "ecdsa", "521") for key_path in (key_path_prv, key_path_pub): key_path.chmod(0o0400) return key_path_prv def _generate_server_ed25519_key(self): key_filename = "ssh_host_ed25519_key" key_path_prv = self.config_dir / key_filename key_path_pub = self.config_dir / "{}.pub".format(key_filename) if key_path_prv.exists() and key_path_pub.exists(): return key_path_prv self._ssh_keygen(key_filename, "ed25519", "521") for key_path in (key_path_prv, key_path_pub): key_path.chmod(0o0400) return key_path_prv def _ssh_keygen(self, key_filename, key_type, bits, comment=None): try: ssh_keygen = self._ssh_keygen_path except AttributeError: ssh_keygen = self._ssh_keygen_path = shutil.which("ssh-keygen") if comment is None: comment = "{user}@{host}-{date}".format( user=running_username(), host=socket.gethostname(), date=datetime.utcnow().strftime("%Y-%m-%d"), ) cmdline = [ ssh_keygen, "-t", key_type, "-b", bits, "-C", comment, "-f", key_filename, "-P", "", ] try: subprocess.run( cmdline, cwd=str(self.config_dir), check=True, universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) except subprocess.CalledProcessError as exc: raise FactoryFailure( "Failed to generate ssh key.", cmdline=exc.args, stdout=exc.stdout, stderr=exc.stderr, exitcode=exc.returncode, ) from exc pytest-salt-factories-0.907.0/src/saltfactories/daemons/syndic.py0000644000175000017500000001267114036250542025670 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE Salt Syndic Factory """ import logging import pathlib import attr import salt.config import salt.utils.dictupdate from saltfactories.bases import SaltDaemon from saltfactories.utils import ports log = logging.getLogger(__name__) @attr.s(kw_only=True, slots=True) class SaltSyndic(SaltDaemon): master = attr.ib(repr=False, hash=False) minion = attr.ib(repr=False, hash=False) @classmethod def default_config( cls, root_dir, syndic_id, defaults=None, overrides=None, master_of_masters=None, system_install=False, ): if defaults is None: defaults = {} if overrides is None: overrides = {} master_of_masters_id = syndic_master_port = None if master_of_masters: master_of_masters_id = master_of_masters.id syndic_master_port = master_of_masters.config["ret_port"] # Match transport if not set defaults.setdefault("transport", master_of_masters.config["transport"]) if system_install is True: conf_dir = root_dir / "etc" / "salt" conf_dir.mkdir(parents=True, exist_ok=True) conf_d_dir = conf_dir / "master.d" conf_d_dir.mkdir(exist_ok=True) conf_file = str(conf_d_dir / "syndic.conf") pidfile_dir = root_dir / "var" / "run" logs_dir = root_dir / "var" / "log" / "salt" logs_dir.mkdir(parents=True, exist_ok=True) _defaults = { "id": syndic_id, "master_id": syndic_id, "conf_file": conf_file, "root_dir": str(root_dir), "syndic_master": "127.0.0.1", "syndic_master_port": syndic_master_port or salt.config.DEFAULT_MASTER_OPTS["ret_port"], "syndic_pidfile": str(pidfile_dir / "syndic.pid"), "syndic_log_file": str(logs_dir / "syndic.log"), "syndic_log_level_logfile": "debug", "pytest-syndic": { "master-id": master_of_masters_id, "log": {"prefix": "{}(id={!r})".format(cls.__name__, syndic_id)}, }, } else: conf_dir = root_dir / "conf" conf_dir.mkdir(parents=True, exist_ok=True) conf_d_dir = conf_dir / "master.d" conf_d_dir.mkdir(exist_ok=True) conf_file = str(conf_d_dir / "syndic.conf") _defaults = { "id": syndic_id, "master_id": syndic_id, "conf_file": conf_file, "root_dir": str(root_dir), "syndic_master": "127.0.0.1", "syndic_master_port": syndic_master_port or ports.get_unused_localhost_port(), "syndic_pidfile": "run/syndic.pid", "syndic_log_file": "logs/syndic.log", "syndic_log_level_logfile": "debug", "syndic_dir": "cache/syndics", "enable_legacy_startup_events": False, "pytest-syndic": { "master-id": master_of_masters_id, "log": {"prefix": "{}(id={!r})".format(cls.__name__, syndic_id)}, }, } # Merge in the initial default options with the internal _defaults salt.utils.dictupdate.update(defaults, _defaults, merge_lists=True) if overrides: # Merge in the default options with the syndic_overrides salt.utils.dictupdate.update(defaults, overrides, merge_lists=True) return defaults @classmethod def _configure( # pylint: disable=arguments-differ cls, factories_manager, daemon_id, root_dir=None, defaults=None, overrides=None, master_of_masters=None, ): return cls.default_config( root_dir, daemon_id, defaults=defaults, overrides=overrides, master_of_masters=master_of_masters, system_install=factories_manager.system_install, ) @classmethod def _get_verify_config_entries(cls, config): # verify env to make sure all required directories are created and have the # right permissions verify_env_entries = [ str(pathlib.Path(config["syndic_log_file"]).parent), ] return verify_env_entries @classmethod def load_config(cls, config_file, config): conf_dir = pathlib.Path(config_file).parent.parent master_config_file = str(conf_dir / "master") minion_config_file = str(conf_dir / "minion") return salt.config.syndic_config(master_config_file, minion_config_file) def get_check_events(self): """ Return a list of tuples in the form of `(master_id, event_tag)` check against to ensure the daemon is running """ pytest_config = self.config["pytest-{}".format(self.config["__role"])] if not pytest_config.get("master-id"): log.warning( "Will not be able to check for start events for %s since it's missing the 'master-id' key " "in the 'pytest-%s' dictionary, or it's value is None.", self, self.config["__role"], ) else: yield pytest_config["master-id"], "salt/{role}/{id}/start".format( role=self.config["__role"], id=self.id ) pytest-salt-factories-0.907.0/src/saltfactories/exceptions.py0000644000175000017500000000600314033044065025120 0ustar vampasvampas00000000000000""" PyTest Salt Factories related exceptions """ import traceback class SaltFactoriesException(Exception): """ Base exception for all pytest salt factories """ class ProcessFailed(SaltFactoriesException): """ Exception raised when a sub-process fails :param str message: The exception message :keyword list,tuple cmdline: The command line used to start the process :keyword str stdout: The ``stdout`` returned by the process :keyword str stderr: The ``stderr`` returned by the process :keyword int exitcode: The exitcode returned by the process :keyword Exception exc: The original exception raised """ def __init__(self, message, cmdline=None, stdout=None, stderr=None, exitcode=None, exc=None): super().__init__() self.message = message self.cmdline = cmdline self.stdout = stdout self.stderr = stderr self.exitcode = exitcode self.exc = exc def __str__(self): message = self.message append_new_line = False if self.cmdline: message += "\n Command Line: {}".format(self.cmdline) append_new_line = True if self.exitcode is not None: append_new_line = True message += "\n Exitcode: {}".format(self.exitcode) if self.stdout or self.stderr: append_new_line = True message += "\n Process Output:" if self.stdout: message += "\n >>>>> STDOUT >>>>>\n{}\n <<<<< STDOUT <<<<<".format(self.stdout) if self.stderr: message += "\n >>>>> STDERR >>>>>\n{}\n <<<<< STDERR <<<<<".format(self.stderr) if self.exc: append_new_line = True message += "\n{}".format("".join(traceback.format_exception(*self.exc)).rstrip()) if append_new_line: message += "\n" 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:`~saltfactories.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:`~saltfactories.exceptions.FactoryFailure` for the supported keyword arguments documentation. """ class ProcessNotStarted(FactoryFailure): """ Exception raised when a process failed to start Please look at :py:class:`~saltfactories.exceptions.FactoryFailure` for the supported keyword arguments documentation. """ class FactoryTimeout(FactoryNotStarted): """ Exception raised when a process timed-out Please look at :py:class:`~saltfactories.exceptions.FactoryFailure` for the supported keyword arguments documentation. """ pytest-salt-factories-0.907.0/src/saltfactories/manager.py0000644000175000017500000006104414062431421024355 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE Salt Factories Manager """ import logging import pathlib import sys import attr from saltfactories import CODE_ROOT_DIR from saltfactories import daemons from saltfactories.bases import Salt from saltfactories.utils import cli_scripts from saltfactories.utils import running_username log = logging.getLogger(__name__) @attr.s(kw_only=True, slots=True) class FactoriesManager: """ The :class:`FactoriesManager` is responsible for configuring and spawning Salt Daemons and making sure that any salt CLI tools are "targeted" to the right daemon. It also keeps track of which daemons were started and adds their termination routines to PyTest's request finalization routines. If process statistics are enabled, it also adds the started daemons to those statistics. :keyword pathlib.Path, str root_dir: :keyword int log_server_port: The port the log server should listen at :keyword int log_server_level: The level of the log server :keyword str log_server_host: The hostname/ip address of the host running the logs server. Defaults to "localhost". :keyword str code_dir: The path to the code root directory of the project being tested. This is important for proper code-coverage paths. :keyword bool inject_coverage: Inject code-coverage related code in the generated CLI scripts :keyword bool inject_sitecustomize: Inject code in the generated CLI scripts in order for our `sitecustomise.py` to be loaded by subprocesses. :keyword str cwd: The path to the current working directory :keyword dict environ: A dictionary of `key`, `value` pairs to add to the environment. :keyword bool 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. :keyword int start_timeout: The amount of time, in seconds, to wait, until a subprocess is considered as not started. :type stats_processes: saltfactories.plugins.sysstats.StatsProcesses :keyword stats_processes: This will be an `StatsProcesses` class instantiated on the :py:func:`~_pytest.hookspec.pytest_sessionstart` hook accessible as a session scoped `stats_processes` fixture. :keyword bool system_install: If true, the daemons and CLI's are run against a system installed salt setup, ie, the default salt system paths apply. """ root_dir = attr.ib() tmp_root_dir = attr.ib(init=False) log_server_port = attr.ib() log_server_level = attr.ib() log_server_host = attr.ib() code_dir = attr.ib(default=None) inject_coverage = attr.ib(default=False) inject_sitecustomize = attr.ib(default=False) cwd = attr.ib(default=None) environ = attr.ib(default=None) slow_stop = attr.ib(default=True) start_timeout = attr.ib(default=None) stats_processes = attr.ib(repr=False, default=None) system_install = attr.ib(repr=False, default=False) event_listener = attr.ib(repr=False) # Internal attributes scripts_dir = attr.ib(default=None, init=False, repr=False) def __attrs_post_init__(self): self.tmp_root_dir = pathlib.Path(self.root_dir.strpath) self.tmp_root_dir.mkdir(exist_ok=True) if self.system_install is False: self.root_dir = self.tmp_root_dir else: self.root_dir = pathlib.Path("/") if self.start_timeout is None: if not sys.platform.startswith(("win", "darwin")): self.start_timeout = 60 else: # Windows and macOS are just slower self.start_timeout = 120 if self.system_install is False: # Setup the internal attributes self.scripts_dir = self.root_dir / "scripts" self.scripts_dir.mkdir(exist_ok=True) @staticmethod def get_salt_log_handlers_path(): """ Returns the path to the Salt log handler this plugin provides """ return CODE_ROOT_DIR / "utils" / "saltext" / "log_handlers" @staticmethod def get_salt_engines_path(): """ Returns the path to the Salt engine this plugin provides """ return CODE_ROOT_DIR / "utils" / "saltext" / "engines" def final_minion_config_tweaks(self, config): pytest_key = "pytest-minion" if pytest_key not in config: # pragma: no cover config[pytest_key] = {} config[pytest_key]["returner_address"] = self.event_listener.address self.final_common_config_tweaks(config, "minion") def final_master_config_tweaks(self, config): pytest_key = "pytest-master" if pytest_key not in config: # pragma: no cover config[pytest_key] = {} config[pytest_key]["returner_address"] = self.event_listener.address self.final_common_config_tweaks(config, "master") def final_syndic_config_tweaks(self, config): self.final_common_config_tweaks(config, "syndic") def final_proxy_minion_config_tweaks(self, config): self.final_common_config_tweaks(config, "minion") def final_cloud_config_tweaks(self, config): self.final_common_config_tweaks(config, "cloud") def final_common_config_tweaks(self, config, role): config.setdefault("engines", []) if "pytest" not in config["engines"]: config["engines"].append("pytest") if "engines_dirs" not in config: config["engines_dirs"] = [] config["engines_dirs"].insert(0, str(FactoriesManager.get_salt_engines_path())) config.setdefault("user", running_username()) if not config["user"]: # pragma: no cover # If this value is empty, None, False, just remove it config.pop("user") if "log_forwarding_consumer" not in config: # Still using old logging, let's add our custom log handler if "log_handlers_dirs" not in config: config["log_handlers_dirs"] = [] config["log_handlers_dirs"].insert( 0, str(FactoriesManager.get_salt_log_handlers_path()) ) pytest_key = "pytest-{}".format(role) if pytest_key not in config: config[pytest_key] = {} pytest_config = config[pytest_key] if "log" not in pytest_config: # pragma: no cover pytest_config["log"] = {} log_config = pytest_config["log"] log_config.setdefault("host", self.log_server_host) log_config.setdefault("port", self.log_server_port) log_config.setdefault("level", "debug") def salt_master_daemon( self, master_id, order_masters=False, master_of_masters=None, defaults=None, overrides=None, max_start_attempts=3, start_timeout=None, factory_class=daemons.master.SaltMaster, **factory_class_kwargs ): """ Configure a salt-master Args: master_id(str): The master ID order_masters(bool): Boolean flag to set if this master is going to control other masters(ie, master of masters), like, for example, in a :ref:`Syndic ` topology scenario master_of_masters(:py:class:`saltfactories.daemons.master.SaltMaster`): A :py:class:`saltfactories.daemons.master.SaltMaster` instance, like, for example, in a :ref:`Syndic ` topology scenario defaults(dict): A dictionary of default configuration to use when configuring the master overrides(dict): A dictionary of configuration overrides to use when configuring the master max_start_attempts(int): How many attempts should be made to start the master in case of failure to validate that its running factory_class_kwargs(dict): Extra keyword arguments to pass to :py:class:`saltfactories.daemons.master.SaltMaster` Returns: :py:class:`saltfactories.daemons.master.SaltMaster`: The master process class instance """ root_dir = self.get_root_dir_for_daemon( master_id, defaults=defaults, factory_class=factory_class ) config = factory_class.configure( self, master_id, root_dir=root_dir, defaults=defaults, overrides=overrides, order_masters=order_masters, master_of_masters=master_of_masters, ) self.final_master_config_tweaks(config) loaded_config = factory_class.write_config(config) return self._get_factory_class_instance( "salt-master", loaded_config, factory_class, master_id, max_start_attempts, start_timeout, **factory_class_kwargs ) def salt_minion_daemon( self, minion_id, master=None, defaults=None, overrides=None, max_start_attempts=3, start_timeout=None, factory_class=daemons.minion.SaltMinion, **factory_class_kwargs ): """ Spawn a salt-minion Args: minion_id(str): The minion ID master(:py:class:`saltfactories.daemons.master.SaltMaster`): An instance of :py:class:`saltfactories.daemons.master.SaltMaster` that this minion will connect to. defaults(dict): A dictionary of default configuration to use when configuring the minion overrides(dict): A dictionary of configuration overrides to use when configuring the minion max_start_attempts(int): How many attempts should be made to start the minion in case of failure to validate that its running factory_class_kwargs(dict): Extra keyword arguments to pass to :py:class:`~saltfactories.daemons.minion.SaltMinion` Returns: :py:class:`~saltfactories.daemons.minion.SaltMinion`: The minion process class instance """ root_dir = self.get_root_dir_for_daemon( minion_id, defaults=defaults, factory_class=factory_class ) config = factory_class.configure( self, minion_id, root_dir=root_dir, defaults=defaults, overrides=overrides, master=master, ) self.final_minion_config_tweaks(config) loaded_config = factory_class.write_config(config) return self._get_factory_class_instance( "salt-minion", loaded_config, factory_class, minion_id, max_start_attempts, start_timeout, **factory_class_kwargs ) def salt_syndic_daemon( self, syndic_id, master_of_masters=None, defaults=None, overrides=None, max_start_attempts=3, start_timeout=None, factory_class=daemons.syndic.SaltSyndic, master_defaults=None, master_overrides=None, master_factory_class=daemons.master.SaltMaster, minion_defaults=None, minion_overrides=None, minion_factory_class=daemons.minion.SaltMinion, **factory_class_kwargs ): """ Spawn a salt-syndic Args: syndic_id(str): The Syndic ID. This ID will be shared by the ``salt-master``, ``salt-minion`` and ``salt-syndic`` processes. master_of_masters(:py:class:`saltfactories.daemons.master.SaltMaster`): An instance of :py:class:`saltfactories.daemons.master.SaltMaster` that the master configured in this :ref:`Syndic ` topology scenario shall connect to. defaults(dict): A dictionary of default configurations with three top level keys, ``master``, ``minion`` and ``syndic``, to use when configuring the ``salt-master``, ``salt-minion`` and ``salt-syndic`` respectively. overrides(dict): A dictionary of configuration overrides with three top level keys, ``master``, ``minion`` and ``syndic``, to use when configuring the ``salt-master``, ``salt-minion`` and ``salt-syndic`` respectively. max_start_attempts(int): How many attempts should be made to start the syndic in case of failure to validate that its running factory_class_kwargs(dict): Extra keyword arguments to pass to :py:class:`~saltfactories.daemons.syndic.SaltSyndic` Returns: :py:class:`~saltfactories.daemons.syndic.SaltSyndic`: The syndic process class instance """ root_dir = self.get_root_dir_for_daemon( syndic_id, defaults=defaults, factory_class=factory_class ) master_config = master_factory_class.configure( self, syndic_id, root_dir=root_dir, defaults=master_defaults, overrides=master_overrides, master_of_masters=master_of_masters, ) # Remove syndic related options for key in list(master_config): if key.startswith("syndic_"): master_config.pop(key) self.final_master_config_tweaks(master_config) master_loaded_config = master_factory_class.write_config(master_config) master_factory = self._get_factory_class_instance( "salt-master", master_loaded_config, master_factory_class, syndic_id, max_start_attempts, start_timeout, ) minion_config = minion_factory_class.configure( self, syndic_id, root_dir=root_dir, defaults=minion_defaults, overrides=minion_overrides, master=master_factory, ) self.final_minion_config_tweaks(minion_config) minion_loaded_config = minion_factory_class.write_config(minion_config) minion_factory = self._get_factory_class_instance( "salt-minion", minion_loaded_config, minion_factory_class, syndic_id, max_start_attempts, start_timeout, ) syndic_config = factory_class.default_config( root_dir, syndic_id=syndic_id, defaults=defaults, overrides=overrides, master_of_masters=master_of_masters, system_install=self.system_install, ) self.final_syndic_config_tweaks(syndic_config) syndic_loaded_config = factory_class.write_config(syndic_config) factory = self._get_factory_class_instance( "salt-syndic", syndic_loaded_config, factory_class, syndic_id, max_start_attempts=max_start_attempts, start_timeout=start_timeout, master=master_factory, minion=minion_factory, **factory_class_kwargs ) # We need the syndic master and minion running factory.before_start(master_factory.start) factory.before_start(minion_factory.start) return factory def salt_proxy_minion_daemon( self, proxy_minion_id, master=None, defaults=None, overrides=None, max_start_attempts=3, start_timeout=None, factory_class=daemons.proxy.SaltProxyMinion, **factory_class_kwargs ): """ Spawn a salt-proxy Args: proxy_minion_id(str): The proxy minion ID master(:py:class:`saltfactories.daemons.master.SaltMaster`): An instance of :py:class:`saltfactories.daemons.master.SaltMaster` that this minion will connect to. defaults(dict): A dictionary of default configuration to use when configuring the proxy minion overrides(dict): A dictionary of configuration overrides to use when configuring the proxy minion max_start_attempts(int): How many attempts should be made to start the proxy minion in case of failure to validate that its running factory_class_kwargs(dict): Extra keyword arguments to pass to :py:class:`~saltfactories.daemons.proxy.SaltProxyMinion` Returns: :py:class:`~saltfactories.daemons.proxy.SaltProxyMinion`: The proxy minion process class instance """ root_dir = self.get_root_dir_for_daemon( proxy_minion_id, defaults=defaults, factory_class=factory_class ) config = factory_class.configure( self, proxy_minion_id, root_dir=root_dir, defaults=defaults, overrides=overrides, master=master, ) self.final_proxy_minion_config_tweaks(config) loaded_config = factory_class.write_config(config) return self._get_factory_class_instance( "salt-proxy", loaded_config, factory_class, proxy_minion_id, max_start_attempts, start_timeout, **factory_class_kwargs ) def salt_api_daemon( self, master, max_start_attempts=3, start_timeout=None, factory_class=daemons.api.SaltApi, **factory_class_kwargs ): """ Spawn a salt-api Please see py:class:`~saltfactories.manager.FactoriesManager.salt_master_daemon` for argument documentation. Returns: :py:class:`~saltfactories.daemons.api.SaltApi`: The salt-api process class instance """ return self._get_factory_class_instance( "salt-api", master.config, factory_class, master.id, max_start_attempts=max_start_attempts, start_timeout=start_timeout, **factory_class_kwargs ) def get_sshd_daemon( self, config_dir=None, listen_address=None, listen_port=None, sshd_config_dict=None, display_name=None, script_name="sshd", max_start_attempts=3, start_timeout=None, factory_class=daemons.sshd.Sshd, **factory_class_kwargs ): """ Start an sshd daemon Args: max_start_attempts(int): How many attempts should be made to start the proxy minion in case of failure to validate that its running config_dir(pathlib.Path): The path to the sshd config directory listen_address(str): The address where the sshd server will listen to connections. Defaults to 127.0.0.1 listen_port(int): The port where the sshd server will listen to connections sshd_config_dict(dict): A dictionary of key-value pairs to construct the sshd config file script_name(str): The name or path to the binary to run. Defaults to ``sshd``. factory_class_kwargs(dict): Extra keyword arguments to pass to :py:class:`~saltfactories.daemons.sshd.Sshd` Returns: :py:class:`~saltfactories.daemons.sshd.Sshd`: The sshd process class instance """ if config_dir is None: config_dir = self.get_root_dir_for_daemon("sshd", factory_class=factory_class) try: config_dir = pathlib.Path(config_dir.strpath).resolve() except AttributeError: config_dir = pathlib.Path(config_dir).resolve() return factory_class( start_timeout=start_timeout or self.start_timeout, slow_stop=self.slow_stop, environ=self.environ, cwd=self.cwd, max_start_attempts=max_start_attempts, factories_manager=self, script_name=script_name, display_name=display_name or "SSHD", config_dir=config_dir, listen_address=listen_address, listen_port=listen_port, sshd_config_dict=sshd_config_dict, **factory_class_kwargs ) def get_container( self, container_name, image_name, docker_client=None, display_name=None, factory_class=daemons.container.Container, max_start_attempts=3, start_timeout=None, **factory_class_kwargs ): """ Start a docker container Args: container_name(str): The name to give the container image_name(str): The image to use docker_client: An instance of the docker client to use display_name(str): Human readable name for the factory factory_class: A factory class. (Default :py:class:`~saltfactories.daemons.container.Container`) max_start_attempts(int): How many attempts should be made to start the container in case of failure to validate that its running. start_timeout(int): The amount of time, in seconds, to wait, until the container is considered as not started. factory_class_kwargs(dict): Extra keyword arguments to pass to :py:class:`~saltfactories.daemons.container.Container` Returns: :py:class:`~saltfactories.daemons.container.Container`: The factory instance """ return factory_class( name=container_name, image=image_name, docker_client=docker_client, display_name=display_name or container_name, environ=self.environ, cwd=self.cwd, start_timeout=start_timeout or self.start_timeout, max_start_attempts=max_start_attempts, **factory_class_kwargs ) def get_salt_script_path(self, script_name): """ Return the path to the customized script path, generating one if needed. """ if self.system_install is True: return script_name return cli_scripts.generate_script( self.scripts_dir, script_name, code_dir=self.code_dir, inject_coverage=self.inject_coverage, inject_sitecustomize=self.inject_sitecustomize, ) def _get_factory_class_instance( self, script_name, daemon_config, factory_class, daemon_id, max_start_attempts, start_timeout, **factory_class_kwargs ): """ Helper method to instantiate daemon factories """ if self.system_install: script_path = script_name else: script_path = self.get_salt_script_path(script_name) factory = factory_class( config=daemon_config, start_timeout=start_timeout or self.start_timeout, slow_stop=self.slow_stop, environ=self.environ, cwd=self.cwd, max_start_attempts=max_start_attempts, event_listener=self.event_listener, factories_manager=self, script_name=script_path, system_install=self.system_install, **factory_class_kwargs ) return factory def get_root_dir_for_daemon(self, daemon_id, defaults=None, factory_class=None): if defaults and "root_dir" in defaults: try: root_dir = pathlib.Path(defaults["root_dir"].strpath).resolve() except AttributeError: root_dir = pathlib.Path(defaults["root_dir"]).resolve() root_dir.mkdir(parents=True, exist_ok=True) return root_dir if self.system_install is True and issubclass(factory_class, Salt): return self.root_dir elif self.system_install is True: root_dir = self.tmp_root_dir else: root_dir = self.root_dir counter = 1 root_dir = root_dir / daemon_id while True: if not root_dir.is_dir(): break root_dir = self.root_dir / "{}_{}".format(daemon_id, counter) counter += 1 root_dir.mkdir(parents=True, exist_ok=True) return root_dir pytest-salt-factories-0.907.0/src/saltfactories/plugins/0000755000175000017500000000000014106144336024052 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/src/saltfactories/plugins/__init__.py0000644000175000017500000000513114053226701026161 0ustar vampasvampas00000000000000""" saltfactories.plugins ~~~~~~~~~~~~~~~~~~~~~ Salt Factories PyTest plugin interface """ import logging import os import tempfile import pytest import saltfactories.utils.platform import saltfactories.utils.tempfiles log = logging.getLogger(__name__) def pytest_tempdir_temproot(): # Taken from https://github.com/saltstack/salt/blob/v2019.2.0/tests/support/paths.py # Avoid ${TMPDIR} and gettempdir() on MacOS as they yield a base path too long # for unix sockets: ``error: AF_UNIX path too long`` # Gentoo Portage prefers ebuild tests are rooted in ${TMPDIR} if saltfactories.utils.platform.is_windows(): tempdir = "C:/Windows/Temp" elif saltfactories.utils.platform.is_darwin(): tempdir = "/tmp" else: tempdir = os.environ.get("TMPDIR") or tempfile.gettempdir() return os.path.abspath(os.path.realpath(tempdir)) def pytest_tempdir_basename(): """ Return the temporary directory basename for the salt test suite. """ return "saltfactories" def pytest_runtest_logstart(nodeid): """ signal the start of running a single test item. This hook will be called **before** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and :func:`pytest_runtest_teardown` hooks. :param str nodeid: full id of the item :param location: a triple of ``(filename, linenum, testname)`` """ log.debug(">>>>>>> START %s >>>>>>>", nodeid) def pytest_runtest_logfinish(nodeid): """ signal the complete finish of running a single test item. This hook will be called **after** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and :func:`pytest_runtest_teardown` hooks. :param str nodeid: full id of the item :param location: a triple of ``(filename, linenum, testname)`` """ log.debug("<<<<<<< END %s <<<<<<<", nodeid) def pytest_runtest_logreport(report): """ Process the :py:class:`_pytest.reports.TestReport` produced for each of the setup, call and teardown runtest phases of an item. See :func:`pytest_runtest_protocol` for a description of the runtest protocol. """ if report.when == "call": log.debug("======= %s %s ========", report.outcome.upper(), report.nodeid) @pytest.hookimpl(trylast=True) def pytest_load_initial_conftests(*_): """ Register our pytest helpers """ if "temp_directory" not in pytest.helpers: pytest.helpers.register(saltfactories.utils.tempfiles.temp_directory, name="temp_directory") if "temp_file" not in pytest.helpers: pytest.helpers.register(saltfactories.utils.tempfiles.temp_file, name="temp_file") pytest-salt-factories-0.907.0/src/saltfactories/plugins/event_listener.py0000644000175000017500000004375114036256353027471 0ustar vampasvampas00000000000000""" Event Listener ============== A salt events store for all daemons started by salt-factories """ import copy import fnmatch import logging import threading import weakref from collections import deque from datetime import datetime from datetime import timedelta import attr import msgpack import pytest import zmq from saltfactories.utils import ports from saltfactories.utils import time log = logging.getLogger(__name__) def _convert_stamp(stamp): try: return datetime.fromisoformat(stamp) except AttributeError: # pragma: no cover # Python < 3.7 return datetime.strptime(stamp, "%Y-%m-%dT%H:%M:%S.%f") @attr.s(kw_only=True, slots=True, hash=True, frozen=True) class Event: """ The ``Event`` class is a container for a salt event which will live on the :py:class:`~saltfactories.plugins.event_listener.EventListener` store. :keyword str daemon_id: The daemon ID which received this event. :keyword str tag: The event tag of the event. :keyword ~datetime.datetime stamp: When the event occurred :keyword dict data: The event payload, filtered of all of Salt's private keys like ``_stamp`` which prevents proper assertions against it. :keyword dict full_data: The full event payload, as received by the daemon, including all of Salt's private keys. :keyword int,float expire_seconds: The time, in seconds, after which the event should be considered as expired and removed from the store. """ daemon_id = attr.ib() tag = attr.ib() stamp = attr.ib(converter=_convert_stamp) data = attr.ib(hash=False) full_data = attr.ib(hash=False) expire_seconds = attr.ib(hash=False) _expire_at = attr.ib(init=False, hash=False) @_expire_at.default def _set_expire_at(self): return self.stamp + timedelta(seconds=self.expire_seconds) @property def expired(self): """ Property to identify if the event has expired, at which time it should be removed from the store. """ if datetime.utcnow() < self._expire_at: return False return True @attr.s(kw_only=True, slots=True, hash=True, frozen=True) class MatchedEvents: """ The ``MatchedEvents`` class is a container which is returned by :py:func:`~saltfactories.plugins.event_listener.EventListener.wait_for_events`. :keyword set matches: A :py:class:`set` of :py:class:`~saltfactories.plugins.event_listener.Event` instances that matched. :keyword set missed: A :py:class:`set` of :py:class:`~saltfactories.plugins.event_listener.Event` instances that remained unmatched. One can also easily iterate through all matched events of this class: .. code-block:: python matched_events = MatchedEvents(..., ...) for event in matched_events: print(event.tag) """ matches = attr.ib() missed = attr.ib() @property def found_all_events(self): """ :return bool: :py:class:`True` if all events were matched, or :py:class:`False` otherwise. """ return (not self.missed) is True def __iter__(self): return iter(self.matches) @attr.s(kw_only=True, slots=True, hash=True) class EventListener: """ The ``EventListener`` is a service started by salt-factories which receives all the events of all the salt masters that it starts. The service runs throughout the whole pytest session. :keyword int timeout: How long, in seconds, should a forwarded event stay in the store, after which, it will be deleted. """ timeout = attr.ib(default=120) address = attr.ib(init=False) store = attr.ib(init=False, repr=False, hash=False) sentinel = attr.ib(init=False, repr=False, hash=False) sentinel_event = attr.ib(init=False, repr=False, hash=False) running_event = attr.ib(init=False, repr=False, hash=False) running_thread = attr.ib(init=False, repr=False, hash=False) cleanup_thread = attr.ib(init=False, repr=False, hash=False) auth_event_handlers = attr.ib(init=False, repr=False, hash=False) def __attrs_post_init__(self): self.store = deque(maxlen=10000) self.address = "tcp://127.0.0.1:{}".format(ports.get_unused_localhost_port()) self.running_event = threading.Event() self.running_thread = threading.Thread(target=self._run) self.cleanup_thread = threading.Thread(target=self._cleanup) self.sentinel = msgpack.dumps(None) self.sentinel_event = threading.Event() self.auth_event_handlers = weakref.WeakValueDictionary() def _run(self): context = zmq.Context() puller = context.socket(zmq.PULL) log.debug("%s Binding PULL socket to %s", self, self.address) puller.bind(self.address) if msgpack.version >= (0, 5, 2): msgpack_kwargs = {"raw": False} else: # pragma: no cover msgpack_kwargs = {"encoding": "utf-8"} log.debug("%s started", self) self.running_event.set() while self.running_event.is_set(): payload = puller.recv() if payload == self.sentinel: log.info("%s Received stop sentinel...", self) self.sentinel_event.set() break try: decoded = msgpack.loads(payload, **msgpack_kwargs) except ValueError: # pragma: no cover log.error( "%s Failed to msgpack.load message with payload: %s", self, payload, exc_info=True, ) continue if decoded is None: log.info("%s Received stop sentinel...", self) self.sentinel_event.set() break try: daemon_id, tag, data = decoded # Salt's event data has some "private" keys, for example, "_stamp" which # get in the way of direct assertions. # We'll just store a full_data attribute and clean up the regular data of these keys full_data = copy.deepcopy(data) for key in list(data): if key.startswith("_"): data.pop(key) event = Event( daemon_id=daemon_id, tag=tag, stamp=full_data["_stamp"], data=data, full_data=full_data, expire_seconds=self.timeout, ) log.info("%s received event: %s", self, event) self.store.append(event) if tag == "salt/auth": auth_event_callback = self.auth_event_handlers.get(daemon_id) if auth_event_callback: try: auth_event_callback(data) except Exception as exc: # pragma: no cover pylint: disable=broad-except log.error( "%s Error calling %r: %s", self, auth_event_callback, exc, exc_info=True, ) log.debug("%s store size after event received: %d", self, len(self.store)) except Exception: # pragma: no cover pylint: disable=broad-except log.error("%s Something funky happened", self, exc_info=True) puller.close(0) context.term() # We need to keep these events stored, restart zmq socket context = zmq.Context() puller = context.socket(zmq.PULL) log.debug("%s Binding PULL socket to %s", self, self.address) puller.bind(self.address) puller.close(1500) context.term() log.debug("%s is no longer running", self) def _cleanup(self): cleanup_at = time.time() + 30 while self.running_event.is_set(): if time.time() < cleanup_at: time.sleep(1) continue # Reset cleanup time cleanup_at = time.time() + 30 # Cleanup expired events to_remove = [] for event in self.store: if event.expired: to_remove.append(event) for event in to_remove: log.debug("%s Removing from event store: %s", self, event) self.store.remove(event) log.debug("%s store size after cleanup: %s", self, len(self.store)) def start(self): if self.running_event.is_set(): # pragma: no cover return log.debug("%s is starting", self) self.running_thread.start() # Wait for the thread to start if self.running_event.wait(5) is not True: self.running_event.clear() raise RuntimeError("Failed to start the event listener") self.cleanup_thread.start() def stop(self): if self.running_event.is_set() is False: # pragma: no cover return log.debug("%s is stopping", self) self.store.clear() self.auth_event_handlers.clear() context = zmq.Context() push = context.socket(zmq.PUSH) push.connect(self.address) try: push.send(self.sentinel) log.debug("%s Sent sentinel to trigger log server shutdown", self) if self.sentinel_event.wait(5) is not True: # pragma: no cover log.warning( "%s Failed to wait for the reception of the stop sentinel message. Stopping anyway.", self, ) finally: push.close(1500) context.term() self.running_event.clear() log.debug("%s Joining running thread...", self) self.running_thread.join(7) if self.running_thread.is_alive(): # pragma: no cover log.debug("%s The running thread is still alive. Waiting a little longer...", self) self.running_thread.join(5) if self.running_thread.is_alive(): log.debug( "%s The running thread is still alive. Exiting anyway and let GC take care of it", self, ) log.debug("%s Joining cleanup thread...", self) self.cleanup_thread.join(7) if self.cleanup_thread.is_alive(): # pragma: no cover log.debug("%s The cleanup thread is still alive. Waiting a little longer...", self) self.cleanup_thread.join(5) if self.cleanup_thread.is_alive(): log.debug( "%s The cleanup thread is still alive. Exiting anyway and let GC take care of it", self, ) log.debug("%s stopped", self) def get_events(self, patterns, after_time=None): """ Get events from the internal store. :param ~collections.abc.Sequence pattern: An iterable of tuples in the form of ``("", "")``, ie, which daemon ID we're targeting and the event tag pattern which will be passed to :py:func:`~fnmatch.fnmatch` to assert a match. :keyword ~datetime.datetime,float after_time: After which time to start matching events. :return set: A set of matched events """ if after_time is None: after_time = datetime.utcnow() elif isinstance(after_time, float): after_time = datetime.utcfromtimestamp(after_time) after_time_iso = after_time.isoformat() log.debug( "%s is checking for event patterns happening after %s: %s", self, after_time_iso, set(patterns), ) found_events = set() patterns = set(patterns) for event in copy.copy(self.store): if event.expired: # Too old, carry on continue if event.stamp < after_time: continue for pattern in set(patterns): _daemon_id, _pattern = pattern if event.daemon_id != _daemon_id: continue if fnmatch.fnmatch(event.tag, _pattern): log.debug("%s Found matching pattern: %s", self, pattern) found_events.add(event) if found_events: log.debug( "%s found the following patterns happening after %s: %s", self, after_time_iso, found_events, ) else: log.debug( "%s did not find any matching event patterns happening after %s", self, after_time_iso, ) return found_events def wait_for_events(self, patterns, timeout=30, after_time=None): """ Wait for a set of patterns to match or until timeout is reached. :param ~collections.abc.Sequence pattern: An iterable of tuples in the form of ``("", "")``, ie, which daemon ID we're targeting and the event tag pattern which will be passed to :py:func:`~fnmatch.fnmatch` to assert a match. :keyword int,float timeout: The amount of time to wait for the events, in seconds. :keyword ~datetime.datetime,float after_time: After which time to start matching events. :return: An instance of :py:class:`~saltfactories.plugins.event_listener.MatchedEvents`. :rtype ~saltfactories.plugins.event_listener.MatchedEvents: """ if after_time is None: after_time = datetime.utcnow() elif isinstance(after_time, float): after_time = datetime.utcfromtimestamp(after_time) after_time_iso = after_time.isoformat() log.debug( "%s is waiting for event patterns happening after %s: %s", self, after_time_iso, set(patterns), ) found_events = set() patterns = set(patterns) timeout_at = time.time() + timeout while True: if not patterns: return True for event in copy.copy(self.store): if event.expired: # Too old, carry on continue if event.stamp < after_time: continue for pattern in set(patterns): _daemon_id, _pattern = pattern if event.daemon_id != _daemon_id: continue if fnmatch.fnmatch(event.tag, _pattern): log.debug("%s Found matching pattern: %s", self, pattern) found_events.add(event) patterns.remove((event.daemon_id, _pattern)) if not patterns: break if time.time() > timeout_at: break time.sleep(0.5) return MatchedEvents(matches=found_events, missed=patterns) def register_auth_event_handler(self, master_id, callback): """ Register a callback to run for every authentication event, to accept or reject the minion authenticating. :param str master_id: The master ID for which the callback should run :type callback: ~collections.abc.Callable :param callback: The function while should be called """ self.auth_event_handlers[master_id] = callback def unregister_auth_event_handler(self, master_id): """ Un-register the authentication event callback, if any, for the provided master ID :param str master_id: The master ID for which the callback is registered """ self.auth_event_handlers.pop(master_id, None) @pytest.fixture(scope="session") def event_listener(request): """ All started daemons will forward their events into an instance of :py:class:`~saltfactories.plugins.event_listener.EventListener`. This fixture can be used to wait for events: .. code-block:: python def test_send(event_listener, salt_master, salt_minion, salt_call_cli): event_tag = random_string("salt/test/event/") data = {"event.fire": "just test it!!!!"} start_time = time.time() ret = salt_call_cli.run("event.send", event_tag, data=data) assert ret.exitcode == 0 assert ret.json assert ret.json is True event_pattern = (salt_master.id, event_tag) matched_events = event_listener.wait_for_events( [event_pattern], after_time=start_time, timeout=30 ) assert matched_events.found_all_events # At this stage, we got all the events we were waiting for And assert against those events events: .. code-block:: python def test_send(event_listener, salt_master, salt_minion, salt_call_cli): # ... check the example above for the initial code ... assert matched_events.found_all_events # At this stage, we got all the events we were waiting for for event in matched_events: assert event.data["id"] == salt_minion.id assert event.data["cmd"] == "_minion_event" assert "event.fire" in event.data["data"] """ return request.config.pluginmanager.get_plugin("saltfactories-event-listener") def pytest_configure(config): event_listener = EventListener() config.pluginmanager.register(event_listener, "saltfactories-event-listener") @pytest.hookimpl(tryfirst=True) def pytest_sessionstart(session): event_listener = session.config.pluginmanager.get_plugin("saltfactories-event-listener") event_listener.start() @pytest.hookimpl(trylast=True) def pytest_sessionfinish(session): event_listener = session.config.pluginmanager.get_plugin("saltfactories-event-listener") event_listener.stop() pytest-salt-factories-0.907.0/src/saltfactories/plugins/factories.py0000644000175000017500000000364114036250542026406 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE Daemon & CLI Factories ====================== Salt Daemon Factories PyTest Plugin """ import logging import os import pprint import pytest import saltfactories from saltfactories.manager import FactoriesManager log = logging.getLogger(__name__) @pytest.fixture(scope="session") def _salt_factories_config(request): """ Return a dictionary with the keyword arguments for FactoriesManager """ log_server = request.config.pluginmanager.get_plugin("saltfactories-log-server") return { "code_dir": saltfactories.CODE_ROOT_DIR.parent, "inject_coverage": True, "inject_sitecustomize": True, "log_server_host": log_server.log_host, "log_server_port": log_server.log_port, "log_server_level": log_server.log_level, "system_install": "SALT_FACTORIES_SYSTEM_INSTALL" in os.environ, } @pytest.fixture(scope="session") def salt_factories_config(): return {} @pytest.fixture(scope="session") def salt_factories( tempdir, event_listener, stats_processes, salt_factories_config, _salt_factories_config ): if not isinstance(salt_factories_config, dict): raise pytest.UsageError("The 'salt_factories_config' fixture MUST return a dictionary") if salt_factories_config: log.debug( "Salt Factories Manager Default Config:\n%s", pprint.pformat(_salt_factories_config) ) log.debug("Salt Factories Manager User Config:\n%s", pprint.pformat(salt_factories_config)) factories_config = _salt_factories_config.copy() factories_config.update(salt_factories_config) log.debug( "Instantiating the Salt Factories Manager with the following keyword arguments:\n%s", pprint.pformat(factories_config), ) return FactoriesManager( root_dir=tempdir, stats_processes=stats_processes, event_listener=event_listener, **factories_config ) pytest-salt-factories-0.907.0/src/saltfactories/plugins/loader.py0000644000175000017500000000646114035547046025707 0ustar vampasvampas00000000000000""" Loader ====== Salt loader mock support for tests """ import logging import pytest from saltfactories.utils.loader import LoaderModuleMock log = logging.getLogger(__name__) @pytest.hookimpl(trylast=True) def pytest_collection_modifyitems(items): """ Iterate through the collected items, in particular their test modules, to see if there's a function named ``configure_loader_modules``. If there is, assert that it's a fixture. If not, raise an error. """ seen_modules = set() for item in items: if item.module.__name__ in seen_modules: # No need to check the same module more than once continue seen_modules.add(item.module.__name__) # Some users have reported that this was not working and it was due to the fixture having the # wrong name. Let's look for typos. typos = ("configure_loader_module", "configure_load_module", "configure_load_modules") for typo in typos: try: fixture = getattr(item.module, typo) try: fixture._pytestfixturefunction # pylint: disable=pointless-statement raise RuntimeError( "The module {} defines a '{}' fixture but the correct fixture name " "is 'configure_loader_modules'".format(item.module, typo) ) except AttributeError: # It's a regular function?! # Carry on pass except AttributeError: # The test module does not define a function with the typo as the name. Good. pass # If the test module defines a configure_loader_modules function, let's confirm that it's actually a fixture try: fixture = item.module.configure_loader_modules except AttributeError: # The test module does not define a `configure_loader_modules` function at all continue else: # The test module defines a `configure_loader_modules` function. Is it a fixture? try: fixture._pytestfixturefunction except AttributeError: # It's not a fixture, raise an error raise RuntimeError( "The module {} defines a 'configure_loader_modules' function but " "that function is not a fixture".format(item.module) ) from None @pytest.fixture(autouse=True) def setup_loader_mock(request): """ Setup Salt's loader mocking/patching if the test module defines a ``configure_loader_modules`` fixture """ # If the test module defines a configure_loader_modules function, we'll setup the LoaderModuleMock # which is what actually sets up the salt loader mocking, if not, it's a no-op try: request.node.module.configure_loader_modules except AttributeError: # The test module does not define a `configure_loader_modules` function at all # Carry on testing yield else: # Mock salt's loader with what the `configure_loader_modules` fixture returns configure_loader_modules = request.getfixturevalue("configure_loader_modules") with LoaderModuleMock(configure_loader_modules) as loader_mock: yield loader_mock pytest-salt-factories-0.907.0/src/saltfactories/plugins/log_server.py0000644000175000017500000001746214062461573026613 0ustar vampasvampas00000000000000""" Log Server ========== """ import logging import threading import attr import msgpack import pytest import zmq from saltfactories.utils import platform from saltfactories.utils import ports from saltfactories.utils import time log = logging.getLogger(__name__) @attr.s(kw_only=True, slots=True, hash=True) class LogServer: log_host = attr.ib() log_port = attr.ib() log_level = attr.ib() socket_hwm = attr.ib() running_event = attr.ib(init=False, repr=False, hash=False) sentinel_event = attr.ib(init=False, repr=False, hash=False) process_queue_thread = attr.ib(init=False, repr=False, hash=False) @log_host.default def _default_log_host(self): if platform.is_windows(): # Windows cannot bind to 0.0.0.0 return "127.0.0.1" return "0.0.0.0" @log_port.default def _default_log_port(self): return ports.get_unused_localhost_port() @socket_hwm.default def _default_socket_hwm(self): # ~1MB return 1000000 def start(self): log.info("%s starting...", self) self.sentinel_event = threading.Event() self.running_event = threading.Event() self.process_queue_thread = threading.Thread(target=self.process_logs) self.process_queue_thread.start() # Wait for the thread to start if self.running_event.wait(5) is not True: # pragma: no cover self.running_event.clear() raise RuntimeError("Failed to start the log server") log.info("%s started", self) def stop(self): log.info("%s stopping...", self) address = "tcp://{}:{}".format(self.log_host, self.log_port) context = zmq.Context() sender = context.socket(zmq.PUSH) sender.connect(address) try: sender.send(msgpack.dumps(None)) log.debug("%s Sent sentinel to trigger log server shutdown", self) if self.sentinel_event.wait(5) is not True: # pragma: no cover log.warning( "%s Failed to wait for the reception of the stop sentinel message. Stopping anyway.", self, ) finally: sender.close(1000) context.term() # Clear the running even, the log process thread know it should stop self.running_event.clear() log.info("%s Joining the logging server process thread", self) self.process_queue_thread.join(7) if not self.process_queue_thread.is_alive(): log.debug("%s Stopped", self) else: # pragma: no cover log.warning( "%s The logging server thread is still running. Waiting a little longer...", self ) self.process_queue_thread.join(5) if not self.process_queue_thread.is_alive(): log.debug("%s Stopped", self) else: log.warning("%s The logging server thread is still running...", self) def process_logs(self): address = "tcp://{}:{}".format(self.log_host, self.log_port) context = zmq.Context() puller = context.socket(zmq.PULL) puller.set_hwm(self.socket_hwm) exit_timeout_seconds = 5 exit_timeout = None try: puller.bind(address) except zmq.ZMQError: # pragma: no cover log.exception("%s Unable to bind to puller at %s", self, address) return try: self.running_event.set() poller = zmq.Poller() poller.register(puller, zmq.POLLIN) while True: if not self.running_event.is_set(): if exit_timeout is None: log.debug( "%s Waiting %d seconds to process any remaning log messages " "before exiting...", self, exit_timeout_seconds, ) exit_timeout = time.time() + exit_timeout_seconds if time.time() >= exit_timeout: log.debug( "%s Unable to process remaining log messages in time. Exiting anyway.", self, ) break try: if not poller.poll(1000): continue msg = puller.recv() if msgpack.version >= (0, 5, 2): record_dict = msgpack.loads(msg, raw=False) else: # pragma: no cover record_dict = msgpack.loads(msg, encoding="utf-8") if record_dict is None: # A sentinel to stop processing the queue log.info("%s Received the sentinel to shutdown", self) self.sentinel_event.set() break try: record_dict["message"] except KeyError: # pragma: no cover # This log record was msgpack dumped from Py2 for key, value in record_dict.copy().items(): skip_update = True if isinstance(value, bytes): value = value.decode("utf-8") skip_update = False if isinstance(key, bytes): key = key.decode("utf-8") skip_update = False if skip_update is False: record_dict[key] = value # Just log everything, filtering will happen on the main process # logging handlers record = logging.makeLogRecord(record_dict) logger = logging.getLogger(record.name) logger.handle(record) except (EOFError, KeyboardInterrupt, SystemExit): # pragma: no cover break except Exception as exc: # pragma: no cover pylint: disable=broad-except log.warning( "%s An exception occurred in the processing queue thread: %s", self, exc, exc_info=True, ) finally: puller.close(1) context.term() log.debug("%s Process log thread terminated", self) @pytest.hookimpl(trylast=True) def pytest_configure(config): # If PyTest has no logging configured, default to ERROR level levels = [logging.ERROR] logging_plugin = config.pluginmanager.get_plugin("logging-plugin") try: level = logging_plugin.log_cli_handler.level if level is not None: levels.append(level) except AttributeError: # pragma: no cover # PyTest CLI logging not configured pass try: level = logging_plugin.log_file_level if level is not None: levels.append(level) except AttributeError: # pragma: no cover # PyTest Log File logging not configured pass if logging.NOTSET in levels: # We don't want the NOTSET level on the levels levels.pop(levels.index(logging.NOTSET)) log_level = logging.getLevelName(min(levels)) log_server = LogServer(log_level=log_level) config.pluginmanager.register(log_server, "saltfactories-log-server") @pytest.hookimpl(tryfirst=True) def pytest_sessionstart(session): log_server = session.config.pluginmanager.get_plugin("saltfactories-log-server") log_server.start() @pytest.hookimpl(trylast=True) def pytest_sessionfinish(session): log_server = session.config.pluginmanager.get_plugin("saltfactories-log-server") log_server.stop() pytest-salt-factories-0.907.0/src/saltfactories/plugins/markers.py0000644000175000017500000001520614036250542026073 0ustar vampasvampas00000000000000import os import pytest import saltfactories.utils.functional import saltfactories.utils.markers def pytest_addoption(parser): """ register argparse-style options and ini-style config values. """ test_selection_group = parser.getgroup("Tests Selection") test_selection_group.addoption( "--run-destructive", action="store_true", default=False, help="Run destructive tests. These tests can include adding " "or removing users from your system for example. " "Default: False", ) test_selection_group.addoption( "--run-expensive", action="store_true", default=False, help="Run expensive tests. These tests usually involve costs " "like for example bootstrapping a cloud VM. " "Default: False", ) @pytest.hookimpl(tryfirst=True) def pytest_runtest_setup(item): """ Fixtures injection based on markers or test skips based on CLI arguments """ __tracebackhide__ = True saltfactories.utils.markers.evaluate_markers(item) @pytest.mark.trylast def pytest_configure(config): """ called after command line options have been parsed and all plugins and initial conftest files been loaded. """ # Expose the markers we use to pytest CLI config.addinivalue_line( "markers", "destructive_test: Run destructive tests. These tests can include adding " "or removing users from your system for example.", ) config.addinivalue_line( "markers", "expensive_test: Run expensive tests. These tests can include starting resources " "which cost money, like VMs, for example.", ) config.addinivalue_line( "markers", "skip_if_not_root: Skip if the current user is not root on non windows platforms or not " "Administrator on windows platforms", ) config.addinivalue_line("markers", "skip_if_not_root: Skip if the current user is not `root`.") config.addinivalue_line( "markers", "skip_if_binaries_missing(*binaries, check_all=True, message=None):" "If 'check_all' is True, all binaries must exist." "If 'check_all' is False, then only one the passed binaries needs to be found. Usefull when, " "for example, passing a list of python interpreter names(python3.5, python3, python), where " "only one needs to exist.", ) config.addinivalue_line( "markers", "requires_network(only_local_network=False): Skip if no networking is set up. " "If 'only_local_network' is 'True', only the local network is checked.", ) # Platform Skip Markers config.addinivalue_line( "markers", "skip_on_windows: Skip test on Windows", ) config.addinivalue_line( "markers", "skip_unless_on_windows: Skip test unless on Windows", ) config.addinivalue_line( "markers", "skip_on_linux: Skip test on Linux", ) config.addinivalue_line( "markers", "skip_unless_on_linux: Skip test unless on Linux", ) config.addinivalue_line( "markers", "skip_on_darwin: Skip test on Darwin", ) config.addinivalue_line( "markers", "skip_unless_on_darwin: Skip test unless on Darwin", ) config.addinivalue_line( "markers", "skip_on_sunos: Skip test on SunOS", ) config.addinivalue_line( "markers", "skip_unless_on_sunos: Skip test unless on SunOS", ) config.addinivalue_line( "markers", "skip_on_smartos: Skip test on SmartOS", ) config.addinivalue_line( "markers", "skip_unless_on_smartos: Skip test unless on SmartOS", ) config.addinivalue_line( "markers", "skip_on_freebsd: Skip test on FreeBSD", ) config.addinivalue_line( "markers", "skip_unless_on_freebsd: Skip test unless on FreeBSD", ) config.addinivalue_line( "markers", "skip_on_netbsd: Skip test on NetBSD", ) config.addinivalue_line( "markers", "skip_unless_on_netbsd: Skip test unless on NetBSD", ) config.addinivalue_line( "markers", "skip_on_openbsd: Skip test on OpenBSD", ) config.addinivalue_line( "markers", "skip_unless_on_openbsd: Skip test unless on OpenBSD", ) config.addinivalue_line( "markers", "skip_on_aix: Skip test on AIX", ) config.addinivalue_line( "markers", "skip_unless_on_aix: Skip test unless on AIX", ) config.addinivalue_line( "markers", "skip_on_aarch64: Skip test on AArch64", ) config.addinivalue_line( "markers", "skip_unless_on_aarch64: Skip test unless on AArch64", ) config.addinivalue_line( "markers", "skip_on_platforms(windows=False, linux=False, darwin=False, sunos=False, smartos=False, freebsd=False, " "netbsd=False, openbsd=False, aix=False, aarch64=False): Pass True to one or more platform names to get the test skipped " "on those platforms", ) config.addinivalue_line( "markers", "skip_unless_on_platforms(windows=False, linux=False, darwin=False, sunos=False, smartos=False, freebsd=False, " "netbsd=False, openbsd=False, aix=False, aarch64=False): Pass True to one or more platform names to get the test skipped " "unless the chosen platforms match", ) config.addinivalue_line( "markers", "requires_salt_modules(*required_module_names): Skip if at least one module is not available.", ) config.addinivalue_line( "markers", "requires_salt_states(*required_state_names): Skip if at least one state module is not available.", ) # Add keys to environ in order to support Salt's helper decorators while it does not migrate to pytest markers env2cli = (("DESTRUCTIVE_TESTS", "--run-destructive"), ("EXPENSIVE_TESTS", "--run-expensive")) for envkey, cliflag in env2cli: os.environ[str(envkey)] = str(config.getoption(cliflag)).lower() @pytest.fixture(scope="session") def session_markers_loader(salt_factories): minion_id = "session-markers-minion" overrides = { "file_client": "local", "features": {"enable_slsvars_fixes": True}, } factory = salt_factories.salt_minion_daemon( minion_id, overrides=overrides, ) loader_instance = saltfactories.utils.functional.Loaders(factory.config.copy()) # Sync Everything loader_instance.modules.saltutil.sync_all() # Reload Everything - This is required or custom modules in _modules will not be found loader_instance.reload_all() return loader_instance pytest-salt-factories-0.907.0/src/saltfactories/plugins/sysinfo.py0000644000175000017500000000547114033044065026122 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE System Information ================== """ import io import pathlib import tempfile import pytest import salt.config import salt.loader import salt.utils.yaml import salt.version def pytest_addoption(parser): """ register argparse-style options and ini-style config values. """ output_options_group = parser.getgroup("Output Options") output_options_group.addoption( "--sys-info", "--sysinfo", default=False, action="store_true", help="Print system information on test session startup", ) @pytest.hookimpl(hookwrapper=True, trylast=True) def pytest_sessionstart(session): """called after the ``Session`` object has been created and before performing collection and entering the run test loop. :param _pytest.main.Session session: the pytest session object """ # Let PyTest do its own thing yield if session.config.getoption("--sys-info") is True: # And now we add our reporting sections terminal_reporter = session.config.pluginmanager.getplugin("terminalreporter") terminal_reporter.ensure_newline() terminal_reporter.section("System Information", sep=">") terminal_reporter.section("Salt Versions Report", sep="-", bold=True) terminal_reporter.write( "\n".join( " {}".format(line.rstrip()) for line in salt.version.versions_report() ).rstrip() + "\n" ) terminal_reporter.ensure_newline() # System Grains root_dir = pathlib.Path(tempfile.mkdtemp()) conf_file = root_dir / "conf" / "minion" conf_file.parent.mkdir() minion_config_defaults = salt.config.DEFAULT_MINION_OPTS.copy() minion_config_defaults.update( { "id": "saltfactories-reports-minion", "root_dir": str(root_dir), "conf_file": str(conf_file), "cachedir": "cache", "pki_dir": "pki", "file_client": "local", "server_id_use_crc": "adler32", } ) minion_config = salt.config.minion_config(None, defaults=minion_config_defaults) grains = salt.loader.grains(minion_config) grains_output_file = io.StringIO() salt.utils.yaml.safe_dump(grains, grains_output_file, default_flow_style=False) grains_output_file.seek(0) terminal_reporter.section("System Grains Report", sep="-") terminal_reporter.write( "\n".join( " {}".format(line.rstrip()) for line in grains_output_file.read().splitlines() ).rstrip() + "\n" ) terminal_reporter.ensure_newline() terminal_reporter.section("System Information", sep="<") terminal_reporter.ensure_newline() pytest-salt-factories-0.907.0/src/saltfactories/plugins/sysstats.py0000644000175000017500000001713414036257405026333 0ustar vampasvampas00000000000000""" System Statistics ================= Process stats PyTest plugin interface """ import os import sys from collections import OrderedDict import attr import psutil import pytest @attr.s(kw_only=True, slots=True, hash=True) class StatsProcesses: processes = attr.ib(init=False, default=attr.Factory(OrderedDict), hash=False) def add(self, display_name, process): if isinstance(process, int): # This is a process pid process = psutil.Process(process) self.processes[display_name] = process def remove(self, display_name): self.processes.pop(display_name, None) def items(self): return self.processes.items() def __iter__(self): return iter(self.processes) @attr.s(kw_only=True, slots=True, hash=True) class SystemStatsReporter: config = attr.ib(repr=False, hash=False) stats_processes = attr.ib(repr=False, hash=False) terminalreporter = attr.ib(repr=False, hash=False) show_sys_stats = attr.ib(init=False) sys_stats_no_children = attr.ib(init=False) sys_stats_mem_type = attr.ib(init=False) def __attrs_post_init__(self): self.show_sys_stats = ( self.config.getoption("--sys-stats") is True and self.config.getoption("--no-sys-stats") is False ) self.sys_stats_no_children = self.config.getoption("--sys-stats-no-children") is True if self.config.getoption("--sys-stats-uss-mem") is True: self.sys_stats_mem_type = "uss" if sys.platform.startswith("freebsd"): # FreeBSD doesn't apparently support uss self.sys_stats_mem_type = "rss" else: self.sys_stats_mem_type = "rss" @pytest.hookimpl(trylast=True) def pytest_runtest_logreport(self, report): if self.terminalreporter.verbosity <= 0: return if report.when != "call": return if self.show_sys_stats is False: return if self.terminalreporter.verbosity > 1: remove_from_stats = set() self.terminalreporter.ensure_newline() self.terminalreporter.section("Processes Statistics", sep="-", bold=True) left_padding = len(max(["System"] + list(self.stats_processes), key=len)) template = ( " ...{dots} {name} - CPU: {cpu:6.2f} % MEM: {mem:6.2f} % (Virtual Memory)" ) stats = { "name": "System", "dots": "." * (left_padding - len("System")), "cpu": psutil.cpu_percent(), "mem": psutil.virtual_memory().percent, } swap = psutil.swap_memory().percent if swap > 0: template += " SWAP: {swap:6.2f} %" stats["swap"] = swap template += "\n" self.terminalreporter.write(template.format(**stats)) template = " ...{dots} {name} - CPU: {cpu:6.2f} % MEM: {mem:6.2f} % ({m_type})" children_template = ( template + " MEM SUM: {c_mem} % ({m_type}) CHILD PROCS: {c_count}\n" ) no_children_template = template + "\n" for name, psproc in self.stats_processes.items(): template = no_children_template dots = "." * (left_padding - len(name)) pids = [] try: with psproc.oneshot(): stats = { "name": name, "dots": dots, "cpu": psproc.cpu_percent(), "mem": psproc.memory_percent(self.sys_stats_mem_type), "m_type": self.sys_stats_mem_type.upper(), } if self.sys_stats_no_children is False: pids.append(psproc.pid) children = psproc.children(recursive=True) if children: template = children_template stats["c_count"] = 0 c_mem = stats["mem"] for child in children: if child.pid in pids: # pragma: no cover continue pids.append(child.pid) if not psutil.pid_exists(child.pid): # pragma: no cover remove_from_stats.add(name) continue try: c_mem += child.memory_percent(self.sys_stats_mem_type) stats["c_count"] += 1 except ( psutil.AccessDenied, psutil.NoSuchProcess, ): # pragma: no cover continue if stats["c_count"]: stats["c_mem"] = "{:6.2f}".format(c_mem) else: template = no_children_template self.terminalreporter.write(template.format(**stats)) except psutil.NoSuchProcess: # pragma: no cover remove_from_stats.add(name) continue if remove_from_stats: # pragma: no cover for name in remove_from_stats: self.stats_processes.remove(name) def pytest_addoption(parser): """ register argparse-style options and ini-style config values. """ output_options_group = parser.getgroup("Output Options") output_options_group.addoption( "--sys-stats", default=False, action="store_true", help="Print System CPU and MEM statistics after each test execution.", ) output_options_group.addoption( "--no-sys-stats", default=False, action="store_true", help="Do not print System CPU and MEM statistics after each test execution.", ) output_options_group.addoption( "--sys-stats-no-children", default=False, action="store_true", help="Don't include child processes memory statistics.", ) output_options_group.addoption( "--sys-stats-uss-mem", default=False, action="store_true", help='Use the USS("Unique Set Size", memory unique to a process which would be freed if the process was ' "terminated) memory instead which is more expensive to calculate.", ) @pytest.hookimpl(trylast=True) def pytest_sessionstart(session): if ( session.config.getoption("--sys-stats") is True and session.config.getoption("--no-sys-stats") is False ): stats_processes = StatsProcesses() stats_processes.add("Test Suite Run", os.getpid()) else: stats_processes = None session.config.pluginmanager.register(stats_processes, "saltfactories-sysstats-processes") terminalreporter = session.config.pluginmanager.getplugin("terminalreporter") sys_stats_reporter = SystemStatsReporter( config=session.config, stats_processes=stats_processes, terminalreporter=terminalreporter ) session.config.pluginmanager.register(sys_stats_reporter, "saltfactories-sysstats-reporter") @pytest.fixture(scope="session") def stats_processes(request): return request.config.pluginmanager.get_plugin("saltfactories-sysstats-processes") pytest-salt-factories-0.907.0/src/saltfactories/utils/0000755000175000017500000000000014106144336023531 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/src/saltfactories/utils/__init__.py0000644000175000017500000000401014106014163025627 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE Utility functions """ import random import string from functools import lru_cache import salt.utils.user def random_string(prefix, size=6, uppercase=True, lowercase=True, digits=True): """ Generates a random string. :keyword str prefix: The prefix for the random string :keyword int size: The size of the random string :keyword bool uppercase: If true, include upper-cased ascii chars in choice sample :keyword bool lowercase: If true, include lower-cased ascii chars in choice sample :keyword bool digits: If true, include digits in choice sample :return str: The random string """ if not any([uppercase, lowercase, digits]): raise RuntimeError("At least one of 'uppercase', 'lowercase' or 'digits' needs to be true") choices = [] if uppercase: choices.extend(string.ascii_uppercase) if lowercase: choices.extend(string.ascii_lowercase) if digits: choices.extend(string.digits) return prefix + "".join(random.choice(choices) for _ in range(size)) @lru_cache(maxsize=1) def running_username(): return salt.utils.user.get_user() def format_callback_to_string(callback, args=None, kwargs=None): """ Convert a callback, its arguments and keyword arguments to a string suitable for logging purposes :param ~collections.abc.Callable,str callback: The callback function :param list,tuple args: The callback arguments :param dict kwargs: The callback keyword arguments :rtype: str """ if not isinstance(callback, str): try: callback_str = "{}(".format(callback.__qualname__) except AttributeError: callback_str = "{}(".format(callback.__name__) else: callback_str = "{}(".format(callback) if args: callback_str += ", ".join([repr(arg) for arg in args]) if kwargs: callback_str += ", ".join(["{}={!r}".format(k, v) for (k, v) in kwargs.items()]) callback_str += ")" return callback_str pytest-salt-factories-0.907.0/src/saltfactories/utils/cli_scripts.py0000644000175000017500000002020714061711027026417 0ustar vampasvampas00000000000000""" saltfactories.utils.cli_scripts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Code to generate Salt CLI scripts for test runs """ import logging import pathlib import stat import textwrap import pytest log = logging.getLogger(__name__) SCRIPT_TEMPLATES = { "salt": textwrap.dedent( """ import atexit import traceback from salt.scripts import salt_main if __name__ == '__main__': exitcode = 0 try: salt_main() except SystemExit as exc: exitcode = exc.code # https://docs.python.org/3/library/exceptions.html#SystemExit if exitcode is None: exitcode = 0 if not isinstance(exitcode, int): # A string?! sys.stderr.write(exitcode) exitcode = 1 except Exception as exc: sys.stderr.write( "An un-handled exception was caught: " + str(exc) + "\\n" + traceback.format_exc() ) exitcode = 1 sys.stdout.flush() sys.stderr.flush() atexit._run_exitfuncs() os._exit(exitcode) """ ), "salt-api": textwrap.dedent( """ import atexit import traceback import salt.cli.api import salt.utils.process salt.utils.process.notify_systemd() def main(): sapi = salt.cli.api.SaltAPI() sapi.start() if __name__ == '__main__': exitcode = 0 try: main() except SystemExit as exc: exitcode = exc.code # https://docs.python.org/3/library/exceptions.html#SystemExit if exitcode is None: exitcode = 0 if not isinstance(exitcode, int): # A string?! sys.stderr.write(exitcode) exitcode = 1 except Exception as exc: sys.stderr.write( "An un-handled exception was caught: " + str(exc) + "\\n" + traceback.format_exc() ) exitcode = 1 sys.stdout.flush() sys.stderr.flush() atexit._run_exitfuncs() os._exit(exitcode) """ ), "common": textwrap.dedent( """ import atexit import traceback from salt.scripts import salt_{0} def main(): if sys.platform.startswith("win"): import os.path import py_compile cfile = os.path.splitext(__file__)[0] + '.pyc' if not os.path.exists(cfile): py_compile.compile(__file__, cfile) salt_{0}() if __name__ == '__main__': exitcode = 0 try: main() except SystemExit as exc: exitcode = exc.code # https://docs.python.org/3/library/exceptions.html#SystemExit if exitcode is None: exitcode = 0 if not isinstance(exitcode, int): # A string?! sys.stderr.write(exitcode) exitcode = 1 except Exception as exc: sys.stderr.write( "An un-handled exception was caught: " + str(exc) + "\\n" + traceback.format_exc() ) exitcode = 1 sys.stdout.flush() sys.stderr.flush() atexit._run_exitfuncs() os._exit(exitcode) """ ), "coverage": textwrap.dedent( """ # Setup coverage environment variables COVERAGE_FILE = os.path.join(CODE_DIR, '.coverage') COVERAGE_PROCESS_START = os.path.join(CODE_DIR, '.coveragerc') os.environ[str('COVERAGE_FILE')] = str(COVERAGE_FILE) os.environ[str('COVERAGE_PROCESS_START')] = str(COVERAGE_PROCESS_START) """ ), "sitecustomize": textwrap.dedent( """ # Allow sitecustomize.py to be importable for test coverage purposes SITECUSTOMIZE_DIR = r'{sitecustomize_dir}' PYTHONPATH = os.environ.get('PYTHONPATH') or None if PYTHONPATH is None: PYTHONPATH_ENV_VAR = SITECUSTOMIZE_DIR else: PYTHON_PATH_ENTRIES = PYTHONPATH.split(os.pathsep) if SITECUSTOMIZE_DIR in PYTHON_PATH_ENTRIES: PYTHON_PATH_ENTRIES.remove(SITECUSTOMIZE_DIR) PYTHON_PATH_ENTRIES.insert(0, SITECUSTOMIZE_DIR) PYTHONPATH_ENV_VAR = os.pathsep.join(PYTHON_PATH_ENTRIES) os.environ[str('PYTHONPATH')] = str(PYTHONPATH_ENV_VAR) if SITECUSTOMIZE_DIR in sys.path: sys.path.remove(SITECUSTOMIZE_DIR) sys.path.insert(0, SITECUSTOMIZE_DIR) """ ), } def generate_script( bin_dir, script_name, code_dir=None, inject_coverage=False, inject_sitecustomize=False, ): """ Generate a CLI script :param ~pathlib.Path bin_dir: The path to the directory which will contain the CLI scripts :param str script_name: The CLI script name :param ~pathlib.Path code_dir: The project's being tested root directory path :param bool inject_coverage: Inject code to track code coverage :param bool inject_sitecustomize: Inject code to support code coverage in subprocesses """ if isinstance(bin_dir, str): bin_dir = pathlib.Path(bin_dir) bin_dir.mkdir(exist_ok=True) cli_script_name = "cli_{}.py".format(script_name.replace("-", "_")) script_path = bin_dir / cli_script_name if not script_path.is_file(): log.info("Generating %s", script_path) with script_path.open("w") as sfh: script_template = SCRIPT_TEMPLATES.get(script_name, None) if script_template is None: script_template = SCRIPT_TEMPLATES.get("common", None) script_contents = ( textwrap.dedent( """ from __future__ import absolute_import import os import sys # We really do not want buffered output os.environ[str("PYTHONUNBUFFERED")] = str("1") # Don't write .pyc files or create them in __pycache__ directories os.environ[str("PYTHONDONTWRITEBYTECODE")] = str("1") """ ).strip() + "\n\n" ) if code_dir: script_contents += ( textwrap.dedent( """ CODE_DIR = r'{code_dir}' if CODE_DIR in sys.path: sys.path.remove(CODE_DIR) sys.path.insert(0, CODE_DIR)""".format( code_dir=code_dir ) ).strip() + "\n\n" ) if inject_coverage and not code_dir: raise pytest.UsageError( "The inject coverage code needs to know the code root to find the " "path to the '.coveragerc' file. Please pass 'code_dir'." ) if inject_coverage: script_contents += SCRIPT_TEMPLATES["coverage"].strip() + "\n\n" if inject_sitecustomize: script_contents += ( SCRIPT_TEMPLATES["sitecustomize"] .format( sitecustomize_dir=str(pathlib.Path(__file__).resolve().parent / "coverage") ) .strip() + "\n\n" ) script_contents += ( script_template.format(script_name.replace("salt-", "").replace("-", "_")).strip() + "\n" ) sfh.write(script_contents) log.debug( "Wrote the following contents to temp script %s:\n%s", script_path, script_contents ) fst = script_path.stat() script_path.chmod(fst.st_mode | stat.S_IEXEC) log.info("Returning script path %r", script_path) return str(script_path) pytest-salt-factories-0.907.0/src/saltfactories/utils/coverage/0000755000175000017500000000000014106144336025324 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/src/saltfactories/utils/coverage/sitecustomize.py0000644000175000017500000000012614033044065030601 0ustar vampasvampas00000000000000try: import coverage coverage.process_startup() except ImportError: pass pytest-salt-factories-0.907.0/src/saltfactories/utils/functional.py0000644000175000017500000004042014106143715026245 0ustar vampasvampas00000000000000""" saltfactories.utils.functional ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Salt functional testing support """ import copy import logging import operator import attr import salt.loader import salt.pillar from saltfactories.utils import format_callback_to_string try: import salt.features # pylint: disable=ungrouped-imports HAS_SALT_FEATURES = True except ImportError: # pragma: no cover HAS_SALT_FEATURES = False log = logging.getLogger(__name__) class Loaders: """ This class provides the required functionality for functional testing against the salt loaders :param dict opts: The options dictionary to load the salt loaders. Example usage: .. code-block:: python import salt.config from saltfactories.utils.functional import Loaders @pytest.fixture(scope="module") def minion_opts(): return salt.config.minion_config(None) @pytest.fixture(scope="module") def loaders(minion_opts): return Loaders(minion_opts) @pytest.fixture(autouse=True) def reset_loaders_state(loaders): try: # Run the tests yield finally: # Reset the loaders state loaders.reset_state() """ def __init__(self, opts): self.opts = opts self.context = {} self._original_opts = copy.deepcopy(opts) self._reset_state_funcs = [self.context.clear] self._reload_all_funcs = [self.reset_state] self._grains = None self._modules = None self._pillar = None self._serializers = None self._states = None self._utils = None if HAS_SALT_FEATURES: salt.features.setup_features(self.opts) self.reload_all() # Force the minion to populate it's cache if need be self.modules.saltutil.sync_all() # Now reload again so that the loader takes into account said cache self.reload_all() def reset_state(self): for func in self._reset_state_funcs: func() def reload_all(self): for func in self._reload_all_funcs: try: func() except Exception as exc: # pragma: no cover pylint: disable=broad-except log.warning("Failed to run '%s': %s", func.__name__, exc, exc_info=True) self.opts = copy.deepcopy(self._original_opts) self._grains = None self._modules = None self._pillar = None self._serializers = None self._states = None self._utils = None self.opts["grains"] = self.grains self.refresh_pillar() @property def grains(self): """ The grains loaded by the salt loader """ if self._grains is None: self._grains = salt.loader.grains(self.opts, context=self.context) return self._grains @property def utils(self): """ The utils loaded by the salt loader """ if self._utils is None: self._utils = salt.loader.utils(self.opts, context=self.context) return self._utils @property def modules(self): """ The execution modules loaded by the salt loader """ if self._modules is None: _modules = salt.loader.minion_mods( self.opts, context=self.context, utils=self.utils, initial_load=True ) if isinstance(_modules.loaded_modules, dict): for func_name in ("single", "sls", "template", "template_str"): full_func_name = "state.{}".format(func_name) if func_name == "single": wrapper_cls = StateResult else: wrapper_cls = MultiStateResult replacement_function = StateModuleFuncWrapper( _modules[full_func_name], wrapper_cls ) _modules._dict[full_func_name] = replacement_function _modules.loaded_modules["state"][func_name] = replacement_function setattr( _modules.loaded_modules["state"], func_name, replacement_function, ) else: # Newer version of Salt where only one dictionary with the loaded functions is maintained class ModulesLoaderDict(_modules.mod_dict_class): def __setitem__(self, key, value): """ We hijack __setitem__ so that we can replace specific state functions with a wrapper which will return a more pythonic data structure to assert against. """ if key in ( "state.single", "state.sls", "state.template", "state.template_str", ): if key == "state.single": wrapper_cls = StateResult else: wrapper_cls = MultiStateResult value = StateModuleFuncWrapper(value, wrapper_cls) return super().__setitem__(key, value) loader_dict = _modules._dict.copy() _modules._dict = ModulesLoaderDict() for key, value in loader_dict.items(): _modules._dict[key] = value self._modules = _modules return self._modules @property def serializers(self): """ The serializers loaded by the salt loader """ if self._serializers is None: self._serializers = salt.loader.serializers(self.opts) return self._serializers @property def states(self): """ The state modules loaded by the salt loader """ if self._states is None: _states = salt.loader.states( self.opts, functions=self.modules, utils=self.utils, serializers=self.serializers, context=self.context, ) # For state execution modules, because we'd have to almost copy/paste what salt.modules.state.single # does, we actually "proxy" the call through salt.modules.state.single instead of calling the state # execution modules directly. This was also how the non pytest test suite worked # Let's load all modules now # Now, we proxy loaded modules through salt.modules.state.single if isinstance(_states.loaded_modules, dict): # Old Salt? _states._load_all() for module_name in list(_states.loaded_modules): for func_name in list(_states.loaded_modules[module_name]): full_func_name = "{}.{}".format(module_name, func_name) replacement_function = StateFunction( self.modules.state.single, full_func_name ) _states._dict[full_func_name] = replacement_function _states.loaded_modules[module_name][func_name] = replacement_function setattr( _states.loaded_modules[module_name], func_name, replacement_function, ) else: # Newer version of Salt where only one dictionary with the loaded functions is maintained class StatesLoaderDict(_states.mod_dict_class): def __init__(self, proxy_func, *args, **kwargs): super().__init__(*args, **kwargs) self.__proxy_func__ = proxy_func def __setitem__(self, name, func): """ We hijack __setitem__ so that we can replace the loaded functions with a wrapper For state execution modules, because we'd have to almost copy/paste what ``salt.modules.state.single`` does, we actually "proxy" the call through ``salt.modules.state.single`` instead of calling the state execution modules directly. This was also how the non pytest test suite worked """ func = StateFunction(self.__proxy_func__, name) return super().__setitem__(name, func) loader_dict = _states._dict.copy() _states._dict = StatesLoaderDict(self.modules.state.single) for key, value in loader_dict.items(): _states._dict[key] = value self._states = _states return self._states @property def pillar(self): """ The pillar loaded by the salt loader """ if self._pillar is None: self._pillar = salt.pillar.get_pillar( self.opts, self.grains, self.opts["id"], saltenv=self.opts["saltenv"], pillarenv=self.opts.get("pillarenv"), ).compile_pillar() return self._pillar def refresh_pillar(self): self._pillar = None self.opts["pillar"] = self.pillar @attr.s class StateResult: """ This class wraps a single salt state return into a more pythonic object in order to simplify assertions :param dict raw: A single salt state return result .. code-block:: python def test_user_absent(loaders): ret = loaders.states.user.absent(name=random_string("account-", uppercase=False)) assert ret.result is True """ raw = attr.ib() state_id = attr.ib(init=False) full_return = attr.ib(init=False) filtered = attr.ib(init=False) @state_id.default def _state_id(self): if not isinstance(self.raw, dict): raise ValueError("The state result errored: {}".format(self.raw)) return next(iter(self.raw.keys())) @full_return.default def _full_return(self): return self.raw[self.state_id] @filtered.default def _filtered_default(self): _filtered = {} for key, value in self.full_return.items(): if key.startswith("_") or key in ("duration", "start_time"): continue _filtered[key] = value return _filtered @property def run_num(self): """ The ``__run_num__`` key on the full state return dictionary """ return self.full_return["__run_num__"] or 0 @property def name(self): """ The ``name`` key on the full state return dictionary """ return self.full_return["name"] @property def result(self): """ The ``result`` key on the full state return dictionary """ return self.full_return["result"] @property def changes(self): """ The ``changes`` key on the full state return dictionary """ return self.full_return["changes"] @property def comment(self): """ The ``comment`` key on the full state return dictionary """ return self.full_return["comment"] @property def warnings(self): """ The ``warnings`` key on the full state return dictionary """ return self.full_return.get("warnings") or [] def __contains__(self, key): """ Checks for the existence of ``key`` in the full state return dictionary """ return key in self.full_return def __eq__(self, _): raise TypeError( "Please assert comparisons with {}.filtered instead".format(self.__class__.__name__) ) def __bool__(self): raise TypeError( "Please assert comparisons with {}.filtered instead".format(self.__class__.__name__) ) @attr.s class StateFunction: """ Simple wrapper around Salt's state execution module functions which actually proxies the call through Salt's ``state.single`` execution module """ proxy_func = attr.ib(repr=False) state_func = attr.ib() def __call__(self, *args, **kwargs): log.info( "Calling %s", format_callback_to_string("state.single", (self.state_func,) + args, kwargs), ) return self.proxy_func(self.state_func, *args, **kwargs) @attr.s class MultiStateResult: """ This class wraps multiple salt state returns, for example, running the ``state.sls`` execution module, into a more pythonic object in order to simplify assertions :param dict,list raw: The multiple salt state returns result, a dictionary on success or a list on failure Example usage on the test suite: .. code-block:: python def test_issue_1876_syntax_error(loaders, state_tree, tmp_path): testfile = tmp_path / "issue-1876.txt" sls_contents = ''' {}: file: - managed - source: salt://testfile file.append: - text: foo '''.format( testfile ) with pytest.helpers.temp_file("issue-1876.sls", sls_contents, state_tree): ret = loaders.modules.state.sls("issue-1876") assert ret.failed errmsg = ( "ID '{}' in SLS 'issue-1876' contains multiple state declarations of the" " same type".format(testfile) ) assert errmsg in ret.errors def test_pydsl(loaders, state_tree, tmp_path): testfile = tmp_path / "testfile" sls_contents = ''' #!pydsl state("{}").file("touch") '''.format( testfile ) with pytest.helpers.temp_file("pydsl.sls", sls_contents, state_tree): ret = loaders.modules.state.sls("pydsl") for staterun in ret: assert staterun.result is True assert testfile.exists() """ raw = attr.ib() _structured = attr.ib(init=False) @_structured.default def _set_structured(self): if self.failed: return [] state_result = [StateResult({state_id: data}) for state_id, data in self.raw.items()] return sorted(state_result, key=operator.attrgetter("run_num")) def __iter__(self): return iter(self._structured) def __contains__(self, key): for state_result in self: if state_result.state_id == key: return True return False def __getitem__(self, state_id_or_index): if isinstance(state_id_or_index, int): # We're trying to get the state run by index return self._structured[state_id_or_index] for state_result in self: if state_result.state_id == state_id_or_index: return state_result raise KeyError("No state by the ID of '{}' was found".format(state_id_or_index)) @property def failed(self): """ Return ``True`` or ``False`` if the multiple state run was not successful """ return isinstance(self.raw, list) @property def errors(self): """ Return the list of errors in case the multiple state run was not successful """ if not self.failed: return [] return list(self.raw) @attr.s(frozen=True) class StateModuleFuncWrapper: """ This class simply wraps a single or multiple state returns into a more pythonic object, :py:class:`~saltfactories.utils.functional.StateResult` or py:class:`~saltfactories.utils.functional.MultiStateResult` :param callable func: A salt loader function :param ~saltfactories.utils.functional.StateResult,~saltfactories.utils.functional.MultiStateResult wrapper: The wrapper to use for the return of the salt loader function's return """ func = attr.ib() wrapper = attr.ib() def __call__(self, *args, **kwargs): ret = self.func(*args, **kwargs) return self.wrapper(ret) pytest-salt-factories-0.907.0/src/saltfactories/utils/loader.py0000644000175000017500000001550514033044065025354 0ustar vampasvampas00000000000000""" saltfactories.utils.loader ~~~~~~~~~~~~~~~~~~~~~~~~~~ Salt's Loader PyTest Mock Support """ import logging import sys import types from collections import deque from unittest.mock import patch import attr import pytest from saltfactories.utils import format_callback_to_string log = logging.getLogger(__name__) @attr.s(init=True, slots=True, frozen=True) class LoaderModuleMock: setup_loader_modules = attr.ib(init=True) # These dunders should always exist at the module global scope salt_module_dunders = attr.ib( init=True, repr=False, kw_only=True, default=( "__opts__", "__salt__", "__runner__", "__context__", "__utils__", "__ext_pillar__", "__thorium__", "__states__", "__serializers__", "__ret__", "__grains__", "__pillar__", "__sdb__", ), ) # These dunders might exist at the module global scope salt_module_dunders_optional = attr.ib( init=True, repr=False, kw_only=True, default=("__proxy__",), ) # These dunders might exist at the function global scope salt_module_dunder_attributes = attr.ib( init=True, repr=False, kw_only=True, default=( # Salt states attributes "__env__", "__low__", "__instance_id__", "__orchestration_jid__", # Salt runners attributes "__jid_event__", # Salt cloud attributes "__active_provider_name__", # Proxy Minions "__proxyenabled__", ), ) _finalizers = attr.ib(init=False, repr=False, hash=False, default=attr.Factory(deque)) def start(self): module_globals = {dunder: {} for dunder in self.salt_module_dunders} for module, globals_to_mock in self.setup_loader_modules.items(): log.trace("Setting up loader globals for %s; globals: %s", module, globals_to_mock) if not isinstance(module, types.ModuleType): raise pytest.UsageError( "The dictionary keys returned by setup_loader_modules() " "must be an imported module, not {}".format(type(module)) ) if not isinstance(globals_to_mock, dict): raise pytest.UsageError( "The dictionary values returned by setup_loader_modules() " "must be a dictionary, not {}".format(type(globals_to_mock)) ) for key in self.salt_module_dunders: if not hasattr(module, key): # Set the dunder name as an attribute on the module if not present setattr(module, key, {}) # Remove the added attribute after the test finishes self.addfinalizer(delattr, module, key) # Patch sys.modules as the first step self._patch_sys_modules(globals_to_mock) # Now patch the module globals # We actually want to grab a copy of the module globals so that if mocking # multiple modules, and at least one of the modules has a function to path, # the patch only happens on the module it's supposed to patch and not all of them. # It's not a deepcopy because we want to maintain the reference to the salt dunders # added in the start of this function self._patch_module_globals(module, globals_to_mock, module_globals.copy()) def stop(self): while self._finalizers: func, args, kwargs = self._finalizers.popleft() func_repr = format_callback_to_string(func, args, kwargs) try: log.trace("Calling finalizer %s", func_repr) func(*args, **kwargs) except Exception as exc: # pragma: no cover pylint: disable=broad-except log.error( "Failed to run finalizer %s: %s", func_repr, exc, exc_info=True, ) def addfinalizer(self, func, *args, **kwargs): """ Register a function to run when stopping """ self._finalizers.append((func, args, kwargs)) def _patch_sys_modules(self, mocks): if "sys.modules" not in mocks: return sys_modules = mocks["sys.modules"] if not isinstance(sys_modules, dict): raise pytest.UsageError( "'sys.modules' must be a dictionary not: {}".format(type(sys_modules)) ) patcher = patch.dict(sys.modules, values=sys_modules) patcher.start() self.addfinalizer(patcher.stop) def _patch_module_globals(self, module, mocks, module_globals): salt_dunder_dicts = self.salt_module_dunders + self.salt_module_dunders_optional allowed_salt_dunders = salt_dunder_dicts + self.salt_module_dunder_attributes for key in mocks: if key == "sys.modules": # sys.modules is addressed on another function continue if key.startswith("__"): if key in ("__init__", "__virtual__"): raise pytest.UsageError( "No need to patch {!r}. Passed loader module dict: {}".format( key, self.setup_loader_modules, ) ) elif key not in allowed_salt_dunders: raise pytest.UsageError( "Don't know how to handle {!r}. Passed loader module dict: {}".format( key, self.setup_loader_modules, ) ) elif key in salt_dunder_dicts and not hasattr(module, key): # Add the key as a dictionary attribute to the module so it can be patched by `patch.dict`' setattr(module, key, {}) # Remove the added attribute after the test finishes self.addfinalizer(delattr, module, key) if not hasattr(module, key): # Set the key as an attribute so it can be patched setattr(module, key, None) # Remove the added attribute after the test finishes self.addfinalizer(delattr, module, key) module_globals[key] = mocks[key] # Patch the module! log.trace("Patching globals for %s; globals: %s", module, module_globals) patcher = patch.multiple(module, **module_globals) patcher.start() self.addfinalizer(patcher.stop) def __enter__(self): self.start() return self def __exit__(self, *args): self.stop() pytest-salt-factories-0.907.0/src/saltfactories/utils/markers.py0000644000175000017500000007275414033044065025563 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE saltfactories.utils.markers ~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyTest Markers related utilities """ import contextlib import fnmatch import logging import os import sys import pytest import salt.utils.path import salt.utils.win_functions import saltfactories.utils.platform import saltfactories.utils.ports as ports import saltfactories.utils.socket as socket log = logging.getLogger(__name__) def skip_if_not_root(): """ Helper function to check for root/Administrator privileges Returns: str: The reason of the skip """ if not sys.platform.startswith("win"): if os.getuid() != 0: return "You must be logged in as root to run this test" else: current_user = salt.utils.win_functions.get_current_user() if current_user != "SYSTEM": if not salt.utils.win_functions.is_admin(current_user): return "You must be logged in as an Administrator to run this test" def skip_if_binaries_missing(binaries, check_all=True, reason=None): """ Helper function to check for existing binaries Args: binaries (list or tuple): Iterator of binaries to check check_all (bool): If ``check_all`` is ``True``, the default, all binaries must exist. If ``check_all`` is ``False``, then only one the passed binaries needs to be found. Useful when, for example, passing a list of python interpreter names(python3.5, python3, python), where only one needs to exist. reason (str): The skip reason. Returns: str: The reason for the skip. None: Should not be skipped. """ if check_all is False: # We only need one of the passed binaries to exist if salt.utils.path.which_bin(binaries) is None: if reason is not None: return reason return "None of the following binaries was found: {}".format(", ".join(binaries)) else: for binary in binaries: if salt.utils.path.which(binary) is None: if reason is not None: return reason return "The '{}' binary was not found".format(binary) log.debug("All binaries found. Searched for: %s", ", ".join(binaries)) def skip_if_no_local_network(): """ Helper function to check for existing local network Returns: str: The reason for the skip. None: Should not be skipped. """ check_port = ports.get_unused_localhost_port() has_local_network = False try: with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as pubsock: pubsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) pubsock.bind(("", check_port)) has_local_network = True except OSError: # I wonder if we just have IPV6 support? try: with contextlib.closing(socket.socket(socket.AF_INET6, socket.SOCK_STREAM)) as pubsock: pubsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) pubsock.bind(("", check_port)) has_local_network = True except OSError: # Let's continue pass if has_local_network is False: return "No local network was detected" def skip_if_no_remote_network(): """ Helper function to check for existing remote network(internet) Returns: str: The reason for the skip. None: Should not be skipped. """ # We are using the google.com DNS records as numerical IPs to avoid # DNS look ups which could greatly slow down this check has_remote_network = False for addr in ( "172.217.17.14", "172.217.16.238", "173.194.41.198", "173.194.41.199", "173.194.41.200", "173.194.41.201", "173.194.41.206", "173.194.41.192", "173.194.41.193", "173.194.41.194", "173.194.41.195", "173.194.41.196", "173.194.41.197", "216.58.201.174", ): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(0.25) sock.connect((addr, 80)) sock.close() # We connected? Stop the loop has_remote_network = True break except OSError: # Let's check the next IP continue if has_remote_network is False: return "No internet network connection was detected" def check_required_loader_attributes(loader_instance, loader_attr, required_items): """ :type loader_instance: ~saltfactories.utils.functional.Loaders :param loader_instance: An instance of :py:class:`~saltfactories.utils.functional.Loaders` :param str loader_attr: The name of the minion attribute to check, such as 'modules' or 'states' :param tuple required_items: The items that must be part of the loader attribute for the decorated test :return: The modules that are not available :rtype: set """ required_salt_items = set(required_items) available_items = list(getattr(loader_instance, loader_attr)) not_available_items = set() name = "__not_available_{items}s__".format(items=loader_attr) if not hasattr(loader_instance, name): cached_not_available_items = set() setattr(loader_instance, name, cached_not_available_items) loader_instance._reload_all_funcs.append(cached_not_available_items.clear) else: cached_not_available_items = getattr(loader_instance, name) for not_available_item in cached_not_available_items: if not_available_item in required_salt_items: not_available_items.add(not_available_item) required_salt_items.remove(not_available_item) for required_item_name in required_salt_items: search_name = required_item_name if "." not in search_name: search_name += ".*" if not fnmatch.filter(available_items, search_name): not_available_items.add(required_item_name) cached_not_available_items.add(required_item_name) return not_available_items def evaluate_markers(item): """ Fixtures injection based on markers or test skips based on CLI arguments """ destructive_tests_marker = item.get_closest_marker("destructive_test") if destructive_tests_marker is not None: if destructive_tests_marker.args or destructive_tests_marker.kwargs: raise pytest.UsageError( "The 'destructive_test' marker does not accept any arguments or keyword arguments" ) if item.config.getoption("--run-destructive") is False: item._skipped_by_mark = True pytest.skip("Destructive tests are disabled") expensive_tests_marker = item.get_closest_marker("expensive_test") if expensive_tests_marker is not None: if expensive_tests_marker.args or expensive_tests_marker.kwargs: raise pytest.UsageError( "The 'expensive_test' marker does not accept any arguments or keyword arguments" ) if item.config.getoption("--run-expensive") is False: item._skipped_by_mark = True pytest.skip("Expensive tests are disabled") skip_if_not_root_marker = item.get_closest_marker("skip_if_not_root") if skip_if_not_root_marker is not None: if skip_if_not_root_marker.args or skip_if_not_root_marker.kwargs: raise pytest.UsageError( "The 'skip_if_not_root' marker does not accept any arguments or keyword arguments" ) skip_reason = skip_if_not_root() if skip_reason: item._skipped_by_mark = True pytest.skip(skip_reason) skip_if_binaries_missing_marker = item.get_closest_marker("skip_if_binaries_missing") if skip_if_binaries_missing_marker is not None: binaries = skip_if_binaries_missing_marker.args if not binaries: raise pytest.UsageError( "The 'skip_if_binaries_missing' marker needs at least one binary name to be passed" ) for arg in binaries: if not isinstance(arg, str): raise pytest.UsageError( "The 'skip_if_binaries_missing' marker only accepts strings as arguments. If you are " "trying to pass multiple binaries, each binary should be an separate argument." ) message = skip_if_binaries_missing_marker.kwargs.pop("message", None) if message: item.warn( """Please stop passing 'message="{0}"' and instead pass 'reason="{0}"'""".format( message ) ) skip_if_binaries_missing_marker.kwargs["reason"] = message skip_reason = skip_if_binaries_missing(binaries, **skip_if_binaries_missing_marker.kwargs) if skip_reason: item._skipped_by_mark = True pytest.skip(skip_reason) requires_network_marker = item.get_closest_marker("requires_network") if requires_network_marker is not None: only_local_network = requires_network_marker.kwargs.get("only_local_network", False) local_skip_reason = skip_if_no_local_network() if local_skip_reason: # Since we're only supposed to check local network, and no # local network was detected, skip the test item._skipped_by_mark = True pytest.skip(local_skip_reason) if only_local_network is False: remote_skip_reason = skip_if_no_remote_network() if remote_skip_reason: item._skipped_by_mark = True pytest.skip(remote_skip_reason) # Platform Skip Markers skip_on_windows_marker = item.get_closest_marker("skip_on_windows") if skip_on_windows_marker is not None: if skip_on_windows_marker.args: raise pytest.UsageError("The skip_on_windows marker does not accept any arguments") reason = skip_on_windows_marker.kwargs.pop("reason", None) if skip_on_windows_marker.kwargs: raise pytest.UsageError( "The skip_on_windows marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Skipped on Windows" if saltfactories.utils.platform.is_windows(): item._skipped_by_mark = True pytest.skip(reason) skip_unless_on_windows_marker = item.get_closest_marker("skip_unless_on_windows") if skip_unless_on_windows_marker is not None: if skip_unless_on_windows_marker.args: raise pytest.UsageError( "The skip_unless_on_windows marker does not accept any arguments" ) reason = skip_unless_on_windows_marker.kwargs.pop("reason", None) if skip_unless_on_windows_marker.kwargs: raise pytest.UsageError( "The skip_unless_on_windows marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Platform is not Windows, skipped" if not saltfactories.utils.platform.is_windows(): item._skipped_by_mark = True pytest.skip(reason) skip_on_linux_marker = item.get_closest_marker("skip_on_linux") if skip_on_linux_marker is not None: if skip_on_linux_marker.args: raise pytest.UsageError("The skip_on_linux marker does not accept any arguments") reason = skip_on_linux_marker.kwargs.pop("reason", None) if skip_on_linux_marker.kwargs: raise pytest.UsageError( "The skip_on_linux marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Skipped on Linux" if saltfactories.utils.platform.is_linux(): item._skipped_by_mark = True pytest.skip(reason) skip_unless_on_linux_marker = item.get_closest_marker("skip_unless_on_linux") if skip_unless_on_linux_marker is not None: if skip_unless_on_linux_marker.args: raise pytest.UsageError("The skip_unless_on_linux marker does not accept any arguments") reason = skip_unless_on_linux_marker.kwargs.pop("reason", None) if skip_unless_on_linux_marker.kwargs: raise pytest.UsageError( "The skip_unless_on_linux marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Platform is not Linux, skipped" if not saltfactories.utils.platform.is_linux(): item._skipped_by_mark = True pytest.skip(reason) skip_on_darwin_marker = item.get_closest_marker("skip_on_darwin") if skip_on_darwin_marker is not None: if skip_on_darwin_marker.args: raise pytest.UsageError("The skip_on_darwin marker does not accept any arguments") reason = skip_on_darwin_marker.kwargs.pop("reason", None) if skip_on_darwin_marker.kwargs: raise pytest.UsageError( "The skip_on_darwin marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Skipped on Darwin" if saltfactories.utils.platform.is_darwin(): item._skipped_by_mark = True pytest.skip(reason) skip_unless_on_darwin_marker = item.get_closest_marker("skip_unless_on_darwin") if skip_unless_on_darwin_marker is not None: if skip_unless_on_darwin_marker.args: raise pytest.UsageError( "The skip_unless_on_darwin marker does not accept any arguments" ) reason = skip_unless_on_darwin_marker.kwargs.pop("reason", None) if skip_unless_on_darwin_marker.kwargs: raise pytest.UsageError( "The skip_unless_on_darwin marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Platform is not Darwin, skipped" if not saltfactories.utils.platform.is_darwin(): item._skipped_by_mark = True pytest.skip(reason) skip_on_sunos_marker = item.get_closest_marker("skip_on_sunos") if skip_on_sunos_marker is not None: if skip_on_sunos_marker.args: raise pytest.UsageError("The skip_on_sunos marker does not accept any arguments") reason = skip_on_sunos_marker.kwargs.pop("reason", None) if skip_on_sunos_marker.kwargs: raise pytest.UsageError( "The skip_on_sunos marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Skipped on SunOS" if saltfactories.utils.platform.is_sunos(): item._skipped_by_mark = True pytest.skip(reason) skip_unless_on_sunos_marker = item.get_closest_marker("skip_unless_on_sunos") if skip_unless_on_sunos_marker is not None: if skip_unless_on_sunos_marker.args: raise pytest.UsageError("The skip_unless_on_sunos marker does not accept any arguments") reason = skip_unless_on_sunos_marker.kwargs.pop("reason", None) if skip_unless_on_sunos_marker.kwargs: raise pytest.UsageError( "The skip_unless_on_sunos marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Platform is not SunOS, skipped" if not saltfactories.utils.platform.is_sunos(): item._skipped_by_mark = True pytest.skip(reason) skip_on_smartos_marker = item.get_closest_marker("skip_on_smartos") if skip_on_smartos_marker is not None: if skip_on_smartos_marker.args: raise pytest.UsageError("The skip_on_smartos marker does not accept any arguments") reason = skip_on_smartos_marker.kwargs.pop("reason", None) if skip_on_smartos_marker.kwargs: raise pytest.UsageError( "The skip_on_smartos marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Skipped on SmartOS" if saltfactories.utils.platform.is_smartos(): item._skipped_by_mark = True pytest.skip(reason) skip_unless_on_smartos_marker = item.get_closest_marker("skip_unless_on_smartos") if skip_unless_on_smartos_marker is not None: if skip_unless_on_smartos_marker.args: raise pytest.UsageError( "The skip_unless_on_smartos marker does not accept any arguments" ) reason = skip_unless_on_smartos_marker.kwargs.pop("reason", None) if skip_unless_on_smartos_marker.kwargs: raise pytest.UsageError( "The skip_unless_on_smartos marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Platform is not SmartOS, skipped" if not saltfactories.utils.platform.is_smartos(): item._skipped_by_mark = True pytest.skip(reason) skip_on_freebsd_marker = item.get_closest_marker("skip_on_freebsd") if skip_on_freebsd_marker is not None: if skip_on_freebsd_marker.args: raise pytest.UsageError("The skip_on_freebsd marker does not accept any arguments") reason = skip_on_freebsd_marker.kwargs.pop("reason", None) if skip_on_freebsd_marker.kwargs: raise pytest.UsageError( "The skip_on_freebsd marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Skipped on FreeBSD" if saltfactories.utils.platform.is_freebsd(): item._skipped_by_mark = True pytest.skip(reason) skip_unless_on_freebsd_marker = item.get_closest_marker("skip_unless_on_freebsd") if skip_unless_on_freebsd_marker is not None: if skip_unless_on_freebsd_marker.args: raise pytest.UsageError( "The skip_unless_on_freebsd marker does not accept any arguments" ) reason = skip_unless_on_freebsd_marker.kwargs.pop("reason", None) if skip_unless_on_freebsd_marker.kwargs: raise pytest.UsageError( "The skip_unless_on_freebsd marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Platform is not FreeBSD, skipped" if not saltfactories.utils.platform.is_freebsd(): item._skipped_by_mark = True pytest.skip(reason) skip_on_netbsd_marker = item.get_closest_marker("skip_on_netbsd") if skip_on_netbsd_marker is not None: if skip_on_netbsd_marker.args: raise pytest.UsageError("The skip_on_netbsd marker does not accept any arguments") reason = skip_on_netbsd_marker.kwargs.pop("reason", None) if skip_on_netbsd_marker.kwargs: raise pytest.UsageError( "The skip_on_netbsd marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Skipped on NetBSD" if saltfactories.utils.platform.is_netbsd(): item._skipped_by_mark = True pytest.skip(reason) skip_unless_on_netbsd_marker = item.get_closest_marker("skip_unless_on_netbsd") if skip_unless_on_netbsd_marker is not None: if skip_unless_on_netbsd_marker.args: raise pytest.UsageError( "The skip_unless_on_netbsd marker does not accept any arguments" ) reason = skip_unless_on_netbsd_marker.kwargs.pop("reason", None) if skip_unless_on_netbsd_marker.kwargs: raise pytest.UsageError( "The skip_unless_on_netbsd marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Platform is not NetBSD, skipped" if not saltfactories.utils.platform.is_netbsd(): item._skipped_by_mark = True pytest.skip(reason) skip_on_openbsd_marker = item.get_closest_marker("skip_on_openbsd") if skip_on_openbsd_marker is not None: if skip_on_openbsd_marker.args: raise pytest.UsageError("The skip_on_openbsd marker does not accept any arguments") reason = skip_on_openbsd_marker.kwargs.pop("reason", None) if skip_on_openbsd_marker.kwargs: raise pytest.UsageError( "The skip_on_openbsd marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Skipped on OpenBSD" if saltfactories.utils.platform.is_openbsd(): item._skipped_by_mark = True pytest.skip(reason) skip_unless_on_openbsd_marker = item.get_closest_marker("skip_unless_on_openbsd") if skip_unless_on_openbsd_marker is not None: if skip_unless_on_openbsd_marker.args: raise pytest.UsageError( "The skip_unless_on_openbsd marker does not accept any arguments" ) reason = skip_unless_on_openbsd_marker.kwargs.pop("reason", None) if skip_unless_on_openbsd_marker.kwargs: raise pytest.UsageError( "The skip_unless_on_openbsd marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Platform is not OpenBSD, skipped" if not saltfactories.utils.platform.is_openbsd(): item._skipped_by_mark = True pytest.skip(reason) skip_on_aix_marker = item.get_closest_marker("skip_on_aix") if skip_on_aix_marker is not None: if skip_on_aix_marker.args: raise pytest.UsageError("The skip_on_aix marker does not accept any arguments") reason = skip_on_aix_marker.kwargs.pop("reason", None) if skip_on_aix_marker.kwargs: raise pytest.UsageError( "The skip_on_aix marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Skipped on AIX" if saltfactories.utils.platform.is_aix(): item._skipped_by_mark = True pytest.skip(reason) skip_unless_on_aix_marker = item.get_closest_marker("skip_unless_on_aix") if skip_unless_on_aix_marker is not None: if skip_unless_on_aix_marker.args: raise pytest.UsageError("The skip_unless_on_aix marker does not accept any arguments") reason = skip_unless_on_aix_marker.kwargs.pop("reason", None) if skip_unless_on_aix_marker.kwargs: raise pytest.UsageError( "The skip_unless_on_aix marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Platform is not AIX, skipped" if not saltfactories.utils.platform.is_aix(): item._skipped_by_mark = True pytest.skip(reason) skip_on_aarch64_marker = item.get_closest_marker("skip_on_aarch64") if skip_on_aarch64_marker is not None: if skip_on_aarch64_marker.args: raise pytest.UsageError("The skip_on_aarch64 marker does not accept any arguments") reason = skip_on_aarch64_marker.kwargs.pop("reason", None) if skip_on_aarch64_marker.kwargs: raise pytest.UsageError( "The skip_on_aarch64 marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Skipped on AArch64" if saltfactories.utils.platform.is_aarch64(): item._skipped_by_mark = True pytest.skip(reason) skip_unless_on_aarch64_marker = item.get_closest_marker("skip_unless_on_aarch64") if skip_unless_on_aarch64_marker is not None: if skip_unless_on_aarch64_marker.args: raise pytest.UsageError( "The skip_unless_on_aarch64 marker does not accept any arguments" ) reason = skip_unless_on_aarch64_marker.kwargs.pop("reason", None) if skip_unless_on_aarch64_marker.kwargs: raise pytest.UsageError( "The skip_unless_on_aarch64 marker only accepts 'reason' as a keyword argument." ) if reason is None: reason = "Platform is not AArch64, skipped" if not saltfactories.utils.platform.is_aarch64(): item._skipped_by_mark = True pytest.skip(reason) skip_on_platforms_marker = item.get_closest_marker("skip_on_platforms") if skip_on_platforms_marker is not None: if skip_on_platforms_marker.args: raise pytest.UsageError("The skip_on_platforms marker does not accept any arguments") reason = skip_on_platforms_marker.kwargs.pop("reason", None) if not skip_on_platforms_marker.kwargs: raise pytest.UsageError( "Pass at least one platform to skip_on_platforms as a keyword argument" ) if not any(skip_on_platforms_marker.kwargs.values()): raise pytest.UsageError( "Pass at least one platform with a True value to skip_on_platforms as a keyword argument" ) if reason is None: reason = "Skipped on platform match" try: if saltfactories.utils.platform.on_platforms(**skip_on_platforms_marker.kwargs): item._skipped_by_mark = True pytest.skip(reason) except TypeError as exc: raise pytest.UsageError( "Passed an invalid platform to skip_on_platforms: {}".format(exc) ) skip_unless_on_platforms_marker = item.get_closest_marker("skip_unless_on_platforms") if skip_unless_on_platforms_marker is not None: if skip_unless_on_platforms_marker.args: raise pytest.UsageError( "The skip_unless_on_platforms marker does not accept any arguments" ) reason = skip_unless_on_platforms_marker.kwargs.pop("reason", None) if not skip_unless_on_platforms_marker.kwargs: raise pytest.UsageError( "Pass at least one platform to skip_unless_on_platforms as a keyword argument" ) if not any(skip_unless_on_platforms_marker.kwargs.values()): raise pytest.UsageError( "Pass at least one platform with a True value to skip_unless_on_platforms as a keyword argument" ) if reason is None: reason = "Platform(s) do not match, skipped" try: if not saltfactories.utils.platform.on_platforms( **skip_unless_on_platforms_marker.kwargs ): item._skipped_by_mark = True pytest.skip(reason) except TypeError as exc: raise pytest.UsageError( "Passed an invalid platform to skip_unless_on_platforms: {}".format(exc) ) # Next are two special markers, requires_salt_modules and requires_salt_states. These need access to a # saltfactories.utils.functional.Loader instance # They will use a session_markers_loader fixture to gain access to that requires_salt_modules_marker = item.get_closest_marker("requires_salt_modules") if requires_salt_modules_marker is not None: if requires_salt_modules_marker.kwargs: raise pytest.UsageError( "The 'required_salt_modules' marker does not accept keyword arguments" ) required_salt_modules = requires_salt_modules_marker.args if not required_salt_modules: raise pytest.UsageError( "The 'required_salt_modules' marker needs at least one module name to be passed" ) for arg in required_salt_modules: if not isinstance(arg, str): raise pytest.UsageError( "The 'required_salt_modules' marker only accepts strings as arguments" ) session_markers_loader = item._request.getfixturevalue("session_markers_loader") required_salt_modules = set(required_salt_modules) not_available_modules = check_required_loader_attributes( session_markers_loader, "modules", required_salt_modules ) if not_available_modules: item._skipped_by_mark = True if len(not_available_modules) == 1: pytest.skip("Salt module '{}' is not available".format(*not_available_modules)) pytest.skip("Salt modules not available: {}".format(", ".join(not_available_modules))) requires_salt_states_marker = item.get_closest_marker("requires_salt_states") if requires_salt_states_marker is not None: if requires_salt_states_marker.kwargs: raise pytest.UsageError( "The 'required_salt_states' marker does not accept keyword arguments" ) required_salt_states = requires_salt_states_marker.args if not required_salt_states: raise pytest.UsageError( "The 'required_salt_states' marker needs at least one state module name to be passed" ) for arg in required_salt_states: if not isinstance(arg, str): raise pytest.UsageError( "The 'required_salt_states' marker only accepts strings as arguments" ) session_markers_loader = item._request.getfixturevalue("session_markers_loader") required_salt_states = set(required_salt_states) not_available_states = check_required_loader_attributes( session_markers_loader, "states", required_salt_states ) if not_available_states: item._skipped_by_mark = True if len(not_available_states) == 1: pytest.skip("Salt state module '{}' is not available".format(*not_available_states)) pytest.skip( "Salt state modules not available: {}".format(", ".join(not_available_states)) ) pytest-salt-factories-0.907.0/src/saltfactories/utils/platform.py0000644000175000017500000000710614033044065025730 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE saltfactories.utils.platform ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Platform related utilities """ import sys import salt.utils.platform def is_windows(): """ Simple function to return if a host is Windows or not :return bool: Return true on Windows """ return salt.utils.platform.is_windows() def is_linux(): """ Simple function to return if a host is Linux or not. Note for a proxy minion, we need to return something else :return bool: Return true on Linux """ return salt.utils.platform.is_linux() def is_darwin(): """ Simple function to return if a host is Darwin (macOS) or not :return bool: Return true on Darwin(macOS) """ return salt.utils.platform.is_darwin() def is_sunos(): """ Simple function to return if host is SunOS or not :return bool: Return true on SunOS """ return salt.utils.platform.is_sunos() def is_smartos(): """ Simple function to return if host is SmartOS (Illumos) or not :return bool: Return true on SmartOS (Illumos) """ return salt.utils.platform.is_smartos() def is_freebsd(): """ Simple function to return if host is FreeBSD or not :return bool: Return true on FreeBSD """ return salt.utils.platform.is_freebsd() def is_netbsd(): """ Simple function to return if host is NetBSD or not :return bool: Return true on NetBSD """ return salt.utils.platform.is_netbsd() def is_openbsd(): """ Simple function to return if host is OpenBSD or not :return bool: Return true on OpenBSD """ return salt.utils.platform.is_openbsd() def is_aix(): """ Simple function to return if host is AIX or not :return bool: Return true on AIX """ return salt.utils.platform.is_aix() def is_aarch64(): """ Simple function to return if host is AArch64 or not """ try: return salt.utils.platform.is_aarch64() except AttributeError: # Salt < 3004 return sys.platform.startswith("aarch64") def on_platforms( windows=False, linux=False, darwin=False, sunos=False, smartos=False, freebsd=False, netbsd=False, openbsd=False, aix=False, aarch64=False, ): """ Check to see if we're on one of the provided platforms. :keyword bool windows: When :py:const:`True`, check if running on Windows. :keyword bool linux: When :py:const:`True`, check if running on Linux. :keyword bool darwin: When :py:const:`True`, check if running on Darwin. :keyword bool sunos: When :py:const:`True`, check if running on SunOS. :keyword bool smartos: When :py:const:`True`, check if running on SmartOS. :keyword bool freebsd: When :py:const:`True`, check if running on FreeBSD. :keyword bool netbsd: When :py:const:`True`, check if running on NetBSD. :keyword bool openbsd: When :py:const:`True`, check if running on OpenBSD. :keyword bool aix: When :py:const:`True`, check if running on AIX. :keyword bool aarch64: When :py:const:`True`, check if running on AArch64. """ if windows and is_windows(): return True if linux and is_linux(): return True if darwin and is_darwin(): return True if sunos and is_sunos(): return True if smartos and is_smartos(): return True if freebsd and is_freebsd(): return True if netbsd and is_netbsd(): return True if openbsd and is_openbsd(): return True if aix and is_aix(): return True if aarch64 and is_aarch64(): return True return False pytest-salt-factories-0.907.0/src/saltfactories/utils/ports.py0000644000175000017500000000360614035623322025255 0ustar vampasvampas00000000000000""" saltfactories.utils.ports ~~~~~~~~~~~~~~~~~~~~~~~~~ Ports related utility functions """ import contextlib import logging import pytest from saltfactories.utils import socket log = logging.getLogger(__name__) def get_unused_localhost_port(use_cache=False): """ :keyword bool use_cache: If ``use_cache`` is ``True``, consecutive calls to this function will never return the cached port. Return a random unused port on localhost """ if not isinstance(use_cache, bool): raise pytest.UsageError( "The value of 'use_cache' needs to be an boolean, not {}".format(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 = usock.getsockname()[1] if use_cache: try: cached_ports = get_unused_localhost_port.__cached_ports__ except AttributeError: cached_ports = get_unused_localhost_port.__cached_ports__ = set() 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): """ :param ~collections.abc.Iterable ports: An iterable of ports to try and connect to :rtype: set :return: Returns a set of the ports where connection was successful """ connectable_ports = set() ports = set(ports) for port in set(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: continue return connectable_ports pytest-salt-factories-0.907.0/src/saltfactories/utils/processes.py0000644000175000017500000002764014036257126026126 0ustar vampasvampas00000000000000""" saltfactories.utils.processes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Process related utilities """ import errno import logging import pprint import signal import weakref import attr import psutil log = logging.getLogger(__name__) @attr.s(frozen=True) class ProcessResult: """ This class serves the purpose of having a common result class which will hold the resulting data from a subprocess command. :keyword int exitcode: The exitcode returned by the process :keyword str stdout: The ``stdout`` returned by the process :keyword str stderr: The ``stderr`` returned by the process :keyword list,tuple cmdline: The command line used to start the process .. admonition:: Note Cast :py:class:`~saltfactories.utils.processes.ProcessResult` to a string to pretty-print it. """ exitcode = attr.ib() stdout = attr.ib() stderr = attr.ib() cmdline = attr.ib(default=None, kw_only=True) @exitcode.validator def _validate_exitcode(self, attribute, value): if not isinstance(value, int): raise ValueError("'exitcode' needs to be an integer, not '{}'".format(type(value))) def __str__(self): message = self.__class__.__name__ if self.cmdline: message += "\n Command Line: {}".format(self.cmdline) if self.exitcode is not None: message += "\n Exitcode: {}".format(self.exitcode) if self.stdout or self.stderr: message += "\n Process Output:" if self.stdout: message += "\n >>>>> STDOUT >>>>>\n{}\n <<<<< STDOUT <<<<<".format(self.stdout) if self.stderr: message += "\n >>>>> STDERR >>>>>\n{}\n <<<<< STDERR <<<<<".format(self.stderr) return message + "\n" @attr.s(frozen=True) class ShellResult(ProcessResult): """ This class serves the purpose of having a common result class which will hold the resulting data from a subprocess command. :keyword dict json: The dictionary returned from the process ``stdout`` if it could JSON decode it. Please look at :py:class:`~saltfactories.utils.processes.ProcessResult` for the additional supported keyword arguments documentation. """ json = attr.ib(default=None, kw_only=True) def __str__(self): message = super().__str__().rstrip() if self.json: message += "\n JSON Object:\n" message += "".join(" {}".format(line) for line in pprint.pformat(self.json)) return message + "\n" def __eq__(self, other): """ Allow comparison against the parsed JSON or the output """ if self.json: return self.json == other return self.stdout == other def collect_child_processes(pid): """ Try to collect any started child processes of the provided pid :param int pid: The PID of the process """ # Let's get the child processes of the started subprocess try: parent = psutil.Process(pid) children = parent.children(recursive=True) except psutil.NoSuchProcess: children = [] return children def _get_cmdline(proc): # 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): # 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: try: cmdline = proc.as_dict() except psutil.NoSuchProcess: cmdline = "".format(proc) 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, kill=False, slow_stop=False): 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, kill=False, slow_stop=False): """ Terminate a list of processes :param ~collections.abc.Iterable process_list: An iterable of :py:class:`psutil.Process` instances to terminate :keyword bool kill: Kill the process instead of terminating it. :keyword bool slow_stop: First try to terminate each process in the list, and if termination was not successful, kill it. """ def on_process_terminated(proc): 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=None, process=None, children=None, kill_children=None, slow_stop=False): """ Try to terminate/kill the started process :keyword int pid: The PID of the process :keyword ~psutil.Process process: An instance of :py:class:`psutil.Process` :keyword ~collections.abc.Iterable children: An iterable of :py:class:`psutil.Process` instances, children to the process being terminated :keyword bool kill_children: Also try to terminate/kill child processes :keyword bool slow_stop: First try to terminate each process in the list, and if termination was not successful, kill it. """ children = children or [] process_list = [] 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(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-salt-factories-0.907.0/src/saltfactories/utils/saltext/0000755000175000017500000000000014106144336025215 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/src/saltfactories/utils/saltext/__init__.py0000644000175000017500000000005014062431421027314 0ustar vampasvampas00000000000000""" Salt Extensions =============== """ pytest-salt-factories-0.907.0/src/saltfactories/utils/saltext/engines/0000755000175000017500000000000014106144336026645 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/src/saltfactories/utils/saltext/engines/pytest_engine.py0000644000175000017500000001356314062431421032077 0ustar vampasvampas00000000000000# -*- coding: utf-8 -*- """ pytest_engine ~~~~~~~~~~~~~ Simple salt engine which will setup a socket to accept connections allowing us to know when a daemon is up and running """ import atexit import datetime import logging import threading import zmq try: from collections.abc import MutableMapping except ImportError: # Py2 compat from collections import MutableMapping try: import msgpack HAS_MSGPACK = True except ImportError: HAS_MSGPACK = False import salt.utils.event try: import salt.utils.immutabletypes as immutabletypes except ImportError: immutabletypes = None try: from salt.utils.data import CaseInsensitiveDict except ImportError: CaseInsensitiveDict = None log = logging.getLogger(__name__) __virtualname__ = "pytest" def __virtual__(): role = __opts__["__role"] pytest_key = "pytest-{}".format(role) if pytest_key not in __opts__: return False, "No '{}' key in opts dictionary".format(pytest_key) pytest_config = __opts__[pytest_key] if "returner_address" not in pytest_config: return False, "No 'returner_address' key in opts['{}'] dictionary".format(pytest_key) if HAS_MSGPACK is False: return False, "msgpack was not importable. Please install msgpack." return True def start(): opts = __opts__ # pylint: disable=undefined-variable try: pytest_engine = PyTestEventForwardEngine(opts=opts) pytest_engine.start() except Exception: # pragma: no cover pylint: disable=broad-except log.error("Failed to start PyTestEventForwardEngine", exc_info=True) raise def ext_type_encoder(obj): """ Convert any types that msgpack cannot handle on it's own """ if isinstance(obj, (datetime.datetime, datetime.date)): # msgpack doesn't support datetime.datetime and datetime.date datatypes. return obj.strftime("%Y%m%dT%H:%M:%S.%f") # The same for immutable types elif immutabletypes is not None and isinstance(obj, immutabletypes.ImmutableDict): return dict(obj) elif immutabletypes is not None and isinstance(obj, immutabletypes.ImmutableList): return list(obj) elif immutabletypes is not None and isinstance(obj, immutabletypes.ImmutableSet): # msgpack can't handle set so translate it to tuple return tuple(obj) elif isinstance(obj, set): # msgpack can't handle set so translate it to tuple return tuple(obj) elif CaseInsensitiveDict is not None and isinstance(obj, CaseInsensitiveDict): return dict(obj) elif isinstance(obj, MutableMapping): return dict(obj) # Nothing known exceptions found. Let msgpack raise its own. return obj class PyTestEventForwardEngine: __slots__ = ("opts", "id", "role", "returner_address", "running_event") def __init__(self, opts): self.opts = opts self.id = self.opts["id"] self.role = self.opts["__role"] self.returner_address = self.opts["pytest-{}".format(self.role)]["returner_address"] self.running_event = threading.Event() def __repr__(self): return "<{} role={!r} id={!r}, returner_address={!r} running={!r}>".format( self.__class__.__name__, self.role, self.id, self.returner_address, self.running_event.is_set(), ) def start(self): if self.running_event.is_set(): return log.info("%s is starting", self) atexit.register(self.stop) self.running_event.set() try: context = zmq.Context() push = context.socket(zmq.PUSH) log.debug("%s connecting PUSH socket to %s", self, self.returner_address) push.connect(self.returner_address) opts = self.opts.copy() opts["file_client"] = "local" with salt.utils.event.get_event( self.role, sock_dir=opts["sock_dir"], transport=opts["transport"], opts=opts, listen=True, ) as eventbus: if self.role == "master": event_tag = "salt/master/{}/start".format(self.id) log.info("%s firing event on engine start. Tag: %s", self, event_tag) load = {"id": self.id, "tag": event_tag, "data": {}} eventbus.fire_event(load, event_tag) log.info("%s started", self) while self.running_event.is_set(): for event in eventbus.iter_events(full=True, auto_reconnect=True): if not event: continue tag = event["tag"] data = event["data"] log.debug("%s Received Event; TAG: %r DATA: %r", self, tag, data) forward = (self.id, tag, data) try: dumped = msgpack.dumps( forward, use_bin_type=True, default=ext_type_encoder ) push.send(dumped) log.info("%s forwarded event: %r", self, forward) except Exception: # pragma: no cover pylint: disable=broad-except log.error( "%s failed to forward event: %r", self, forward, exc_info=True ) finally: if self.running_event.is_set(): # Some exception happened, unset self.running_event.clear() if not push.closed: push.close(1500) if not context.closed: context.term() def stop(self): if self.running_event.is_set() is False: return log.info("Stopping %s", self) self.running_event.clear() log.info("%s stopped", self) pytest-salt-factories-0.907.0/src/saltfactories/utils/saltext/log_handlers/0000755000175000017500000000000014106144336027656 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/src/saltfactories/utils/saltext/log_handlers/__init__.py0000644000175000017500000000000014062431421031750 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/src/saltfactories/utils/saltext/log_handlers/pytest_log_handler.py0000644000175000017500000003177014063562634034135 0ustar vampasvampas00000000000000""" pytest_log_handler ~~~~~~~~~~~~~~~~~~ Salt External Logging Handler """ import copy import logging import os import pprint import socket import sys import time import traceback try: from salt.utils.stringutils import to_unicode except ImportError: # pragma: no cover # This likely due to running backwards compatibility tests against older minions from salt.utils import to_unicode try: from salt._logging.impl import LOG_LEVELS from salt._logging.mixins import ExcInfoOnLogLevelFormatMixin except ImportError: # pragma: no cover # This likely due to running backwards compatibility tests against older minions from salt.log.setup import LOG_LEVELS from salt.log.mixins import ExcInfoOnLogLevelFormatMixIn as ExcInfoOnLogLevelFormatMixin try: from salt._logging.mixins import NewStyleClassMixin except ImportError: # pragma: no cover try: # This likely due to running backwards compatibility tests against older minions from salt.log.mixins import NewStyleClassMixIn as NewStyleClassMixin except ImportError: # pragma: no cover # NewStyleClassMixin was removed from salt class NewStyleClassMixin(object): """ A copy of Salt's previous NewStyleClassMixin implementation """ try: import msgpack HAS_MSGPACK = True except ImportError: # pragma: no cover HAS_MSGPACK = False try: import zmq HAS_ZMQ = True except ImportError: # pragma: no cover HAS_ZMQ = False __virtualname__ = "pytest_log_handler" log = logging.getLogger(__name__) def __virtual__(): role = __opts__["__role"] pytest_key = "pytest-{}".format(role) pytest_config = __opts__[pytest_key] if "log" not in pytest_config: return False, "No 'log' key in opts {} dictionary".format(pytest_key) log_opts = pytest_config["log"] if "port" not in log_opts: return ( False, "No 'port' key in opts['pytest']['log'] or opts['pytest'][{}]['log']".format( __opts__["role"] ), ) if HAS_MSGPACK is False: return False, "msgpack was not importable. Please install msgpack." if HAS_ZMQ is False: return False, "zmq was not importable. Please install pyzmq." return True def setup_handlers(): role = __opts__["__role"] pytest_key = "pytest-{}".format(role) pytest_config = __opts__[pytest_key] log_opts = pytest_config["log"] if log_opts.get("disabled"): return host_addr = log_opts.get("host") if not host_addr: import subprocess if log_opts["pytest_windows_guest"] is True: proc = subprocess.Popen("ipconfig", stdout=subprocess.PIPE) for line in proc.stdout.read().strip().encode(__salt_system_encoding__).splitlines(): if "Default Gateway" in line: parts = line.split() host_addr = parts[-1] break else: proc = subprocess.Popen( "netstat -rn | grep -E '^0.0.0.0|default' | awk '{ print $2 }'", shell=True, stdout=subprocess.PIPE, ) host_addr = proc.stdout.read().strip().encode(__salt_system_encoding__) host_port = log_opts["port"] sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.connect((host_addr, host_port)) except OSError as exc: # Don't even bother if we can't connect log.warning("Cannot connect back to log server at %s:%d: %s", host_addr, host_port, exc) return finally: sock.close() pytest_log_prefix = log_opts.get("prefix") try: level = LOG_LEVELS[(log_opts.get("level") or "error").lower()] except KeyError: level = logging.ERROR handler = ZMQHandler(host=host_addr, port=host_port, log_prefix=pytest_log_prefix, level=level) handler.setLevel(level) handler.start() return handler class ZMQHandler(ExcInfoOnLogLevelFormatMixin, logging.Handler, NewStyleClassMixin): # We implement a lazy start approach which is deferred until sending the # first message because, logging handlers, on platforms which support # forking, are inherited by forked processes, and we want to minimize the ZMQ # machinery inherited. # For the cases where the ZMQ machinery is still inherited because a # process was forked after ZMQ has been prepped up, we check the handler's # pid attribute against the current process pid. If it's not a match, we # reconnect the ZMQ machinery. def __init__( self, host="127.0.0.1", port=3330, log_prefix=None, level=logging.NOTSET, socket_hwm=100000 ): super(ZMQHandler, self).__init__(level=level) self.host = host self.port = port self._log_prefix = log_prefix self.socket_hwm = socket_hwm self.log_prefix = self._get_log_prefix(log_prefix) self.context = self.pusher = None self._exiting = False self.dropped_messages_count = 0 # We set the formatter so that we only include the actual log message and not any other # fields found in the log record self.__formatter = logging.Formatter("%(message)s") self.pid = os.getpid() def _get_formatter(self): return self.__formatter def _set_formatter(self, fmt): if fmt is not None: self.setFormatter(fmt) def _del_formatter(self): raise RuntimeError("Cannot delete the 'formatter' attribute") # We set formatter as a property to make it immutable formatter = property(_get_formatter, _set_formatter, _del_formatter) def setFormatter(self, _): raise RuntimeError("Do not set a formatter on {}".format(self.__class__.__name__)) def __getstate__(self): return { "host": self.host, "port": self.port, "log_prefix": self._log_prefix, "level": self.level, "socket_hwm": self.socket_hwm, } def __setstate__(self, state): self.__init__(**state) self.stop() self._exiting = False def __repr__(self): return "<{} host={} port={} level={}>".format( self.__class__.__name__, self.host, self.port, logging.getLevelName(self.level) ) def _get_log_prefix(self, log_prefix): if log_prefix is None: return if sys.argv[0] == sys.executable: cli_arg_idx = 1 else: cli_arg_idx = 0 cli_name = os.path.basename(sys.argv[cli_arg_idx]) return log_prefix.format(cli_name=cli_name) def start(self): if self.pid != os.getpid(): self.stop() self._exiting = False if self._exiting is True: return if self.pusher is not None: # We're running ... return self.dropped_messages_count = 0 context = pusher = None try: context = zmq.Context() self.context = context except zmq.ZMQError as exc: sys.stderr.write( "Failed to create the ZMQ Context: {}\n{}\n".format(exc, traceback.format_exc()) ) sys.stderr.flush() self.stop() # Allow the handler to re-try starting self._exiting = False return try: pusher = context.socket(zmq.PUSH) pusher.set_hwm(self.socket_hwm) pusher.connect("tcp://{}:{}".format(self.host, self.port)) self.pusher = pusher except zmq.ZMQError as exc: if pusher is not None: pusher.close(0) sys.stderr.write( "Failed to connect the ZMQ PUSH socket: {}\n{}\n".format( exc, traceback.format_exc() ) ) sys.stderr.flush() self.stop() # Allow the handler to re-try starting self._exiting = False return self.pid = os.getpid() def stop(self, flush=True): if self._exiting: return self._exiting = True if self.dropped_messages_count: sys.stderr.write( "Dropped {} messages from getting forwarded. High water mark reached...\n".format( self.dropped_messages_count ) ) sys.stderr.flush() try: if self.pusher is not None and not self.pusher.closed: if flush: # Give it 1.5 seconds to flush any messages in it's queue linger = 1500 else: linger = 0 self.pusher.close(linger) self.pusher = None if self.context is not None and not self.context.closed: self.context.term() self.context = None except (SystemExit, KeyboardInterrupt): # pragma: no cover pylint: disable=try-except-raise # Don't write these exceptions to stderr raise except Exception as exc: # pragma: no cover pylint: disable=broad-except sys.stderr.write( "Failed to terminate ZMQHandler: {}\n{}\n".format(exc, traceback.format_exc()) ) sys.stderr.flush() raise finally: if self.pusher is not None and not self.pusher.closed: self.pusher.close(0) self.pusher = None if self.context is not None and not self.context.closed: self.context.term() self.context = None self.pid = None def format(self, record): msg = super(ZMQHandler, self).format(record) if self.log_prefix: msg = "[{}] {}".format(to_unicode(self.log_prefix), to_unicode(msg)) return msg def prepare(self, record): msg = self.format(record) record = copy.copy(record) record.msg = msg # Reduce network bandwidth, we don't need these any more record.args = None record.exc_info = None record.exc_text = None record.message = None # redundant with msg # On Python >= 3.5 we also have stack_info, but we've formatted already so, reset it record.stack_info = None try: return msgpack.dumps(record.__dict__, use_bin_type=True) except TypeError as exc: # Failed to serialize something with msgpack sys.stderr.write( "Failed to serialize log record:{}.\n{}\nLog Record:\n{}\n".format( exc, traceback.format_exc(), pprint.pformat(record.__dict__) ) ) sys.stderr.flush() self.handleError(record) def emit(self, record): """ Emit a record. Writes the LogRecord to the queue, preparing it for pickling first. """ # Python's logging machinery acquires a lock before calling this method # that's why it's safe to call the start method without an explicit acquire if self._exiting: return self.start() if self.pusher is None: sys.stderr.write( "Not sending log message over the wire because " "we were unable to connect to the log server.\n" ) sys.stderr.flush() return try: msg = self.prepare(record) if msg: try: self._send_message(msg) except zmq.error.Again: # Sleep a little and give up time.sleep(0.001) try: self._send_message(msg) except zmq.error.Again: # We can't send it nor queue it for send. # Drop it, otherwise, this call blocks until we can at least queue the message self.dropped_messages_count += 1 except (SystemExit, KeyboardInterrupt): # pragma: no cover pylint: disable=try-except-raise # Catch and raise SystemExit and KeyboardInterrupt so that we can handle # all other exception below self.stop(flush=False) raise except Exception: # pragma: no cover pylint: disable=broad-except self.handleError(record) def _send_message(self, msg): self.pusher.send(msg, flags=zmq.NOBLOCK) if self.dropped_messages_count: logging.getLogger(__name__).debug( "Dropped %s messages from getting forwarded. High water mark reached...", self.dropped_messages_count, ) self.dropped_messages_count = 0 def close(self): """ Tidy up any resources used by the handler. """ # The logging machinery has asked to stop this handler self.stop(flush=False) # self._exiting should already be True, nonetheless, we set it here # too to ensure the handler doesn't get a chance to restart itself self._exiting = True super().close() pytest-salt-factories-0.907.0/src/saltfactories/utils/socket.py0000644000175000017500000000064414033044065025374 0ustar vampasvampas00000000000000""" saltfactories.utils.socket ========================== This module's sole purpose is to have the standard library :py:mod:`socket` module functions under a different namespace to be used in salt-factories so that projects using it, which need to mock :py:mod:`socket` functions, don't influence the salt-factories run time behavior. """ from socket import * # pylint: disable=wildcard-import,unused-wildcard-import pytest-salt-factories-0.907.0/src/saltfactories/utils/tempfiles.py0000644000175000017500000002537114045026371026103 0ustar vampasvampas00000000000000""" .. PYTEST_DONT_REWRITE saltfactories.utils.tempfiles ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Temporary files utilities """ import logging import os import pathlib import shutil import tempfile import textwrap from contextlib import contextmanager import attr log = logging.getLogger(__name__) @contextmanager def temp_directory(name=None, basepath=None): """ This helper creates a temporary directory. It should be used as a context manager which returns the temporary directory path, and, once out of context, deletes it. :keyword str name: The name of the directory to create :keyword basepath name: The base path of where to create the directory. Defaults to :py:func:`~tempfile.gettempdir` :rtype: pathlib.Path Can be directly imported and used: .. code-block:: python from saltfactories.utils.tempfiles import temp_directory def test_func(): with temp_directory() as temp_path: assert temp_path.is_dir() assert not temp_path.is_dir() is False Or, it can be used as a pytest helper function: .. code-block:: python import pytest def test_blah(): with pytest.helpers.temp_directory() as temp_path: assert temp_path.is_dir() assert not temp_path.is_dir() is False """ if basepath is None: basepath = pathlib.Path(tempfile.gettempdir()) try: if name is not None: directory_path = basepath / name else: directory_path = pathlib.Path(tempfile.mkdtemp(dir=str(basepath))) if not directory_path.is_dir(): directory_path.mkdir(parents=True) log.debug("Created temp directory: %s", directory_path) yield directory_path finally: created_directory = directory_path while True: if str(created_directory) == str(basepath): break if not any(created_directory.iterdir()): shutil.rmtree(str(created_directory), ignore_errors=True) log.debug("Deleted temp directory: %s", created_directory) else: log.debug("Not deleting %s because it's not empty", created_directory) created_directory = created_directory.parent @contextmanager def temp_file(name=None, contents=None, directory=None, strip_first_newline=True): """ This helper creates a temporary file. It should be used as a context manager which returns the temporary file path, and, once out of context, deletes it. :keyword str name: The temporary file name :keyword str contents: The contents of the temporary file :keyword str,pathlib.Path directory: The directory where to create the temporary file. Defaults to the value of :py:func:`~tempfile.gettempdir` :keyword bool strip_first_newline: Either strip the initial first new line char or not. :rtype: pathlib.Path Can be directly imported and used: .. code-block:: python from saltfactories.utils.tempfiles import temp_file def test_func(): with temp_file(name="blah.txt") as temp_path: assert temp_path.is_file() assert not temp_path.is_file() is False Or, it can be used as a pytest helper function: .. code-block:: python import pytest def test_blah(): with pytest.helpers.temp_file("blah.txt") as temp_path: assert temp_path.is_file() assert not temp_path.is_file() is False """ if directory is None: directory = tempfile.gettempdir() if not isinstance(directory, pathlib.Path): directory = pathlib.Path(str(directory)) if name is not None: file_path = directory / name else: handle, file_path = tempfile.mkstemp(dir=str(directory)) os.close(handle) file_path = pathlib.Path(file_path) # Find out if we were given sub-directories on `name` create_directories = file_path.parent.relative_to(directory) if create_directories: with temp_directory(create_directories, basepath=directory): with _write_or_touch(file_path, contents, strip_first_newline=strip_first_newline): yield file_path else: with _write_or_touch(file_path, contents, strip_first_newline=strip_first_newline): yield file_path @contextmanager def _write_or_touch(file_path, contents, strip_first_newline=True): try: if contents is not None: if contents: if contents.startswith("\n") and strip_first_newline: contents = contents[1:] file_contents = textwrap.dedent(contents) else: file_contents = contents file_path.write_text(file_contents) log_contents = "{0} Contents of {1}\n{2}\n{3} Contents of {1}".format( ">" * 6, file_path, file_contents, "<" * 6 ) log.debug("Created temp file: %s\n%s", file_path, log_contents) else: file_path.touch() log.debug("Touched temp file: %s", file_path) yield finally: if file_path.exists(): file_path.unlink() log.debug("Deleted temp file: %s", file_path) @attr.s(kw_only=True, slots=True) class SaltEnv: """ This helper class represent a Salt Environment, either for states or pillar. It's base purpose it to handle temporary file creation/deletion during testing. :keyword str saltenv: The salt environment name, commonly, 'base' or 'prod' :keyword list paths: The salt environment list of paths. .. admonition:: Note The first entry in this list, is the path that will get used to create temporary files in. """ saltenv = attr.ib() paths = attr.ib(default=attr.Factory(list)) write_path = attr.ib(init=False) def __attrs_post_init__(self): for idx, path in enumerate(self.paths[:]): if not isinstance(path, pathlib.Path): # We have to cast path to a string because on Py3.5, path might be an instance of pathlib2.Path path = pathlib.Path(str(path)) self.paths[idx] = path path.mkdir(parents=True, exist_ok=True) self.write_path = self.paths[0] def temp_file(self, name, contents=None, strip_first_newline=True): """ Create a temporary file within this saltenv. Please check :py:func:`saltfactories.utils.temp_file` for documentation. """ return temp_file( name=name, contents=contents, directory=self.write_path, strip_first_newline=strip_first_newline, ) @attr.s(kw_only=True) class SaltEnvs: """ This class serves as a container for multiple salt environments for states or pillar. :keyword dict envs: The `envs` dictionary should be a mapping of a string as key, the `saltenv`, commonly 'base' or 'prod', and the value an instance of :py:class:`~saltfactories.utils.SaltEnv` or a list of strings(paths). In the case where a list of strings(paths) is passed, it is converted to an instance of :py:class:`~saltfactories.utils.SaltEnv` To provide a better user experience, the salt environments can be accessed as attributes of this class. .. code-block:: python envs = SaltEnvs( { "base": [ "/path/to/base/env", ], "prod": [ "/path/to/prod/env", ], } ) with envs.base.temp_file("foo.txt", "foo contents") as base_foo_path: ... with envs.prod.temp_file("foo.txt", "foo contents") as prod_foo_path: ... """ envs = attr.ib() def __attrs_post_init__(self): for envname, envtree in self.envs.items(): if not isinstance(envtree, SaltEnv): if isinstance(envtree, str): envtree = [envtree] self.envs[envname] = SaltEnv(saltenv=envname, paths=envtree) setattr(self, envname, self.envs[envname]) @attr.s(kw_only=True) class SaltStateTree(SaltEnvs): """ Helper class which handles temporary file creation within the state tree. :keyword dict envs: A mapping of a ``saltenv`` to a list of paths. .. code-block:: python envs = { "base": [ "/path/to/base/env", "/another/path/to/base/env", ], "prod": [ "/path/to/prod/env", "/another/path/to/prod/env", ], } The state tree environments can be accessed by attribute: .. code-block:: python # See example of envs definition above state_tree = SaltStateTree(envs=envs) # To access the base saltenv base = state_tree.envs["base"] # Alternatively, in a simpler form base = state_tree.base .. admonition:: Attention The temporary files created by the :py:meth:`~saltfactories.utils.tempfiles.SaltEnv.temp_file` are written to the first path passed when instantiating the ``SaltStateTree``. .. code-block:: python # Given the example mapping shown above ... with state_tree.base.temp_file("foo.sls") as path: assert path == "/path/to/base/env" """ @attr.s(kw_only=True) class SaltPillarTree(SaltEnvs): """ Helper class which handles temporary file creation within the pillar tree. :keyword dict envs: A mapping of a ``saltenv`` to a list of paths. .. code-block:: python envs = { "base": [ "/path/to/base/env", "/another/path/to/base/env", ], "prod": [ "/path/to/prod/env", "/another/path/to/prod/env", ], } The pillar tree environments can be accessed by attribute: .. code-block:: python # See example of envs definition above pillar_tree = SaltPillarTree(envs=envs) # To access the base saltenv base = pillar_tree.envs["base"] # Alternatively, in a simpler form base = pillar_tree.base .. admonition:: Attention The temporary files created by the :py:meth:`~saltfactories.utils.tempfiles.SaltEnv.temp_file` are written to the first path passed when instantiating the ``SaltPillarTree``. .. code-block:: python # Given the example mapping shown above ... with pillar_tree.base.temp_file("foo.sls") as path: assert path == "/path/to/base/env" """ pytest-salt-factories-0.907.0/src/saltfactories/utils/time.py0000644000175000017500000000063214033044065025037 0ustar vampasvampas00000000000000""" saltfactories.utils.time ======================== This module's sole purpose is to have the standard library :py:mod:`time` module functions under a different namespace to be used in salt-factories so that projects using it, which need to mock :py:mod:`time` functions, don't influence the salt-factories run time behavior. """ from time import * # pylint: disable=wildcard-import,unused-wildcard-import pytest-salt-factories-0.907.0/src/saltfactories/utils/virtualenv.py0000644000175000017500000001606014062634703026310 0ustar vampasvampas00000000000000import json import logging import os import pathlib import shutil import subprocess import sys import tempfile import textwrap import attr from saltfactories.exceptions import ProcessFailed from saltfactories.utils import platform from saltfactories.utils.processes import ProcessResult log = logging.getLogger(__name__) def _cast_to_pathlib_path(value): if isinstance(value, pathlib.Path): return value return pathlib.Path(str(value)) @attr.s(frozen=True, slots=True) class VirtualEnv: """ Helper class to create and use a virtual environment :keyword str,~pathlib.Path venv_dir: The path to the directory where the virtual environment should be created :keyword list venv_create_args: Additional list of strings to pass when creating the virtualenv :keyword dict env: Additional environment entries :keyword str,~pathlib.Path cwd: The default ``cwd`` to use. Can be overridden when calling :py:func:`~saltfactories.utils.virtualenv.VirtualEnv.run` and :py:func:`~saltfactories.utils.virtualenv.VirtualEnv.install` .. code-block:: python with VirtualEnv("/tmp/venv") as venv: venv.install("pep8") assert "pep8" in venv.get_installed_packages() """ venv_dir = attr.ib(converter=_cast_to_pathlib_path) venv_create_args = attr.ib(default=attr.Factory(list)) env = attr.ib(default=None) cwd = attr.ib(default=None) environ = attr.ib(init=False, repr=False) venv_python = attr.ib(init=False, repr=False) venv_bin_dir = attr.ib(init=False, repr=False) @venv_dir.default def _default_venv_dir(self): return pathlib.Path(tempfile.mkdtemp(dir=tempfile.gettempdir())) @environ.default def _default_environ(self): environ = os.environ.copy() if self.env: environ.update(self.env) return environ @venv_python.default def _default_venv_python(self): # Once we drop Py3.5 we can stop casting to string if platform.is_windows(): return str(self.venv_dir / "Scripts" / "python.exe") return str(self.venv_dir / "bin" / "python") @venv_bin_dir.default def _default_venv_bin_dir(self): return pathlib.Path(self.venv_python).parent def __enter__(self): try: self._create_virtualenv() except subprocess.CalledProcessError as exc: raise AssertionError("Failed to create virtualenv") from exc return self def __exit__(self, *args): shutil.rmtree(str(self.venv_dir), ignore_errors=True) def install(self, *args, **kwargs): return self.run(self.venv_python, "-m", "pip", "install", *args, **kwargs) def run(self, *args, **kwargs): """ Run a shell command :rtype: ~saltfactories.utils.processes.ProcessResult """ check = kwargs.pop("check", True) kwargs.setdefault("cwd", str(self.cwd or self.venv_dir)) kwargs.setdefault("stdout", subprocess.PIPE) kwargs.setdefault("stderr", subprocess.PIPE) kwargs.setdefault("universal_newlines", True) kwargs.setdefault("env", self.environ) proc = subprocess.run(args, check=False, **kwargs) ret = ProcessResult( exitcode=proc.returncode, stdout=proc.stdout, stderr=proc.stderr, cmdline=proc.args, ) log.debug(ret) if check is True: try: proc.check_returncode() except subprocess.CalledProcessError as exc: # pragma: no cover raise ProcessFailed( "Command failed return code check", cmdline=proc.args, stdout=proc.stdout, stderr=proc.stderr, exitcode=proc.returncode, ) from exc return ret @staticmethod def get_real_python(): """ The reason why the virtualenv creation is proxied by this function is mostly because under windows, we can't seem to properly create a virtualenv off of another virtualenv(we can on Linux) and also because, we really don't want to test virtualenv creation off of another virtualenv, we want a virtualenv created from the original python. Also, on windows, we must also point to the virtualenv binary outside the existing virtualenv because it will fail otherwise """ try: if platform.is_windows(): return os.path.join(sys.real_prefix, os.path.basename(sys.executable)) else: python_binary_names = [ "python{}.{}".format(*sys.version_info), "python{}".format(*sys.version_info), "python", ] for binary_name in python_binary_names: python = os.path.join(sys.real_prefix, "bin", binary_name) if os.path.exists(python): break else: raise AssertionError( "Couldn't find a python binary name under '{}' matching: {}".format( os.path.join(sys.real_prefix, "bin"), python_binary_names ) ) return python except AttributeError: return sys.executable def run_code(self, code_string, **kwargs): """ Run python code using the virtualenv python environment :param str code_string: The code string to run against the virtualenv python interpreter """ if code_string.startswith("\n"): code_string = code_string[1:] code_string = textwrap.dedent(code_string).rstrip() log.debug("Code to run passed to python:\n>>>>>>>>>>\n%s\n<<<<<<<<<<", code_string) return self.run(str(self.venv_python), "-c", code_string, **kwargs) def get_installed_packages(self): """ Get a dictionary of the installed packages where the keys are the package names and the values their versions """ data = {} ret = self.run(str(self.venv_python), "-m", "pip", "list", "--format", "json") for pkginfo in json.loads(ret.stdout): data[pkginfo["name"]] = pkginfo["version"] return data def _create_virtualenv(self): args = [ self.get_real_python(), "-m", "virtualenv", ] passed_python = False for arg in self.venv_create_args: if arg.startswith(("--python", "--python=")): passed_python = True args.append(arg) if passed_python is False: args.append("--python={}".format(self.get_real_python())) args.append(str(self.venv_dir)) # We pass CWD because run defaults to the venv_dir, which, at this stage # is not yet created self.run(*args, cwd=os.getcwd()) self.install("-U", "pip", "setuptools!=50.*,!=51.*,!=52.*") log.debug("Created virtualenv in %s", self.venv_dir) pytest-salt-factories-0.907.0/src/saltfactories/version.py0000644000175000017500000000005514106144335024427 0ustar vampasvampas00000000000000# pylint: skip-file __version__ = "0.907.0" pytest-salt-factories-0.907.0/tests/0000755000175000017500000000000014106144336020101 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/__init__.py0000644000175000017500000000000013720712732022202 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/conftest.py0000644000175000017500000001113614042504036022276 0ustar vampasvampas00000000000000import functools import logging import os import pathlib import stat import tempfile import textwrap import pytest import salt.version log = logging.getLogger(__name__) TESTS_PATH = pathlib.Path(__file__).resolve().parent 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): return pkg_resources.get_distribution(package).version def pkg_version_info(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: @pytest.fixture def pytester(): pytest.skip("The pytester fixture is not available in Pytest < 6.2.0") def pytest_report_header(): return "salt-version: {}".format(salt.version.__version__) class Tempfiles: """ Class which generates temporary files and cleans them when done """ def __init__(self, request): self.request = request def makepyfile(self, contents, prefix=None, executable=False): """ 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) 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 makeslsfile(self, contents, name=None): """ Creates an sls file and returns it's path """ if name is None: tfile = tempfile.NamedTemporaryFile("w", suffix=".sls", delete=False) name = tfile.name with open(name, "w") as wfh: contents = textwrap.dedent(contents.lstrip("\n")).strip() wfh.write(contents) self.request.addfinalizer(functools.partial(self._delete_temp_file, name)) with open(name) as rfh: log.debug( "Created SLS file with contents:\n>>>>> %s >>>>>\n%s\n<<<<< %s <<<<<\n", name, rfh.read(), name, ) return name def _delete_temp_file(self, fpath): """ Cleanup the temporary path """ if os.path.exists(fpath): os.unlink(fpath) @pytest.fixture def tempfiles(request): """ Temporary files fixture """ return Tempfiles(request) @pytest.fixture(scope="session") def salt_version(): return pkg_version("salt") @pytest.mark.trylast def pytest_configure(config): """ called after command line options have been parsed and all plugins and initial conftest files been loaded. """ # Expose the markers we use to pytest CLI config.addinivalue_line( "markers", "skip_on_salt_system_install: Marker to skip tests when testing" "against salt installed in the system.", ) @pytest.hookimpl(tryfirst=True) def pytest_runtest_setup(item): salt_factories_fixture = item._request.getfixturevalue("salt_factories") if salt_factories_fixture.system_install is False: return system_install_skip_paths = ( # There's no point on running these tests against a system install of salt str(TESTS_PATH / "unit"), str(TESTS_PATH / "functional"), str(TESTS_PATH / "scenarios" / "examples"), str(TESTS_PATH / "integration" / "factories" / "cli"), str(TESTS_PATH / "integration" / "factories" / "daemons" / "sshd"), str(TESTS_PATH / "integration" / "factories" / "daemons" / "container"), ) if str(item.fspath).startswith(system_install_skip_paths): item._skipped_by_mark = True pytest.skip("Test should not run against system install of Salt") skip_on_salt_system_install_marker = item.get_closest_marker("skip_on_salt_system_install") if skip_on_salt_system_install_marker is not None: item._skipped_by_mark = True pytest.skip("Test should not run against system install of Salt") pytest-salt-factories-0.907.0/tests/functional/0000755000175000017500000000000014106144336022243 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/__init__.py0000644000175000017500000000000013720712732024344 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/factories/0000755000175000017500000000000014106144336024222 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/factories/__init__.py0000644000175000017500000000000013720712732026323 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/factories/base/0000755000175000017500000000000014106144336025134 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/factories/base/__init__.py0000644000175000017500000000000013723372562027243 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/factories/base/test_daemon_factory.py0000644000175000017500000004243614036250542031547 0ustar vampasvampas00000000000000import functools import logging import pprint import re import sys import time import attr import psutil import pytest from saltfactories.bases import Daemon from saltfactories.exceptions import FactoryNotRunning from saltfactories.exceptions import FactoryNotStarted from saltfactories.utils import platform from saltfactories.utils.processes import _get_cmdline PROCESS_START_TIMEOUT = 2 log = logging.getLogger(__name__) def kill_children(procs): # pragma: no cover _, alive = psutil.wait_procs(procs, timeout=3) for p in alive: p.kill() def test_daemon_process_termination(request, tempfiles): 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(): factory_kwargs = dict(script_name=script) else: # Windows don't know how to handle python scripts directly factory_kwargs = dict(script_name=sys.executable, base_script_args=[script]) daemon = Daemon(start_timeout=1, **factory_kwargs) 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[:2] == (3, 7): # Under Python 3.7 and Windows we always seem to get +1 child # XXX: Don't forget to look what this extra child is 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, tempfiles): 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(): factory_kwargs = dict(script_name=script) else: # Windows don't know how to handle python scripts directly factory_kwargs = dict(script_name=sys.executable, base_script_args=[script]) daemon = Daemon(start_timeout=1, **factory_kwargs) 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, tempfiles, start_timeout): 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): 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, factory_stopped_script): 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 def test_stopped_context_manager(request, factory_stopped_script): 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 CallbackState: daemon = attr.ib() before_stop_callback_called = attr.ib(default=False) after_stop_callback_called = attr.ib(default=False) before_start_callback_called = attr.ib(default=False) after_start_callback_called = attr.ib(default=False) def before_stop_callback(self, daemon): assert daemon is self.daemon self.before_stop_callback_called = True def after_stop_callback(self, daemon): assert daemon is self.daemon self.after_stop_callback_called = True def before_start_callback(self, daemon): assert daemon is self.daemon self.before_start_callback_called = True def after_start_callback(self, daemon): assert daemon is self.daemon self.after_start_callback_called = True def test_stopped_context_manager_callbacks(request, factory_stopped_script): 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) daemon_started_once = False with daemon.started(): daemon_started_once = daemon.is_running() assert daemon_started_once is True callbacks = CallbackState(daemon) assert callbacks.before_stop_callback_called is False assert callbacks.after_stop_callback_called is False assert callbacks.before_start_callback_called is False assert callbacks.after_start_callback_called is False with daemon.stopped( before_stop_callback=callbacks.before_stop_callback, after_stop_callback=callbacks.after_stop_callback, before_start_callback=callbacks.before_start_callback, after_start_callback=callbacks.after_start_callback, ): assert daemon.is_running() is False assert callbacks.before_stop_callback_called is True assert callbacks.after_stop_callback_called is True assert callbacks.before_start_callback_called is False assert callbacks.after_start_callback_called is False assert daemon.is_running() assert callbacks.before_stop_callback_called is True assert callbacks.after_stop_callback_called is True assert callbacks.before_start_callback_called is True assert callbacks.after_start_callback_called is True # Reset the callbacks state callbacks.before_stop_callback_called = False callbacks.after_stop_callback_called = False callbacks.before_start_callback_called = False callbacks.after_start_callback_called = False # Let's got through stopped again, the 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 callbacks.before_stop_callback_called is False assert callbacks.after_stop_callback_called is False assert callbacks.before_start_callback_called is False assert callbacks.after_start_callback_called is False assert daemon_started_once is True def test_context_manager_returns_class_instance(tempfiles): 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, caplog, max_start_attempts): """ 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-salt-factories-0.907.0/tests/functional/factories/base/test_process_factory.py0000644000175000017500000000643014036250542031754 0ustar vampasvampas00000000000000import sys import pytest from saltfactories.bases import Process from saltfactories.exceptions import FactoryTimeout @pytest.mark.parametrize("exitcode", [0, 1, 3, 9, 40, 120]) def test_exitcode(exitcode, tempfiles): shell = Process(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.exitcode == exitcode def test_timeout_defined_on_class_instantiation(tempfiles): shell = Process(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): shell = Process(script_name=sys.executable) script = tempfiles.makepyfile( """ # coding=utf-8 import time time.sleep(0.5) exit(0) """ ) result = shell.run(script) assert result.exitcode == 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, expected_object, tempfiles): shell = Process(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.exitcode == 0 if result.json: assert result.json == expected_object assert result.stdout == input_str def test_stderr_output(tempfiles): input_str = "Thou shalt not exit cleanly" shell = Process(script_name=sys.executable) script = tempfiles.makepyfile( """ # coding=utf-8 exit("{}") """.format( input_str ) ) result = shell.run(script) assert result.exitcode == 1 assert result.stderr == input_str + "\n" def test_unicode_output(tempfiles): shell = Process(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.exitcode == 0, str(result) assert result.stdout == "STDOUT Fátima" assert result.stderr == "STDERR Fátima" def test_process_failed_to_start(tempfiles): shell = Process(script_name=sys.executable) script = tempfiles.makepyfile( """ # coding=utf-8 1/0 """ ) result = shell.run(script) assert result.exitcode == 1 assert "ZeroDivisionError: division by zero" in result.stderr pytest-salt-factories-0.907.0/tests/functional/factories/base/test_salt_daemon_factory.py0000644000175000017500000000415214036250542032563 0ustar vampasvampas00000000000000import shutil import pytest from saltfactories.bases import SaltDaemon from saltfactories.exceptions import FactoryNotStarted @pytest.fixture def config_dir(pytester): _conf_dir = pytester.mkdir("conf") try: yield _conf_dir finally: shutil.rmtree(str(_conf_dir), ignore_errors=True) @pytest.fixture def master_id(): return "test-master-id" @pytest.fixture def config_file(config_dir, master_id): config_file = str(config_dir / "config") with open(config_file, "w") as wfh: wfh.write("id: {}\n".format(master_id)) return config_file def test_extra_cli_arguments_after_first_failure( config_dir, config_file, tempfiles, master_id, tmp_path ): """ This test asserts that after the first start failure, the extra_cli_arguments_after_first_start_failure arguments are added """ output_file = tmp_path.joinpath("output.txt").resolve() config = {"conf_file": config_file, "id": master_id} script = tempfiles.makepyfile( r""" # coding=utf-8 import sys import multiprocessing def main(): with open(r"{}", "a") as wfh: wfh.write(" ".join(sys.argv)) wfh.write("\n") sys.exit(1) # Support for windows test runs if __name__ == '__main__': multiprocessing.freeze_support() main() """.format( output_file ), ) daemon = SaltDaemon( script_name=script, config=config, start_timeout=0.25, max_start_attempts=2, check_ports=[12345], extra_cli_arguments_after_first_start_failure=["--log-level=debug"], ) with pytest.raises(FactoryNotStarted) as exc: with daemon.started(): pass str_exc = str(exc.value) output_file_contents = output_file.read_text().splitlines() expected = [ "{} --config-dir={} --log-level=critical".format(script, config_dir), "{} --config-dir={} --log-level=debug".format(script, config_dir), ] assert output_file_contents == expected assert "Exitcode: 1" in str_exc pytest-salt-factories-0.907.0/tests/functional/factories/cli/0000755000175000017500000000000014106144336024771 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/factories/cli/__init__.py0000644000175000017500000000000013723372562027100 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/factories/cli/conftest.py0000644000175000017500000000204614036250542027171 0ustar vampasvampas00000000000000""" tests.functional.factories.cli.conftest ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ """ import pytest @pytest.fixture(scope="package") def master_id(): return "functional-cli-master" @pytest.fixture(scope="package") def minion_id(): return "functional-cli-minion" @pytest.fixture(scope="package") def proxy_minion_id(): return "functional-cli-proxy-minion" @pytest.fixture def salt_master(request, salt_factories, master_id): """ This fixture just configures a salt-master. It does not start one. """ return salt_factories.salt_master_daemon(master_id) @pytest.fixture def salt_minion(request, salt_factories, minion_id, salt_master): """ This fixture just configures a salt-minion. It does not start one. """ return salt_master.salt_minion_daemon(minion_id) @pytest.fixture def salt_proxy_minion(request, salt_factories, salt_master, proxy_minion_id): """ This fixture just configures a salt-minion. It does not start one. """ return salt_master.salt_proxy_minion_daemon(proxy_minion_id) pytest-salt-factories-0.907.0/tests/functional/factories/cli/test_call.py0000644000175000017500000000117614036250542027321 0ustar vampasvampas00000000000000""" tests.functional.factories.cli.test_call ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``salt-call`` CLI functionality """ import pathlib def test_version_info(salt_minion, salt_proxy_minion, salt_version): cli = salt_minion.salt_call_cli() ret = cli.run("--version") assert ret.exitcode == 0, ret assert ret.stdout.strip() == "{} {}".format(pathlib.Path(cli.script_name).name, salt_version) cli = salt_proxy_minion.salt_call_cli() ret = cli.run("--version") assert ret.exitcode == 0, ret assert ret.stdout.strip() == "{} {}".format(pathlib.Path(cli.script_name).name, salt_version) pytest-salt-factories-0.907.0/tests/functional/factories/cli/test_cloud.py0000644000175000017500000000064014036250542027507 0ustar vampasvampas00000000000000""" tests.functional.factories.cli.test_cloud ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``salt-cloud`` CLI functionality """ import pathlib def test_version_info(salt_master, salt_version): cli = salt_master.salt_cloud_cli() ret = cli.run("--version") assert ret.exitcode == 0, ret assert ret.stdout.strip() == "{} {}".format(pathlib.Path(cli.script_name).name, salt_version) pytest-salt-factories-0.907.0/tests/functional/factories/cli/test_cp.py0000644000175000017500000000062414036250542027005 0ustar vampasvampas00000000000000""" tests.functional.factories.cli.test_cp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``salt-cp`` CLI functionality """ import pathlib def test_version_info(salt_master, salt_version): cli = salt_master.salt_cp_cli() ret = cli.run("--version") assert ret.exitcode == 0, ret assert ret.stdout.strip() == "{} {}".format(pathlib.Path(cli.script_name).name, salt_version) pytest-salt-factories-0.907.0/tests/functional/factories/cli/test_key.py0000644000175000017500000000063014036250542027170 0ustar vampasvampas00000000000000""" tests.functional.factories.cli.test_key ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``salt-key`` CLI functionality """ import pathlib def test_version_info(salt_master, salt_version): cli = salt_master.salt_key_cli() ret = cli.run("--version") assert ret.exitcode == 0, ret assert ret.stdout.strip() == "{} {}".format(pathlib.Path(cli.script_name).name, salt_version) pytest-salt-factories-0.907.0/tests/functional/factories/cli/test_run.py0000644000175000017500000000063014036250542027204 0ustar vampasvampas00000000000000""" tests.functional.factories.cli.test_run ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``salt-run`` CLI functionality """ import pathlib def test_version_info(salt_master, salt_version): cli = salt_master.salt_run_cli() ret = cli.run("--version") assert ret.exitcode == 0, ret assert ret.stdout.strip() == "{} {}".format(pathlib.Path(cli.script_name).name, salt_version) pytest-salt-factories-0.907.0/tests/functional/factories/cli/test_salt.py0000644000175000017500000000062214062623454027351 0ustar vampasvampas00000000000000""" tests.functional.factories.cli.test_salt ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``salt`` CLI functionality """ import pathlib def test_version_info(salt_master, salt_version): cli = salt_master.salt_cli() ret = cli.run("--version") assert ret.exitcode == 0, ret assert ret.stdout.strip() == "{} {}".format(pathlib.Path(cli.script_name).name, salt_version) pytest-salt-factories-0.907.0/tests/functional/factories/cli/test_spm.py0000644000175000017500000000062314036250542027201 0ustar vampasvampas00000000000000""" tests.functional.factories.cli.test_spm ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``spm`` CLI functionality """ import pathlib def test_version_info(salt_master, salt_version): cli = salt_master.salt_spm_cli() ret = cli.run("--version") assert ret.exitcode == 0, ret assert ret.stdout.strip() == "{} {}".format(pathlib.Path(cli.script_name).name, salt_version) pytest-salt-factories-0.907.0/tests/functional/factories/cli/test_ssh.py0000644000175000017500000000063014036250542027175 0ustar vampasvampas00000000000000""" tests.functional.factories.cli.test_ssh ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``salt-ssh`` CLI functionality """ import pathlib def test_version_info(salt_master, salt_version): cli = salt_master.salt_ssh_cli() ret = cli.run("--version") assert ret.exitcode == 0, ret assert ret.stdout.strip() == "{} {}".format(pathlib.Path(cli.script_name).name, salt_version) pytest-salt-factories-0.907.0/tests/functional/factories/daemons/0000755000175000017500000000000014106144336025650 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/factories/daemons/__init__.py0000644000175000017500000000000013723372562027757 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/factories/daemons/test_master_factory.py0000644000175000017500000000742014036250542032305 0ustar vampasvampas00000000000000import pytest from saltfactories.utils import random_string from saltfactories.utils import running_username def test_keyword_basic_defaults(salt_factories): master_config = salt_factories.salt_master_daemon( random_string("master-"), defaults={"zzzz": True} ).config assert "zzzz" in master_config def test_interface_defaults(salt_factories): interface = "172.17.0.1" master_config = salt_factories.salt_master_daemon( random_string("master-"), defaults={"interface": interface} ).config assert master_config["interface"] != interface assert master_config["interface"] == "127.0.0.1" def test_keyword_basic_overrides(salt_factories): master_config = salt_factories.salt_master_daemon( random_string("master-"), overrides={"zzzz": True} ).config assert "zzzz" in master_config def test_interface_overrides(salt_factories): interface = "172.17.0.1" master_config = salt_factories.salt_master_daemon( random_string("master-"), overrides={"interface": interface} ).config assert master_config["interface"] != "127.0.0.1" assert master_config["interface"] == interface def test_keyword_simple_overrides_override_defaults(salt_factories): master_config = salt_factories.salt_master_daemon( random_string("master-"), defaults={"zzzz": False}, overrides={"zzzz": True} ).config assert "zzzz" in master_config assert master_config["zzzz"] is True def test_keyword_nested_overrides_override_defaults(salt_factories): master_config = salt_factories.salt_master_daemon( random_string("master-"), defaults={ "zzzz": False, "user": "foobar", "colors": {"black": True, "white": False}, }, overrides={"colors": {"white": True, "grey": False}}, ).config assert "zzzz" in master_config assert master_config["zzzz"] is False assert master_config["colors"] == {"black": True, "white": True, "grey": False} def test_provide_root_dir(pytester, salt_factories): root_dir = str(pytester.mkdir("custom-root")) defaults = {"root_dir": root_dir} master_config = salt_factories.salt_master_daemon( random_string("master-"), defaults=defaults ).config assert master_config["root_dir"] == root_dir def configure_kwargs_ids(value): return "configure_kwargs={!r}".format(value) @pytest.mark.parametrize( "configure_kwargs", [{"defaults": {"user": "blah"}}, {"overrides": {"user": "blah"}}, {}], ids=configure_kwargs_ids, ) def test_provide_user(salt_factories, configure_kwargs): master_config = salt_factories.salt_master_daemon( random_string("master-"), **configure_kwargs ).config if not configure_kwargs: # salt-factories injects the current username assert master_config["user"] is not None assert master_config["user"] == running_username() else: # salt-factories does not override the passed user value assert master_config["user"] != running_username() assert master_config["user"] == "blah" @pytest.mark.parametrize( "configure_kwargs", [ {"defaults": None}, {"overrides": None}, {}, {"defaults": None, "overrides": {"user": "blah"}}, {"defaults": {"user": "blah"}, "overrides": None}, {"defaults": {"user": "blah"}, "overrides": {"user": "blah"}}, ], ids=configure_kwargs_ids, ) def test_pytest_config(salt_factories, configure_kwargs): master_id = random_string("master-") config = salt_factories.salt_master_daemon(master_id, **configure_kwargs).config config_key = "pytest-master" assert config_key in config assert "log" in config[config_key] for key in ("host", "level", "port", "prefix"): assert key in config[config_key]["log"] pytest-salt-factories-0.907.0/tests/functional/factories/daemons/test_minion_factory.py0000644000175000017500000001100614062616276032307 0ustar vampasvampas00000000000000import pytest from saltfactories.utils import random_string from saltfactories.utils import running_username def test_keyword_basic_defaults(salt_factories): minion_config = salt_factories.salt_minion_daemon( random_string("minion-"), defaults={"zzzz": True} ).config assert "zzzz" in minion_config def test_interface_defaults(salt_factories): interface = "172.17.0.1" minion_config = salt_factories.salt_minion_daemon( random_string("minion-"), defaults={"interface": interface} ).config assert minion_config["interface"] != interface assert minion_config["interface"] == "127.0.0.1" def test_master_defaults(salt_factories): master = "172.17.0.1" minion_config = salt_factories.salt_minion_daemon( random_string("minion-"), defaults={"master": master} ).config assert minion_config["master"] != master assert minion_config["master"] == "127.0.0.1" def test_keyword_basic_overrides(salt_factories): minion_config = salt_factories.salt_minion_daemon( random_string("minion-"), overrides={"zzzz": True} ).config assert "zzzz" in minion_config def test_interface_overrides(salt_factories): interface = "172.17.0.1" minion_config = salt_factories.salt_minion_daemon( random_string("minion-"), overrides={"interface": interface} ).config assert minion_config["interface"] == interface assert minion_config["interface"] != "127.0.0.1" def test_master_overrides(salt_factories): master = "172.17.0.1" minion_config = salt_factories.salt_minion_daemon( random_string("minion-"), overrides={"master": master} ).config assert minion_config["master"] == master assert minion_config["master"] != "127.0.0.1" def test_keyword_simple_overrides_override_defaults(salt_factories): minion_config = salt_factories.salt_minion_daemon( random_string("minion-"), defaults={"zzzz": False}, overrides={"zzzz": True} ).config assert "zzzz" in minion_config assert minion_config["zzzz"] is True def test_keyword_nested_overrides_override_defaults(salt_factories): minion_config = salt_factories.salt_minion_daemon( random_string("minion-"), defaults={ "zzzz": False, "user": "foobar", "colors": {"black": True, "white": False}, }, overrides={"colors": {"white": True, "grey": False}}, ).config assert "zzzz" in minion_config assert minion_config["zzzz"] is False assert minion_config["colors"] == {"black": True, "white": True, "grey": False} def test_provide_root_dir(pytester, salt_factories): root_dir = str(pytester.mkdir("custom-root")) defaults = {"root_dir": root_dir} minion_config = salt_factories.salt_minion_daemon( random_string("minion-"), defaults=defaults ).config assert minion_config["root_dir"] == root_dir def configure_kwargs_ids(value): return "configure_kwargs={!r}".format(value) @pytest.mark.parametrize( "configure_kwargs", [{"defaults": {"user": "blah"}}, {"overrides": {"user": "blah"}}, {}], ids=configure_kwargs_ids, ) def test_provide_user(salt_factories, configure_kwargs): minion_config = salt_factories.salt_minion_daemon( random_string("minion-"), **configure_kwargs ).config if not configure_kwargs: # salt-factories injects the current username assert minion_config["user"] is not None assert minion_config["user"] == running_username() else: # salt-factories does not override the passed user value assert minion_config["user"] != running_username() assert minion_config["user"] == "blah" @pytest.mark.parametrize( "configure_kwargs", [ {"defaults": None}, {"overrides": None}, {}, {"defaults": None, "overrides": {"user": "blah"}}, {"defaults": {"user": "blah"}, "overrides": None}, {"defaults": {"user": "blah"}, "overrides": {"user": "blah"}}, ], ids=configure_kwargs_ids, ) def test_pytest_config(salt_factories, configure_kwargs): master_id = random_string("master-") master = salt_factories.salt_master_daemon(master_id) config = master.salt_minion_daemon(random_string("the-id-"), **configure_kwargs).config config_key = "pytest-minion" assert config_key in config assert "log" in config[config_key] for key in ("host", "level", "port", "prefix"): assert key in config[config_key]["log"] assert "master-id" in config[config_key] assert config[config_key]["master-id"] == master_id pytest-salt-factories-0.907.0/tests/functional/factories/daemons/test_proxy_minion_factory.py0000644000175000017500000000700714036250542033545 0ustar vampasvampas00000000000000import pytest from saltfactories.utils import random_string from saltfactories.utils import running_username def test_keyword_basic_defaults(salt_factories): proxy_minion_config = salt_factories.salt_proxy_minion_daemon( random_string("proxy-minion-"), defaults={"zzzz": True} ).config assert "zzzz" in proxy_minion_config def test_keyword_basic_overrides(salt_factories): proxy_minion_config = salt_factories.salt_proxy_minion_daemon( random_string("proxy-minion-"), overrides={"zzzz": True} ).config assert "zzzz" in proxy_minion_config def test_keyword_simple_overrides_override_defaults(salt_factories): proxy_minion_config = salt_factories.salt_proxy_minion_daemon( random_string("proxy-minion-"), defaults={"zzzz": False}, overrides={"zzzz": True}, ).config assert "zzzz" in proxy_minion_config assert proxy_minion_config["zzzz"] is True def test_keyword_nested_overrides_override_defaults(salt_factories): proxy_minion_config = salt_factories.salt_proxy_minion_daemon( random_string("proxy-minion-"), defaults={ "zzzz": False, "user": "foobar", "colors": {"black": True, "white": False}, }, overrides={"colors": {"white": True, "grey": False}}, ).config assert "zzzz" in proxy_minion_config assert proxy_minion_config["zzzz"] is False assert proxy_minion_config["colors"] == {"black": True, "white": True, "grey": False} def test_provide_root_dir(pytester, salt_factories): root_dir = str(pytester.mkdir("custom-root")) defaults = {"root_dir": root_dir} proxy_minion_config = salt_factories.salt_proxy_minion_daemon( random_string("proxy_minion-"), defaults=defaults ).config assert proxy_minion_config["root_dir"] == root_dir def configure_kwargs_ids(value): return "configure_kwargs={!r}".format(value) @pytest.mark.parametrize( "configure_kwargs", [{"defaults": {"user": "blah"}}, {"overrides": {"user": "blah"}}, {}], ids=configure_kwargs_ids, ) def test_provide_user(salt_factories, configure_kwargs): proxy_minion_config = salt_factories.salt_proxy_minion_daemon( random_string("proxy-minion-"), **configure_kwargs ).config if not configure_kwargs: # salt-factories injects the current username assert proxy_minion_config["user"] is not None assert proxy_minion_config["user"] == running_username() else: # salt-factories does not override the passed user value assert proxy_minion_config["user"] != running_username() assert proxy_minion_config["user"] == "blah" @pytest.mark.parametrize( "configure_kwargs", [ {"defaults": None}, {"overrides": None}, {}, {"defaults": None, "overrides": {"user": "blah"}}, {"defaults": {"user": "blah"}, "overrides": None}, {"defaults": {"user": "blah"}, "overrides": {"user": "blah"}}, ], ids=configure_kwargs_ids, ) def test_pytest_config(salt_factories, configure_kwargs): master_id = random_string("master-") master = salt_factories.salt_master_daemon(master_id) config = master.salt_proxy_minion_daemon(random_string("the-id-"), **configure_kwargs).config config_key = "pytest-minion" assert config_key in config assert "log" in config[config_key] for key in ("host", "level", "port", "prefix"): assert key in config[config_key]["log"] assert "master-id" in config[config_key] assert config[config_key]["master-id"] == master_id pytest-salt-factories-0.907.0/tests/functional/factories/daemons/test_syndic_factory.py0000644000175000017500000001212714036250542032303 0ustar vampasvampas00000000000000import pytest from saltfactories.utils import random_string from saltfactories.utils import running_username @pytest.fixture def mom(salt_factories): return salt_factories.salt_master_daemon(random_string("mom-")) def test_keyword_basic_defaults(mom): syndic_id = random_string("syndic-") defaults = {"zzzz": True} master_defaults = defaults.copy() minion_defaults = defaults.copy() syndic = mom.salt_syndic_daemon( syndic_id, defaults=defaults, master_defaults=master_defaults, minion_defaults=minion_defaults, ) assert "zzzz" in syndic.config assert syndic.config["zzzz"] is True assert "zzzz" in syndic.master.config assert syndic.master.config["zzzz"] is True assert "zzzz" in syndic.minion.config assert syndic.minion.config["zzzz"] is True def test_keyword_basic_overrides(mom): syndic_id = random_string("syndic-") overrides = {"zzzz": True} master_overrides = overrides.copy() minion_overrides = overrides.copy() syndic = mom.salt_syndic_daemon( syndic_id, overrides=overrides, master_overrides=master_overrides, minion_overrides=minion_overrides, ) assert "zzzz" in syndic.config assert syndic.config["zzzz"] is True assert "zzzz" in syndic.master.config assert syndic.master.config["zzzz"] is True assert "zzzz" in syndic.minion.config assert syndic.minion.config["zzzz"] is True def test_keyword_simple_overrides_override_defaults(mom): syndic_id = random_string("syndic-") defaults = {"zzzz": False} master_defaults = defaults.copy() minion_defaults = defaults.copy() overrides = {"zzzz": True} master_overrides = overrides.copy() minion_overrides = overrides.copy() syndic = mom.salt_syndic_daemon( syndic_id, defaults=defaults, master_defaults=master_defaults, minion_defaults=minion_defaults, overrides=overrides, master_overrides=master_overrides, minion_overrides=minion_overrides, ) assert "zzzz" in syndic.config assert syndic.config["zzzz"] is True assert "zzzz" in syndic.master.config assert syndic.master.config["zzzz"] is True assert "zzzz" in syndic.minion.config assert syndic.minion.config["zzzz"] is True def test_keyword_nested_overrides_override_defaults(mom): defaults = {"zzzz": False, "user": "foobar", "colors": {"black": True, "white": False}} overrides = {"zzzz": True, "colors": {"white": True, "grey": False}} expected_colors = {"black": True, "white": True, "grey": False} syndic_id = random_string("syndic-") syndic = mom.salt_syndic_daemon( syndic_id, defaults=defaults, master_defaults=defaults.copy(), minion_defaults=defaults.copy(), overrides=overrides, master_overrides=overrides.copy(), minion_overrides=overrides.copy(), ) assert "zzzz" in syndic.config assert syndic.config["zzzz"] is True assert syndic.config["colors"] == expected_colors assert "zzzz" in syndic.master.config assert syndic.master.config["zzzz"] is True assert syndic.master.config["colors"] == expected_colors assert "zzzz" in syndic.minion.config assert syndic.minion.config["zzzz"] is True assert syndic.minion.config["colors"] == expected_colors def test_provide_root_dir(pytester, mom): root_dir = str(pytester.mkdir("custom-root")) defaults = {"root_dir": root_dir} syndic_id = random_string("syndic-") syndic = mom.salt_syndic_daemon(syndic_id, defaults=defaults) assert syndic.config["root_dir"] == root_dir def configure_kwargs_ids(value): return "configure_kwargs={!r}".format(value) @pytest.mark.parametrize( "configure_kwargs", [ { "defaults": {"user": "blah"}, "master_defaults": {"user": "blah"}, "minion_defaults": {"user": "blah"}, }, { "overrides": {"user": "blah"}, "master_overrides": {"user": "blah"}, "minion_overrides": {"user": "blah"}, }, {}, ], ids=configure_kwargs_ids, ) def test_provide_user(salt_factories, mom, configure_kwargs): syndic_id = random_string("syndic-") syndic = mom.salt_syndic_daemon(syndic_id, **configure_kwargs) if not configure_kwargs: # salt-factories injects the current username assert syndic.master.config["user"] is not None assert syndic.master.config["user"] == running_username() assert syndic.minion.config["user"] is not None assert syndic.minion.config["user"] == running_username() assert syndic.config["user"] is not None assert syndic.config["user"] == running_username() else: assert syndic.master.config["user"] != running_username() assert syndic.master.config["user"] == "blah" assert syndic.minion.config["user"] != running_username() assert syndic.minion.config["user"] == "blah" # salt-factories does not override the passed user value assert syndic.config["user"] != running_username() assert syndic.config["user"] == "blah" pytest-salt-factories-0.907.0/tests/functional/loader/0000755000175000017500000000000014106144336023511 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/loader/__init__.py0000644000175000017500000000000013775371715025626 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/loader/test_fixture_deps.py0000644000175000017500000000307013775535006027634 0ustar vampasvampas00000000000000import logging from unittest.mock import patch import pytest import saltfactories try: saltfactories.__salt__ # pylint: disable=pointless-statement HAS_SALT_DUNDER = True except AttributeError: HAS_SALT_DUNDER = False log = logging.getLogger(__name__) @pytest.fixture(autouse=True) def confirm_saltfactories_does_not_have_salt_dunders(): assert ( HAS_SALT_DUNDER is False ), "Weirdly, the saltfactories module has a __salt__ dunder defined. That's a bug!" def confirm_saltfactories_does_not_have_salt_dunders_after_setup_loader_mock_terminates( setup_loader_mock, ): yield with pytest.raises(AttributeError): assert isinstance(saltfactories.__salt__, dict) @pytest.fixture def pre_loader_modules_patched_fixture(): with pytest.raises(AttributeError): assert isinstance(saltfactories.__salt__, dict) yield False @pytest.fixture def configure_loader_modules(pre_loader_modules_patched_fixture): return { saltfactories: { "__salt__": {"test.echo": lambda x: x, "foo": pre_loader_modules_patched_fixture} } } @pytest.fixture def fixture_that_needs_loader_modules_patched(): assert saltfactories.__salt__["foo"] is False with patch.dict(saltfactories.__salt__, {"foo": True}): assert saltfactories.__salt__["foo"] is True yield assert saltfactories.__salt__["foo"] is False def test_fixture_deps(fixture_that_needs_loader_modules_patched): assert saltfactories.__salt__["foo"] is True assert saltfactories.__salt__["test.echo"]("foo") == "foo" pytest-salt-factories-0.907.0/tests/functional/loader/test_loader.py0000644000175000017500000001234414027303716026375 0ustar vampasvampas00000000000000import pytest import saltfactories try: saltfactories.__salt__ # pylint: disable=pointless-statement HAS_SALT_DUNDER = True except AttributeError: HAS_SALT_DUNDER = False @pytest.fixture def configure_loader_modules(): return {saltfactories: {"__salt__": {"test.echo": lambda x: x}}} def test_loader_mocking(): assert ( HAS_SALT_DUNDER is False ), "Weirdly, the saltfactories module has a __salt__ dunder defined. That's a bug!" # The saltfactories.__init__ module DOES NOT have a __salt__ dunder defined # So, if the assert bellow works, it means that the loader mocking works. assert "test.echo" in saltfactories.__salt__ assert saltfactories.__salt__["test.echo"]("foo") == "foo" def test_loader_mocking_through_runpytest(pytester): pytester.makepyfile( """ import pytest import saltfactories @pytest.fixture def configure_loader_modules(): return {saltfactories: {"__salt__": {"test.echo": lambda x: x}}} def test_one(): assert saltfactories.__salt__["test.echo"]("foo") == "foo" """ ) res = pytester.runpytest() res.assert_outcomes(passed=1) def test_runtime_error_raised_for_non_module_type_keys(pytester): pytester.makepyfile( """ import pytest @pytest.fixture def configure_loader_modules(): return {"foobar": {}} def test_one(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(errors=1) res.stdout.fnmatch_lines( [ "*UsageError: The dictionary keys returned by setup_loader_modules() must be an imported module*" ] ) def test_runtime_error_raised_for_non_dict_values(pytester): pytester.makepyfile( """ import pytest import string @pytest.fixture def configure_loader_modules(): return {string: "yes"} def test_one(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(errors=1) res.stdout.fnmatch_lines( [ "*UsageError: The dictionary values returned by setup_loader_modules() must be a dictionary*" ] ) def test_runtime_error_raised_when_sys_modules_is_not_list(pytester): pytester.makepyfile( """ import pytest import string @pytest.fixture def configure_loader_modules(): return {string: {"sys.modules": False}, "sys.modules": True} def test_one(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(errors=1) res.stdout.fnmatch_lines(["*UsageError: 'sys.modules' must be a dictionary*"]) @pytest.mark.parametrize("dunder", ["__virtual__", "__init__"]) def test_runtime_error_raised_when_not_needed_dunders_are_passed(pytester, dunder): pytester.makepyfile( """ import pytest import string @pytest.fixture def configure_loader_modules(): return {{string: {{"{}": False}}}} def test_one(): assert True """.format( dunder ) ) res = pytester.runpytest() res.assert_outcomes(errors=1) res.stdout.fnmatch_lines(["*UsageError: No need to patch '{}'*".format(dunder)]) def test_runtime_error_raised_on_unknown_salt_dunders(pytester): pytester.makepyfile( """ import pytest import string @pytest.fixture def configure_loader_modules(): return {string: {"__foobar__": False}} def test_one(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(errors=1) res.stdout.fnmatch_lines(["*UsageError: Don't know how to handle '__foobar__'*"]) def test_configure_loader_modules_not_a_fixture(pytester): pytester.makepyfile( """ import pytest import saltfactories def configure_loader_modules(): return {saltfactories: {"__salt__": {"test.echo": lambda x: x}}} def test_one(): assert True """ ) res = pytester.runpytest() res.stdout.fnmatch_lines( [ "*RuntimeError:*defines a 'configure_loader_modules' function but that function is not a fixture*" ] ) def test_runtime_error_raised_for_bad_fixture_name(pytester): pytester.makepyfile( """ import pytest @pytest.fixture def configure_loader_module(): return {"foobar": {}} def test_one(): assert True """ ) res = pytester.runpytest() res.stdout.fnmatch_lines( [ "*RuntimeError:*defines a 'configure_loader_module' fixture but the " "correct fixture name is 'configure_loader_modules'*" ] ) def test_bad_fixture_name_as_plain_function_ok(pytester): pytester.makepyfile( """ import pytest import string def configure_loader_module(): return {"foobar": {}} @pytest.fixture def configure_loader_modules(): return {string: {}} def test_one(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(passed=1) pytest-salt-factories-0.907.0/tests/functional/markers/0000755000175000017500000000000014106144336023707 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/markers/__init__.py0000644000175000017500000000000013720712732026010 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/markers/test_destructive_test.py0000644000175000017500000000272014033044065030716 0ustar vampasvampas00000000000000""" tests.functional.markers.test_destructive_test ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.destructive_test`` marker """ def test_run_destructive_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.destructive_test def test_one(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_run_destructive_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.destructive_test def test_one(): assert True """ ) res = pytester.runpytest("--run-destructive") res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_error_on_args_or_kwargs(pytester): pytester.makepyfile( """ import pytest @pytest.mark.destructive_test("arg") def test_one(): assert True @pytest.mark.destructive_test(kwarg="arg") def test_two(): assert True """ ) res = pytester.runpytest("--run-destructive") res.assert_outcomes(errors=2) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") res.stdout.fnmatch_lines( [ "*UsageError: The 'destructive_test' marker does not accept any arguments or keyword arguments*" ] ) pytest-salt-factories-0.907.0/tests/functional/markers/test_expensive_test.py0000644000175000017500000000267214033044065030371 0ustar vampasvampas00000000000000""" tests.functional.markers.test_expensive_test ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.expensive_test`` marker """ def test_run_expensive_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.expensive_test def test_one(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_run_expensive_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.expensive_test def test_one(): assert True """ ) res = pytester.runpytest("--run-expensive") res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_error_on_args_or_kwargs(pytester): pytester.makepyfile( """ import pytest @pytest.mark.expensive_test("arg") def test_one(): assert True @pytest.mark.expensive_test(kwarg="arg") def test_two(): assert True """ ) res = pytester.runpytest("--run-destructive") res.assert_outcomes(errors=2) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") res.stdout.fnmatch_lines( [ "*UsageError: The 'expensive_test' marker does not accept any arguments or keyword arguments*" ] ) pytest-salt-factories-0.907.0/tests/functional/markers/test_requires_network.py0000644000175000017500000000251114027303716030730 0ustar vampasvampas00000000000000""" tests.functional.markers.test_requires_network ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.requires_network`` marker """ from unittest import mock from saltfactories.utils import ports from saltfactories.utils import socket def test_has_local_network(pytester): pytester.makepyfile( """ import pytest @pytest.mark.requires_network def test_one(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_no_local_network(pytester): pytester.makepyfile( """ import pytest @pytest.mark.requires_network def test_one(): assert True """ ) mock_socket = mock.MagicMock() mock_socket.bind = mock.MagicMock(side_effect=socket.error) with mock.patch( "saltfactories.utils.ports.get_unused_localhost_port", side_effect=[ports.get_unused_localhost_port() for n in range(10)], ): with mock.patch("saltfactories.utils.markers.socket.socket", return_value=mock_socket): res = pytester.runpytest_inprocess("-p", "no:salt-factories-log-server") res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") pytest-salt-factories-0.907.0/tests/functional/markers/test_requires_salt_modules.py0000644000175000017500000001061314036250542031732 0ustar vampasvampas00000000000000""" tests.functional.markers.test_requires_salt_modules ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.requires_salt_modules`` marker """ import pytest @pytest.mark.parametrize( "modules", [ ("cmd",), ("cmd", "test"), ], ) def test_has_required_salt_module(pytester, modules): pytester.makepyfile( """ import pytest @pytest.mark.requires_salt_modules({}) def test_one(): assert True """.format( ", ".join(repr(module) for module in modules) ) ) res = pytester.runpytest() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") @pytest.mark.parametrize( "modules", [ ("cmdmod",), ("cmd", "tests"), ], ) def test_missing_required_salt_module(pytester, modules): pytester.makepyfile( """ import pytest @pytest.mark.requires_salt_modules({}) def test_one(): assert True """.format( ", ".join(repr(module) for module in modules) ) ) res = pytester.runpytest() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_has_required_custom_salt_module(pytester): pytester.makepyfile( r""" import pathlib import textwrap import pytest import logging import saltfactories.utils.functional log = logging.getLogger(__name__) custom_module_name = "bogus" @pytest.fixture(scope="session") def session_markers_loader(salt_factories): minion_id = "session-markers-loader-modules" root_dir = salt_factories.get_root_dir_for_daemon(minion_id) rootfs = root_dir / "rootfs" / "states" rootfs.mkdir(parents=True) rootfs_modules = rootfs / "_modules" rootfs_modules.mkdir(parents=True) module_contents = textwrap.dedent('''\ def echo(text): return text ''') module_path = rootfs_modules / "{}.py".format(custom_module_name) module_path.write_text(module_contents) defaults = { "root_dir": str(root_dir), } overrides = { "file_client": "local", "features": {"enable_slsvars_fixes": True}, "file_roots": { "base": [str(rootfs)] } } factory = salt_factories.salt_minion_daemon( minion_id, defaults=defaults, overrides=overrides, ) loader_instance = saltfactories.utils.functional.Loaders(factory.config.copy()) assert loader_instance.modules.bogus.echo("foo") == "foo" return loader_instance @pytest.mark.requires_salt_modules(custom_module_name) def test_custom_module(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_marker_does_not_accept_keyword_argument(pytester): pytester.makepyfile( """ import pytest @pytest.mark.requires_salt_modules("cmd", foo=True) def test_one(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(errors=1) res.stdout.fnmatch_lines( ["*UsageError: The 'required_salt_modules' marker does not accept keyword arguments*"] ) def test_marker_only_accepts_string_arguments(pytester): pytester.makepyfile( """ import pytest @pytest.mark.requires_salt_modules(("cmd", "test")) def test_one(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(errors=1) res.stdout.fnmatch_lines( ["*UsageError: The 'required_salt_modules' marker only accepts strings as arguments*"] ) def test_marker_errors_with_no_arguments(pytester): pytester.makepyfile( """ import pytest @pytest.mark.requires_salt_modules def test_one(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(errors=1) res.stdout.fnmatch_lines( [ "*UsageError: The 'required_salt_modules' marker needs at least one module name to be passed*" ] ) pytest-salt-factories-0.907.0/tests/functional/markers/test_requires_salt_states.py0000644000175000017500000001102414105130370031553 0ustar vampasvampas00000000000000""" tests.functional.markers.test_requires_salt_states ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.requires_salt_states`` marker """ import pytest @pytest.mark.parametrize( "modules", [ ("cmd",), ("cmd", "test"), ], ) def test_has_required_salt_state(pytester, modules): pytester.makepyfile( """ import pytest @pytest.mark.requires_salt_states({}) def test_one(): assert True """.format( ", ".join(repr(module) for module in modules) ) ) res = pytester.runpytest() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") @pytest.mark.parametrize( "modules", [ ("cmdmod",), ("cmd", "tests"), ], ) def test_missing_required_salt_state(pytester, modules): pytester.makepyfile( """ import pytest @pytest.mark.requires_salt_states({}) def test_one(): assert True """.format( ", ".join(repr(module) for module in modules) ) ) res = pytester.runpytest() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_has_required_custom_salt_state(pytester): pytester.makepyfile( r""" import pathlib import textwrap import pytest import logging import saltfactories.utils.functional log = logging.getLogger(__name__) custom_module_name = "bogus" @pytest.fixture(scope="session") def session_markers_loader(salt_factories): minion_id = "session-markers-loader-states" root_dir = salt_factories.get_root_dir_for_daemon(minion_id) rootfs = root_dir / "rootfs" / "states" rootfs.mkdir(parents=True) rootfs_states = rootfs / "_states" rootfs_states.mkdir(parents=True) module_contents = textwrap.dedent('''\ def echoed(name): return {"result": True, "changes": {}, "comment": name, "name": name} ''') module_path = rootfs_states / "{}.py".format(custom_module_name) module_path.write_text(module_contents) defaults = { "root_dir": str(root_dir), } overrides = { "file_client": "local", "features": {"enable_slsvars_fixes": True}, "file_roots": { "base": [str(rootfs)] } } factory = salt_factories.salt_minion_daemon( minion_id, defaults=defaults, overrides=overrides, ) loader_instance = saltfactories.utils.functional.Loaders(factory.config.copy()) ret = loader_instance.states.bogus.echoed("foo") assert ret.filtered == {"result": True, "changes": {}, "comment": "foo", "name": "foo"} return loader_instance @pytest.mark.requires_salt_states(custom_module_name) def test_custom_module(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_marker_does_not_accept_keyword_argument(pytester): pytester.makepyfile( """ import pytest @pytest.mark.requires_salt_states("cmd", foo=True) def test_one(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(errors=1) res.stdout.fnmatch_lines( ["*UsageError: The 'required_salt_states' marker does not accept keyword arguments*"] ) def test_marker_only_accepts_string_arguments(pytester): pytester.makepyfile( """ import pytest @pytest.mark.requires_salt_states(("cmd", "test")) def test_one(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(errors=1) res.stdout.fnmatch_lines( ["*UsageError: The 'required_salt_states' marker only accepts strings as arguments*"] ) def test_marker_errors_with_no_arguments(pytester): pytester.makepyfile( """ import pytest @pytest.mark.requires_salt_states def test_one(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(errors=1) res.stdout.fnmatch_lines( [ "*UsageError: The 'required_salt_states' marker needs at least one state module name to be passed*" ] ) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_if_binaries_missing.py0000644000175000017500000000320214033044065032343 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_if_binaries_missing ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_if_binaries_missing`` marker """ def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_if_binaries_missing("python9") def test_one(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skipped_multiple_binaries(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_if_binaries_missing("python", "python9", check_all=True) def test_one(): assert True """ ) res = pytester.runpytest() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_if_binaries_missing("python") def test_one(): assert True """ ) res = pytester.runpytest_inprocess("-ra", "-vv") res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped_multiple_binaries(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_if_binaries_missing("python", "pip") def test_one(): assert True """ ) res = pytester.runpytest_inprocess("-ra", "-vv") res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_if_not_root.py0000644000175000017500000000372414033044065030672 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_if_not_root ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_if_not_root`` marker """ import sys from unittest import mock def test_skip_if_not_root_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_if_not_root def test_one(): assert True """ ) if not sys.platform.startswith("win"): mocked_func = mock.patch("os.getuid", return_value=1000) else: mocked_func = mock.patch("salt.utils.win_functions.is_admin", return_value=False) with mocked_func: res = pytester.runpytest() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_if_not_root_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_if_not_root def test_one(): assert True """ ) if not sys.platform.startswith("win"): mocked_func = mock.patch("os.getuid", return_value=0) else: mocked_func = mock.patch("salt.utils.win_functions.is_admin", return_value=True) with mocked_func: res = pytester.runpytest_inprocess("-ra", "-vv") res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_error_on_args_or_kwargs(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_if_not_root("arg") def test_one(): assert True @pytest.mark.skip_if_not_root(kwarg="arg") def test_two(): assert True """ ) res = pytester.runpytest("--run-destructive") res.assert_outcomes(errors=2) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") res.stdout.fnmatch_lines( [ "*UsageError: The 'skip_if_not_root' marker does not accept any arguments or keyword arguments*" ] ) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_on_aarch64.py0000644000175000017500000000274314033044065030275 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_on_aarch64 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_on_aarch64`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_aarch64 def test_one(): assert True """ ) with mock.patch("saltfactories.utils.platform.is_aarch64", return_value=True): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_aarch64 def test_one(): assert True """ ) with mock.patch("saltfactories.utils.platform.is_aarch64", return_value=False): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_aarch64(reason='Because!') def test_one(): assert True """ ) with mock.patch("saltfactories.utils.platform.is_aarch64", return_value=True): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_on_aix.py0000644000175000017500000000304314027303716027624 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_on_aix ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_on_aix`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_aix def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_aix", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_aix def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_aix", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_aix(reason='Because!') def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_aix", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_on_darwin.py0000644000175000017500000000307614027303716030335 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_on_darwin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_on_darwin`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_darwin def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_darwin", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_darwin def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_darwin", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_darwin(reason='Because!') def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_darwin", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_on_freebsd.py0000644000175000017500000000310714027303716030456 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_on_freebsd ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_on_freebsd`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_freebsd def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_freebsd", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_freebsd def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_freebsd", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_freebsd(reason='Because!') def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_freebsd", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_on_linux.py0000644000175000017500000000306514027303716030206 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_on_linux ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_on_linux`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_linux def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_linux", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_linux def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_linux", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_linux(reason='Because!') def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_linux", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_on_netbsd.py0000644000175000017500000000307614027303716030330 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_on_netbsd ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_on_netbsd`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_netbsd def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_netbsd", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_netbsd def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_netbsd", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_netbsd(reason='Because!') def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_netbsd", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_on_openbsd.py0000644000175000017500000000310714027303716030476 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_on_openbsd ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_on_openbsd`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_openbsd def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_openbsd", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_openbsd def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_openbsd", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_openbsd(reason='Because!') def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_openbsd", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_on_platforms.py0000644000175000017500000000643214027303716031057 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_on_platforms ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_on_platforms`` marker """ from unittest import mock import pytest @pytest.mark.parametrize( "platform", ["windows", "linux", "darwin", "sunos", "smartos", "freebsd", "netbsd", "openbsd", "aix"], ) def test_skipped(pytester, platform): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_platforms({}=True) def test_one(): assert True """.format( platform ) ) return_value = True with mock.patch( "saltfactories.utils.platform.is_{}".format(platform), return_value=return_value ): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") @pytest.mark.parametrize( "platform", ["windows", "linux", "darwin", "sunos", "smartos", "freebsd", "netbsd", "openbsd", "aix"], ) def test_not_skipped(pytester, platform): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_platforms({}=True) def test_one(): assert True """.format( platform ) ) return_value = False with mock.patch( "saltfactories.utils.platform.is_{}".format(platform), return_value=return_value ): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_platforms(windows=True, reason='Because!') def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_windows", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) def test_no_platforms(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_platforms def test_one(): assert True """ ) res = pytester.runpytest_inprocess() res.stdout.fnmatch_lines( ["*UsageError: Pass at least one platform to skip_on_platforms as a keyword argument"] ) def test_all_platforms_false(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_platforms(windows=False, linux=False) def test_one(): assert True """ ) res = pytester.runpytest_inprocess() res.stdout.fnmatch_lines( [ "*UsageError: Pass at least one platform with a True value to skip_on_platforms as a keyword argument" ] ) def test_unknown_platform(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_platforms(car=True) def test_one(): assert True """ ) res = pytester.runpytest_inprocess() res.stdout.fnmatch_lines( [ "*UsageError: Passed an invalid platform to skip_on_platforms: on_platforms() got an unexpected keyword argument 'car'" ] ) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_on_smartos.py0000644000175000017500000000310714027303716030534 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_on_smartos ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_on_smartos`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_smartos def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_smartos", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_smartos def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_smartos", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_smartos(reason='Because!') def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_smartos", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_on_sunos.py0000644000175000017500000000306514027303716030216 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_on_sunos ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_on_sunos`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_sunos def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_sunos", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_sunos def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_sunos", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_sunos(reason='Because!') def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_sunos", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_on_windows.py0000644000175000017500000000310714027303716030536 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_on_windows ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_on_windows`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_windows def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_windows", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_windows def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_windows", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_on_windows(reason='Because!') def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_windows", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_unless_on_aarch64.py0000644000175000017500000000301614033044065031660 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_unless_on_aarch64 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_unless_on_aarch64`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_aarch64 def test_one(): assert True """ ) with mock.patch("saltfactories.utils.platform.is_aarch64", return_value=False): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_aarch64 def test_one(): assert True """ ) with mock.patch("saltfactories.utils.platform.is_aarch64", return_value=True): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_aarch64(reason='Because!') def test_one(): assert True """ ) with mock.patch("saltfactories.utils.platform.is_aarch64", return_value=False): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_unless_on_aix.py0000644000175000017500000000311614027303716031216 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_unless_on_aix ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_unless_on_aix`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_aix def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_aix", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_aix def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_aix", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_aix(reason='Because!') def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_aix", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_unless_on_darwin.py0000644000175000017500000000315114027303716031720 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_unless_on_darwin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_unless_on_darwin`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_darwin def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_darwin", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_darwin def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_darwin", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_darwin(reason='Because!') def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_darwin", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_unless_on_freebsd.py0000644000175000017500000000316214027303716032050 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_unless_on_freebsd ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_unless_on_freebsd`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_freebsd def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_freebsd", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_freebsd def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_freebsd", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_freebsd(reason='Because!') def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_freebsd", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_unless_on_linux.py0000644000175000017500000000314014027303716031571 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_unless_on_linux ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_unless_on_linux`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_linux def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_linux", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_linux def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_linux", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_linux(reason='Because!') def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_linux", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_unless_on_netbsd.py0000644000175000017500000000315114027303716031713 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_unless_on_netbsd ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_unless_on_netbsd`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_netbsd def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_netbsd", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_netbsd def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_netbsd", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_netbsd(reason='Because!') def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_netbsd", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_unless_on_openbsd.py0000644000175000017500000000316214027303716032070 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_unless_on_openbsd ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_unless_on_openbsd`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_openbsd def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_openbsd", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_openbsd def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_openbsd", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_openbsd(reason='Because!') def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_openbsd", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_unless_on_platforms.py0000644000175000017500000000660714027303716032454 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_unless_on_platforms ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_unless_on_platforms`` marker """ from unittest import mock import pytest @pytest.mark.parametrize( "platform", ["windows", "linux", "darwin", "sunos", "smartos", "freebsd", "netbsd", "openbsd", "aix"], ) def test_skipped(pytester, platform): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_platforms({}=True) def test_one(): assert True """.format( platform ) ) return_value = False with mock.patch( "saltfactories.utils.platform.is_{}".format(platform), return_value=return_value ): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") @pytest.mark.parametrize( "platform", ["windows", "linux", "darwin", "sunos", "smartos", "freebsd", "netbsd", "openbsd", "aix"], ) def test_not_skipped(pytester, platform): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_platforms({}=True) def test_one(): assert True """.format( platform ) ) return_value = True with mock.patch( "saltfactories.utils.platform.is_{}".format(platform), return_value=return_value ): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_platforms(windows=True, reason='Because!') def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_windows", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) def test_no_platforms(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_platforms def test_one(): assert True """ ) res = pytester.runpytest_inprocess() res.stdout.fnmatch_lines( [ "*UsageError: Pass at least one platform to skip_unless_on_platforms as a keyword argument" ] ) def test_all_platforms_false(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_platforms(windows=False, linux=False) def test_one(): assert True """ ) res = pytester.runpytest_inprocess() res.stdout.fnmatch_lines( [ "*UsageError: Pass at least one platform with a True value to skip_unless_on_platforms as a keyword argument" ] ) def test_unknown_platform(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_platforms(car=True) def test_one(): assert True """ ) res = pytester.runpytest_inprocess() res.stdout.fnmatch_lines( [ "*UsageError: Passed an invalid platform to skip_unless_on_platforms: on_platforms() got an unexpected keyword argument 'car'" ] ) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_unless_on_smartos.py0000644000175000017500000000316214027303716032126 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_unless_on_smartos ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_unless_on_smartos`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_smartos def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_smartos", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_smartos def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_smartos", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_smartos(reason='Because!') def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_smartos", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_unless_on_sunos.py0000644000175000017500000000314014027303716031601 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_unless_on_sunos ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_unless_on_sunos`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_sunos def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_sunos", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_sunos def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_sunos", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_sunos(reason='Because!') def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_sunos", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/markers/test_skip_unless_on_windows.py0000644000175000017500000000316214027303716032130 0ustar vampasvampas00000000000000""" tests.functional.markers.test_skip_unless_on_windows ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``@pytest.mark.skip_unless_on_windows`` marker """ from unittest import mock def test_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_windows def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_windows", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(skipped=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_not_skipped(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_windows def test_one(): assert True """ ) return_value = True with mock.patch("saltfactories.utils.platform.is_windows", return_value=return_value): res = pytester.runpytest_inprocess() res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*PytestUnknownMarkWarning*") def test_skip_reason(pytester): pytester.makepyfile( """ import pytest @pytest.mark.skip_unless_on_windows(reason='Because!') def test_one(): assert True """ ) return_value = False with mock.patch("saltfactories.utils.platform.is_windows", return_value=return_value): res = pytester.runpytest_inprocess("-ra", "-s", "-vv") res.assert_outcomes(skipped=1) res.stdout.fnmatch_lines(["SKIPPED * test_skip_reason.py:*: Because!"]) pytest-salt-factories-0.907.0/tests/functional/test_cli.py0000644000175000017500000000257514044531341024431 0ustar vampasvampas00000000000000import shutil import subprocess import sys import pytest import saltfactories def cmdline_ids(value): if value[0] == sys.executable: return " ".join(["python"] + value[1:]) return " ".join(value) @pytest.mark.parametrize( "cmdline", ( ["salt-factories", "--coverage"], [sys.executable, "-m", "saltfactories", "--coverage"], ), ids=cmdline_ids, ) def test_salt_factories_cli(cmdline): if not shutil.which(cmdline[0]): pytest.skip("binary {} not found".format(cmdline[0])) ret = subprocess.run( cmdline, stdout=subprocess.PIPE, universal_newlines=True, check=False, ) assert ret.returncode == 0 assert ret.stdout assert ret.stdout.strip() == str(saltfactories.CODE_ROOT_DIR / "utils" / "coverage") @pytest.mark.parametrize( "cmdline", ( ["salt-factories"], [sys.executable, "-m", "saltfactories"], ), ids=cmdline_ids, ) def test_salt_factories_cli_show_help(cmdline): if not shutil.which(cmdline[0]): pytest.skip("binary {} not found".format(cmdline[0])) ret = subprocess.run( cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, check=False, ) assert ret.returncode == 1 assert not ret.stdout assert ret.stderr assert "usage:" in ret.stderr.strip() pytest-salt-factories-0.907.0/tests/functional/test_sys_info.py0000644000175000017500000000221414027303716025505 0ustar vampasvampas00000000000000""" tests.functional.test_sys_info ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests related to system information reports enabled by the `--sys-info` flag. """ import pytest @pytest.mark.parametrize("flag", ["--sysinfo", "--sys-info"]) def test_sysinfo(pytester, flag): pytester.makepyfile( """ def test_one(): assert True """ ) res = pytester.runpytest("-vv", flag) res.assert_outcomes(passed=1) res.stdout.fnmatch_lines( [ "*>> System Information >>*", "*-- Salt Versions Report --*", "*-- System Grains Report --*", "*<< System Information <<*", "collect*", "* PASSED*", "* 1 passed in *", ] ) def test_no_sysinfo(pytester): pytester.makepyfile( """ def test_one(): assert True """ ) res = pytester.runpytest("-vv") res.assert_outcomes(passed=1) res.stdout.no_fnmatch_line("*>> System Information >>*") res.stdout.fnmatch_lines( [ "collect*", "* PASSED*", "* 1 passed in *", ] ) pytest-salt-factories-0.907.0/tests/functional/test_sys_stats.py0000644000175000017500000000415614044531341025713 0ustar vampasvampas00000000000000""" tests.functional.test_sys_stats ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests related to processes system statistics enabled by the `--sys-stats` flag. """ import pytest def test_basic_sys_stats(pytester): pytester.makepyfile( """ def test_one(): assert True """ ) res = pytester.runpytest("-vv", "--sys-stats") res.assert_outcomes(passed=1) res.stdout.fnmatch_lines( [ "* PASSED*", "* Processes Statistics *", "* System - CPU: * % MEM: * % (Virtual Memory)*", "* Test Suite Run - CPU: * % MEM: * % (RSS)*", "* 1 passed in *", ] ) @pytest.mark.skip_on_freebsd def test_basic_sys_stats_uss(pytester): pytester.makepyfile( """ def test_one(): assert True """ ) res = pytester.runpytest("-vv", "--sys-stats", "--sys-stats-uss-mem") res.assert_outcomes(passed=1) res.stdout.fnmatch_lines( [ "* PASSED*", "* Processes Statistics *", "* System - CPU: * % MEM: * % (Virtual Memory)*", "* Test Suite Run - CPU: * % MEM: * % (USS)*", "* 1 passed in *", ] ) @pytest.mark.skip_on_windows @pytest.mark.skip_if_binaries_missing("sshd", "ssh-keygen") def test_proc_sys_stats(pytester): pytester.makepyfile( """ import pytest @pytest.fixture(scope="module") def sshd(request, salt_factories): factory = salt_factories.get_sshd_daemon() with factory.started(): yield factory def test_one(sshd): assert sshd.is_running() """ ) res = pytester.runpytest("-vv", "--sys-stats") res.assert_outcomes(passed=1) res.stdout.fnmatch_lines( [ "* PASSED*", "* Processes Statistics *", "* System - CPU: * % MEM: * % (Virtual Memory)*", "* Test Suite Run - CPU: * % MEM: * % (RSS) * CHILD PROCS: *", "* SSHD - CPU: * % MEM: * % (RSS)*", "* 1 passed in *", ] ) pytest-salt-factories-0.907.0/tests/functional/utils/0000755000175000017500000000000014106144336023403 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/utils/__init__.py0000644000175000017500000000000013720712732025504 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/utils/processes/0000755000175000017500000000000014106144336025411 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/utils/processes/__init__.py0000644000175000017500000000000013720712732027512 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/utils/processes/bases/0000755000175000017500000000000014106144336026506 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/utils/processes/bases/__init__.py0000644000175000017500000000000013720712732030607 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/utils/saltext/0000755000175000017500000000000014106144336025067 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/utils/saltext/__init__.py0000644000175000017500000000000014062653620027170 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/functional/utils/saltext/test_log_handlers.py0000644000175000017500000002657414063562634031166 0ustar vampasvampas00000000000000import logging import sys from datetime import datetime from datetime import timedelta import pytest import saltfactories.utils.platform from saltfactories.bases import Process from saltfactories.exceptions import FactoryTimeout def test_handler_does_not_block_when_not_connected(tempfiles): log_forwarding_socket_hwm = 5 shell = Process(script_name=sys.executable, timeout=10) script = tempfiles.makepyfile( """ # coding=utf-8 import sys import time import logging import multiprocessing from saltfactories.utils.saltext.log_handlers.pytest_log_handler import ZMQHandler # Salt was imported by now and set up it's own logging handlers. Remove them. logging.root.handlers = [] # Setup a stream handler so that we can confirm that logging is working logging.basicConfig( stream=sys.stderr, level=logging.DEBUG, format="[%(levelname)-8s][%(processName)s(%(process)s)] %(message)s" ) # Add our ZMQ handler, it should not block even though it does not connect anywhere handler = ZMQHandler(port=123456, socket_hwm={}) logging.root.addHandler(handler) def main(): log = logging.getLogger("foo") print("Logging started", flush=True) for idx in range(50): log.debug("Foo %s", idx) print("Logging finished", flush=True) logging.shutdown() exit(0) if __name__ == '__main__': multiprocessing.freeze_support() main() """.format( log_forwarding_socket_hwm ) ) try: result = shell.run(script) except FactoryTimeout as exc: # pragma: no cover pytest.fail("The ZMQHandler blocked. Process result:\n{}".format(exc)) # If the exitcode is not 0, that means the script was forcefully terminated, which, # in turn means the ZMQHandler blocked the process when not connected to the log # listener. assert "Logging started" in result.stdout assert "Logging finished" in result.stdout # Since we set a HWM of log_forwarding_socket_hwm, we should at least see # Foo {log_forwarding_socket_hwm + 1} logged to the console. # If we don't, the handler blocked the process assert "Foo {}".format(log_forwarding_socket_hwm + 1) in result.stderr assert result.exitcode == 0 def test_all_messages_received(tempfiles, salt_factories, caplog): log_forwarding_socket_hwm = 500 log_forwarding_calls = log_forwarding_socket_hwm * 2 shell = Process(script_name=sys.executable, timeout=10) script = tempfiles.makepyfile( """ # coding=utf-8 import sys import time import logging import multiprocessing from saltfactories.utils.saltext.log_handlers.pytest_log_handler import ZMQHandler # Salt was imported by now and set up it's own logging handlers. Remove them. logging.root.handlers = [] # Setup a stream handler so that we can confirm that logging is working logging.basicConfig( stream=sys.stderr, level=logging.DEBUG, format="[%(levelname)-8s][%(processName)s(%(process)s)] %(message)s" ) # Add our ZMQ handler handler = ZMQHandler(port={}, socket_hwm={}) logging.root.addHandler(handler) def main(): log = logging.getLogger("foo") print("Logging started", flush=True) for idx in range(1, {} + 1): log.debug("Foo:%s", idx) print("Logging finished", flush=True) exit(0) if __name__ == '__main__': multiprocessing.freeze_support() main() """.format( salt_factories.log_server_port, log_forwarding_socket_hwm, log_forwarding_calls ) ) with caplog.at_level(logging.DEBUG, logger="foo"): try: result = shell.run(script) except FactoryTimeout as exc: # pragma: no cover pytest.fail("The ZMQHandler blocked. Process result:\n{}".format(exc)) # If the exitcode is not 0, that means the script was forcefully terminated, which, # in turn means the ZMQHandler blocked the process when not connected to the log # listener. assert "Logging started" in result.stdout assert "Logging finished" in result.stdout expected_log_message = "Foo:{}".format(log_forwarding_calls) assert expected_log_message in result.stderr assert result.exitcode == 0 timeout = datetime.utcnow() + timedelta(seconds=120) while True: missed = [] found_log_messages = [] # We try multiple times because the script might have properly # flushed it's messages to the log server, but the log server # might still be processing them for record in caplog.records: if record.message.startswith("Foo"): msgnum = int(record.message.split(":")[-1]) found_log_messages.append(msgnum) for idx in range(1, log_forwarding_calls + 1): if idx not in found_log_messages: # pragma: no cover missed.append(idx) try: assert ( len(found_log_messages) == log_forwarding_calls ), "len(found_log_messages={}) != {} // Missed: {}".format( len(found_log_messages), log_forwarding_calls, missed ) break except AssertionError: # pragma: no cover if datetime.utcnow() > timeout: raise @pytest.mark.parametrize("fork_method", ("fork", "spawn")) def test_all_messages_received_multiprocessing(tempfiles, salt_factories, caplog, fork_method): # The purpose of this test is just to make sure if forked/spawned processes inherit the # ZMQHandler and continue logging if fork_method == "fork": if saltfactories.utils.platform.is_windows(): pytest.skip("Start method '{}' is not supported on Windows".format(fork_method)) if sys.version_info >= (3, 8) and saltfactories.utils.platform.is_darwin(): pytest.skip( "Start method '{}' is not supported on Darwin on Py3.8+".format(fork_method) ) num_processes = 2 log_forwarding_calls = 10 shell = Process(script_name=sys.executable, timeout=30) script = tempfiles.makepyfile( """ # coding=utf-8 import os import sys import time import logging import multiprocessing from saltfactories.utils.saltext.log_handlers.pytest_log_handler import ZMQHandler # Salt was imported by now and set up it's own logging handlers. Remove them. logging.root.handlers = [] # Add our ZMQ handler handler = ZMQHandler(port={port}) handler.setLevel(logging.DEBUG) logging.root.addHandler(handler) logging.root.setLevel(logging.DEBUG) def log_from_child_process(idx, parent_pid, evt): evt.set() # process started, ready to start another one import os import logging log = logging.getLogger("foo") for idx in range(1, {calls} + 1): log.debug("Foo(Child of pid %s):%s:%s", parent_pid, idx, os.getpid()) exit(0) def log_from_process(pidx, evt): import os import logging num_processes = {num_processes} procs = [] cevt = multiprocessing.Event() for idx in range(num_processes): proc = multiprocessing.Process( target=log_from_child_process, args=(idx, os.getpid(), cevt), name="P{{}}C{{}}".format(pidx, idx) ) proc.start() procs.append(proc) cevt.wait() cevt.clear() time.sleep(0.25) evt.set() # process started, ready to start another one log = logging.getLogger("foo") for idx in range(1, {calls} + 1): log.debug("Foo:%s:%s", idx, os.getpid()) for proc in procs: proc.join() exit(0) def main(): procs = [] num_processes = {num_processes} print("Logging started", flush=True) evt = multiprocessing.Event() for idx in range(num_processes): proc = multiprocessing.Process( target=log_from_process, args=(idx, evt), name="P{{}}".format(idx) ) proc.start() procs.append(proc) evt.wait() evt.clear() time.sleep(0.25) for proc in procs: proc.join() print("Logging finished", flush=True) exit(0) if __name__ == '__main__': multiprocessing.freeze_support() multiprocessing.set_start_method("{fork_method}") main() """.format( port=salt_factories.log_server_port, calls=log_forwarding_calls, num_processes=num_processes, fork_method=fork_method, ) ) with caplog.at_level(logging.DEBUG, logger="foo"): try: result = shell.run(script) except FactoryTimeout as exc: # pragma: no cover pytest.fail("The ZMQHandler blocked. Process result:\n{}".format(exc)) # If the exitcode is not 0, that means the script was forcefully terminated, which, # in turn means the ZMQHandler blocked the process when not connected to the log # listener. assert "Logging started" in result.stdout assert "Logging finished" in result.stdout assert result.exitcode == 0 # It can take quite a while to receive all these messages, # Specially for Windows and macOS under CI timeout = datetime.utcnow() + timedelta(seconds=30) # We start N processes and each process starts N processes expected_process_count = (num_processes * num_processes) + num_processes while True: procs = {} # We try multiple times because the script might have properly # flushed it's messages to the log server, but the log server # might still be processing them for record in caplog.records: if record.msg.startswith("Foo"): _, msgnum, pid = record.message.split(":") assert record.process == int(pid) procs.setdefault(record.processName, []).append(int(msgnum)) try: assert procs assert len(procs) == expected_process_count break except AssertionError: # pragma: no cover if datetime.utcnow() > timeout: if len(procs) >= num_processes + 1: # Sometimes under CI, some processes either fail to start or don't log. # If we have at least the expected top level processes plus 1 child process, # we've asserted that logs are forwarded from forked processes. # Good enough for the test break raise pytest-salt-factories-0.907.0/tests/functional/utils/test_tempfiles.py0000644000175000017500000001306514033044065027006 0ustar vampasvampas00000000000000import pathlib import shutil import tempfile import pytest from saltfactories.utils import tempfiles @pytest.mark.parametrize("name", ["foo", "foo/bar"]) def test_temp_directory_with_name(name): try: expected_path = pathlib.Path(tempfile.gettempdir()) / name assert expected_path.is_dir() is False with tempfiles.temp_directory(name=name) as tpath: assert tpath.is_dir() assert tpath == expected_path assert expected_path.is_dir() is False finally: shutil.rmtree(str(expected_path), ignore_errors=True) def test_temp_directory_without_name(): try: expected_parent_path = pathlib.Path(tempfile.gettempdir()) with tempfiles.temp_directory() as tpath: assert tpath.is_dir() assert tpath.parent == expected_parent_path assert tpath.is_dir() is False finally: shutil.rmtree(str(tpath), ignore_errors=True) def test_temp_directory_with_basepath(tmp_path): with tempfiles.temp_directory(basepath=tmp_path) as tpath: assert tpath.is_dir() assert str(tpath.parent) == str(tmp_path) assert tpath.is_dir() is False assert tmp_path.is_dir() is True @pytest.mark.parametrize("name", ["foo.txt", "foo/bar.txt"]) def test_temp_file_with_name(tmp_path, name): expected_path = tmp_path / name assert expected_path.is_file() is False with tempfiles.temp_file(name=name, directory=tmp_path) as tpath: assert tpath.is_file() assert str(tpath) == str(expected_path) assert expected_path.is_file() is False def test_temp_file_without_name(tmp_path): expected_parent_path = tmp_path with tempfiles.temp_file(directory=tmp_path) as tpath: assert tpath.is_file() assert str(tpath.parent) == str(expected_parent_path) assert tpath.is_file() is False @pytest.mark.parametrize("name", ["foo.txt", "foo/bar.txt"]) def test_temp_file_with_name_no_directory(name): try: expected_path = pathlib.Path(tempfile.gettempdir()) / name assert expected_path.is_file() is False with tempfiles.temp_file(name=name) as tpath: assert tpath.is_file() assert str(tpath) == str(expected_path) assert expected_path.is_file() is False finally: shutil.rmtree(str(expected_path), ignore_errors=True) def test_temp_file_without_name_no_directory(): try: expected_parent_path = pathlib.Path(tempfile.gettempdir()) with tempfiles.temp_file() as tpath: assert tpath.is_file() assert str(tpath.parent) == str(expected_parent_path) assert tpath.is_file() is False finally: shutil.rmtree(str(tpath), ignore_errors=True) def test_temp_file_does_not_delete_non_empty_directories(tmp_path): expected_parent_path = tmp_path level1_path = expected_parent_path / "level1" level2_path = level1_path / "level2" assert not level1_path.is_dir() assert not level2_path.is_dir() with tempfiles.temp_file("level1/foo.txt", directory=expected_parent_path) as tpath1: assert tpath1.is_file() assert level1_path.is_dir() assert not level2_path.is_dir() with tempfiles.temp_file("level1/level2/foo.txt", directory=expected_parent_path) as tpath2: assert tpath2.is_file() assert level1_path.is_dir() assert level2_path.is_dir() assert not tpath2.is_file() assert not level2_path.is_dir() assert tpath1.is_file() assert level1_path.is_dir() assert not level1_path.is_dir() assert not level2_path.is_dir() @pytest.mark.parametrize("strip_first_newline", [True, False]) def test_temp_file_contents(strip_first_newline): contents = """ These are the contents, first line Second line """ if strip_first_newline: expected_contents = "These are the contents, first line\n Second line\n" else: expected_contents = "\nThese are the contents, first line\n Second line\n" with tempfiles.temp_file(contents=contents, strip_first_newline=strip_first_newline) as tpath: assert tpath.is_file() assert tpath.read_text() == expected_contents def test_salt_env_temp_file(tmp_path): with tempfiles.temp_directory("state-tree", basepath=tmp_path) as state_tree_path: with tempfiles.temp_directory( "base1", basepath=state_tree_path ) as base_env_path_1, tempfiles.temp_directory( "base2", basepath=state_tree_path ) as base_env_path_2: saltenv = tempfiles.SaltEnvs(envs={"base": [base_env_path_1, base_env_path_2]}) # Let's make sure we can access the saltenv by attribute assert saltenv.base == saltenv.envs["base"] # Let's create a temporary file using the `temp_file` helper method top_file_contents = """ 'base': '*': - bar """ with saltenv.base.temp_file("top.sls", contents=top_file_contents) as top_file_path: with pytest.raises(ValueError): # the top file shall not be created within the base_env_path_2 # We have to cast to a string because on Py3.5, the path might be an instance of pathlib2.Path top_file_path.relative_to(str(base_env_path_2)) # It should however, be created within the base_env_path_1 # We have to cast to a string because on Py3.5, the path might be an instance of pathlib2.Path relpath = top_file_path.relative_to(str(base_env_path_1)) assert relpath pytest-salt-factories-0.907.0/tests/integration/0000755000175000017500000000000014106144336022424 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/__init__.py0000644000175000017500000000000013720712732024525 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/0000755000175000017500000000000014106144336024403 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/__init__.py0000644000175000017500000000000013720712732026504 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/cli/0000755000175000017500000000000014106144336025152 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/cli/__init__.py0000644000175000017500000000000013757120510027250 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/cli/conftest.py0000644000175000017500000000171114062623454027355 0ustar vampasvampas00000000000000""" tests.integration.factories.cli.conftest ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ """ import pytest @pytest.fixture(scope="package") def master_id(): return "integration-cli-master" @pytest.fixture(scope="package") def minion_id(): return "integration-cli-minion" @pytest.fixture(scope="package") def salt_master(salt_factories, master_id): """ This fixture just configures and starts a salt-master. """ overrides = {"open_mode": True} factory = salt_factories.salt_master_daemon(master_id, overrides=overrides) with factory.started(): yield factory @pytest.fixture(scope="package") def salt_minion(salt_factories, minion_id, salt_master): """ This fixture just configures and starts a salt-minion. """ factory = salt_master.salt_minion_daemon(minion_id) with factory.started(): yield factory @pytest.fixture(scope="package") def salt_cli(salt_master): return salt_master.salt_cli() pytest-salt-factories-0.907.0/tests/integration/factories/cli/test_salt.py0000644000175000017500000000207514036250542027531 0ustar vampasvampas00000000000000""" tests.integration.factories.cli.test_salt ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``salt`` CLI functionality """ import pytest @pytest.fixture def salt_minion_2(salt_master, salt_minion): """ This fixture just configures and starts a salt-minion. """ factory = salt_master.salt_minion_daemon(salt_minion.id + "-2") with factory.started(): yield factory def test_merged_json_out(salt_cli, salt_minion, salt_minion_2): ret = salt_cli.run("test.ping", minion_tgt="*") assert ret.exitcode == 0, ret assert ret.json assert salt_minion.id in ret.json assert ret.json[salt_minion.id] is True assert salt_minion_2.id in ret.json assert ret.json[salt_minion_2.id] is True def test_merged_json_out_disabled(salt_cli, salt_minion, salt_minion_2): ret = salt_cli.run("test.ping", minion_tgt="*", merge_json_output=False) assert ret.exitcode == 0, ret assert not ret.json assert '"{}": true'.format(salt_minion.id) in ret.stdout assert '"{}": true'.format(salt_minion_2.id) in ret.stdout pytest-salt-factories-0.907.0/tests/integration/factories/daemons/0000755000175000017500000000000014106144336026031 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/__init__.py0000644000175000017500000000000013723372562030140 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/api/0000755000175000017500000000000014106144336026602 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/api/__init__.py0000644000175000017500000000000013723372562030711 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/api/test_api.py0000644000175000017500000000113614036250542030764 0ustar vampasvampas00000000000000import pytest from saltfactories.utils import ports from saltfactories.utils import random_string @pytest.fixture(scope="module") def master(salt_factories): defaults = {"rest_tornado": {"port": ports.get_unused_localhost_port(), "disable_ssl": True}} factory = salt_factories.salt_master_daemon(random_string("master-"), defaults=defaults) with factory.started(): yield factory @pytest.fixture(scope="module") def salt_api(master): factory = master.salt_api_daemon() with factory.started(): yield factory def test_api(salt_api): assert salt_api.is_running() pytest-salt-factories-0.907.0/tests/integration/factories/daemons/api/test_restarts.py0000644000175000017500000000141714036250542032064 0ustar vampasvampas00000000000000import pytest from saltfactories.utils import ports from saltfactories.utils import random_string @pytest.fixture(scope="module") def master(salt_factories): defaults = {"rest_tornado": {"port": ports.get_unused_localhost_port(), "disable_ssl": True}} factory = salt_factories.salt_master_daemon(random_string("master-"), defaults=defaults) with factory.started(): yield factory def test_multiple_start_stops(master): factory = master.salt_api_daemon() assert factory.is_running() is False pid = None with factory.started(): assert factory.is_running() is True pid = factory.pid assert factory.is_running() is False with factory.started(): assert factory.is_running() is True assert factory.pid != pid pytest-salt-factories-0.907.0/tests/integration/factories/daemons/container/0000755000175000017500000000000014106144336030013 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/container/__init__.py0000644000175000017500000000000013723372562032122 0ustar vampasvampas00000000000000././@LongLink0000000000000000000000000000014600000000000011216 Lustar 00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/container/test_container_factory.pypytest-salt-factories-0.907.0/tests/integration/factories/daemons/container/test_container_factory.p0000644000175000017500000000460514036250542034750 0ustar vampasvampas00000000000000import pytest from saltfactories.daemons.container import Container from saltfactories.utils import ports from saltfactories.utils import socket docker = pytest.importorskip("docker") from docker.errors import DockerException @pytest.fixture(scope="module") def docker_client(): try: client = docker.from_env() except DockerException: pytest.skip("Failed to get a connection to docker running on the system") connectable = Container.client_connectable(client) if connectable is not True: # pragma: no cover pytest.skip(connectable) return client @pytest.fixture(scope="module") def echo_server_port(): return ports.get_unused_localhost_port() @pytest.fixture(scope="module") def docker_container(salt_factories, docker_client, echo_server_port): container = salt_factories.get_container( "echo-server-test", "cjimti/go-echo", docker_client=docker_client, check_ports=[echo_server_port], container_run_kwargs={ "ports": {"{}/tcp".format(echo_server_port): echo_server_port}, "environment": {"TCP_PORT": str(echo_server_port), "NODE_NAME": "echo-server-test"}, }, ) with container.started() as factory: yield factory @pytest.mark.skip_on_darwin @pytest.mark.skip_on_windows def test_spawn_container(docker_container, echo_server_port): message = b"Hello!\n" client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: client.connect(("127.0.0.1", echo_server_port)) client.settimeout(0.1) # Get any welcome message from the server while True: try: client.recv(4096) except socket.timeout: break client.send(message) response = None while True: try: response = client.recv(4096) except socket.timeout: break assert response is not None assert response == message finally: client.close() @pytest.mark.skip_on_darwin @pytest.mark.skip_on_windows def test_container_run(docker_container): ret = docker_container.run("echo", "foo") assert ret.exitcode == 0 assert ret.stdout == "foo\n" assert ret.stderr is None ret = docker_container.run("sh", "-c", ">&2 echo foo") assert ret.exitcode == 0 assert ret.stdout is None assert ret.stderr == "foo\n" pytest-salt-factories-0.907.0/tests/integration/factories/daemons/master/0000755000175000017500000000000014106144336027324 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/master/__init__.py0000644000175000017500000000000013723372562031433 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/master/test_master.py0000644000175000017500000001042414036250542032230 0ustar vampasvampas00000000000000import os import tempfile import pytest import salt.defaults.exitcodes from saltfactories.exceptions import FactoryNotStarted from saltfactories.utils import random_string @pytest.fixture(scope="module") def master(salt_factories): factory = salt_factories.salt_master_daemon( random_string("master-"), overrides={"max_open_files": 4096} ) with factory.started(): yield factory @pytest.fixture(scope="module") def minion(master): factory = master.salt_minion_daemon(random_string("minion-1-")) with factory.started(): yield factory @pytest.fixture def minion_3(master): factory = master.salt_minion_daemon(random_string("minion-3-")) with factory.started(): yield factory @pytest.fixture def salt_run(master): return master.salt_run_cli() @pytest.fixture def salt_cp(master): return master.salt_cp_cli() @pytest.fixture def salt_key(master): return master.salt_key_cli() @pytest.fixture def salt_call(minion): return minion.salt_call_cli() def test_master(master): assert master.is_running() def test_salt_run(master, salt_run): max_open_files_config_value = master.config["max_open_files"] ret = salt_run.run("config.get", "max_open_files") assert ret.exitcode == 0, ret assert ret.json == max_open_files_config_value def test_salt_cp(master, minion, salt_cp, tempfiles): """ Test copying a file from the master to the minion """ tfile = tempfile.NamedTemporaryFile(delete=True) tfile.close() dest = tfile.name try: contents = "id: foo" sls = tempfiles.makeslsfile(contents) assert master.is_running() assert minion.is_running() ret = salt_cp.run(minion.id, sls, dest) assert ret.exitcode == 0, ret assert ret.json == {minion.id: {dest: True}}, ret assert os.path.exists(dest) with open(dest) as rfh: assert rfh.read() == contents finally: # pragma: no cover if os.path.exists(dest): os.unlink(dest) tfile = tempfile.NamedTemporaryFile(delete=True) tfile.close() dest = tfile.name try: contents = "id: foo" sls = tempfiles.makeslsfile(contents) assert master.is_running() assert minion.is_running() ret = salt_cp.run(sls, dest, minion_tgt=minion.id) assert ret.exitcode == 0, ret assert ret.json == {dest: True}, ret assert os.path.exists(dest) with open(dest) as rfh: assert rfh.read() == contents finally: # pragma: no cover if os.path.exists(dest): os.unlink(dest) def test_salt_cp_no_match(master, minion, salt_cp, tempfiles): assert master.is_running() assert minion.is_running() tfile = tempfile.NamedTemporaryFile(delete=True) tfile.close() dest = tfile.name try: contents = "id: foo" sls = tempfiles.makeslsfile(contents) assert master.is_running() assert minion.is_running() ret = salt_cp.run(sls, dest, minion_tgt="minion-2") assert ret.exitcode == 0, ret assert not ret.json, ret assert not os.path.exists(dest) finally: # pragma: no cover if os.path.exists(dest): os.unlink(dest) @pytest.mark.skip_on_salt_system_install def test_salt_key(master, minion, minion_3, salt_key): ret = salt_key.run("--list-all") assert ret.exitcode == 0, ret assert ret.json == { "minions": [minion.id, minion_3.id], "minions_pre": [], "minions_denied": [], "minions_rejected": [], }, ret @pytest.mark.skip_on_windows @pytest.mark.skip_on_salt_system_install def test_exit_status_unknown_user(salt_factories): master = salt_factories.salt_master_daemon("set-exitcodes", overrides={"user": "unknown-user"}) with pytest.raises(FactoryNotStarted) as exc: master.start(max_start_attempts=1) assert exc.value.exitcode == salt.defaults.exitcodes.EX_NOUSER, str(exc.value) assert "The user is not available." in exc.value.stderr, str(exc.value) def test_state_tree(master, salt_call): sls_contents = """ test: test.succeed_without_changes """ with master.state_tree.base.temp_file("foo.sls", sls_contents): ret = salt_call.run("state.sls", "foo") assert ret.exitcode == 0 pytest-salt-factories-0.907.0/tests/integration/factories/daemons/master/test_restarts.py0000644000175000017500000000113014036250542032576 0ustar vampasvampas00000000000000import pytest from saltfactories.utils import random_string @pytest.fixture(scope="module") def master(salt_factories): factory = salt_factories.salt_master_daemon( random_string("master-"), overrides={"max_open_files": 4096} ) return factory def test_multiple_start_stops(master): assert master.is_running() is False pid = None with master.started(): assert master.is_running() is True pid = master.pid assert master.is_running() is False with master.started(): assert master.is_running() is True assert master.pid != pid pytest-salt-factories-0.907.0/tests/integration/factories/daemons/minion/0000755000175000017500000000000014106144336027322 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/minion/__init__.py0000644000175000017500000000000013723372562031431 0ustar vampasvampas00000000000000././@LongLink0000000000000000000000000000015000000000000011211 Lustar 00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/minion/test_event_forwarder_engine.pypytest-salt-factories-0.907.0/tests/integration/factories/daemons/minion/test_event_forwarder_engine0000644000175000017500000000330214036250542035022 0ustar vampasvampas00000000000000import logging import time import pytest import salt.defaults.events from saltfactories.utils import random_string log = logging.getLogger(__name__) @pytest.fixture(scope="module") def master(salt_factories): factory = salt_factories.salt_master_daemon(random_string("master-")) with factory.started(): yield factory @pytest.fixture(scope="module") def minion(master): factory = master.salt_minion_daemon(random_string("minion-")) with factory.started(): yield factory @pytest.fixture def salt_call_cli(minion): return minion.salt_call_cli() def test_event_listener_engine(minion, salt_call_cli, event_listener): """ There are some events which the minion fires internally that never reach the master. We test if we're receiving those """ assert minion.is_running() event_pattern = (minion.id, salt.defaults.events.MINION_PILLAR_REFRESH_COMPLETE) start_time = time.time() stop_time = start_time + 120 ret = salt_call_cli.run("saltutil.refresh_pillar") assert ret.exitcode == 0, ret master_event = None expected_tag = salt.defaults.events.MINION_PILLAR_REFRESH_COMPLETE master_event_pattern = (minion.id, expected_tag) while True: if time.time() > stop_time: pytest.fail("Failed to receive the refresh pillar event.") if not master_event: events = event_listener.get_events([master_event_pattern], after_time=start_time) for event in events: master_event = event break if master_event: # We got all events back break time.sleep(0.5) log.debug("Refresh pillar event received: %s", master_event) pytest-salt-factories-0.907.0/tests/integration/factories/daemons/minion/test_minion.py0000644000175000017500000000371114062623454032232 0ustar vampasvampas00000000000000import pytest from saltfactories.utils import random_string @pytest.fixture(scope="module") def master(salt_factories): factory = salt_factories.salt_master_daemon(random_string("master-")) with factory.started(): yield factory @pytest.fixture(scope="module") def minion(master): factory = master.salt_minion_daemon(random_string("minion-")) with factory.started(): yield factory @pytest.fixture def salt_cli(master): return master.salt_cli() @pytest.fixture def salt_call_cli(minion): return minion.salt_call_cli() def test_minion(minion, salt_cli): assert minion.is_running() ret = salt_cli.run("test.ping", minion_tgt=minion.id) assert ret.exitcode == 0, ret assert ret.json is True def test_no_match(minion, salt_cli): assert minion.is_running() ret = salt_cli.run("test.ping", minion_tgt="minion-2") assert ret.exitcode == 2, ret assert not ret.json def test_show_jid(minion, salt_cli): assert minion.is_running() ret = salt_cli.run("--show-jid", "test.ping", minion_tgt=minion.id) assert ret.exitcode == 0, ret assert ret.json is True def test_minion_salt_call(minion, salt_call_cli): assert minion.is_running() ret = salt_call_cli.run("test.ping") assert ret.exitcode == 0, ret assert ret.json is True # Now with --local ret = salt_call_cli.run("--local", "test.ping") assert ret.exitcode == 0, ret assert ret.json is True def test_salt_call_exception_handling_doesnt_timeout(minion, salt_call_cli): ret = salt_call_cli.run( "test.raise_exception", "OSError", "2", "No such file or directory", "/tmp/foo.txt" ) assert ret.exitcode == 1, ret def test_state_tree(minion, salt_call_cli): sls_contents = """ test: test.succeed_without_changes """ with minion.state_tree.base.temp_file("foo.sls", sls_contents): ret = salt_call_cli.run("--local", "state.sls", "foo") assert ret.exitcode == 0 pytest-salt-factories-0.907.0/tests/integration/factories/daemons/minion/test_restarts.py0000644000175000017500000000121714036250542032602 0ustar vampasvampas00000000000000import pytest from saltfactories.utils import random_string @pytest.fixture(scope="module") def master(salt_factories): factory = salt_factories.salt_master_daemon(random_string("master-")) with factory.started(): yield factory def test_multiple_start_stops(master): factory = master.salt_minion_daemon(random_string("minion-")) assert factory.is_running() is False pid = None with factory.started(): assert factory.is_running() is True pid = factory.pid assert factory.is_running() is False with factory.started(): assert factory.is_running() is True assert factory.pid != pid pytest-salt-factories-0.907.0/tests/integration/factories/daemons/proxy/0000755000175000017500000000000014106144336027212 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/proxy/__init__.py0000644000175000017500000000000013723372562031321 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/proxy/test_proxy_minion.py0000644000175000017500000000446514062623454033372 0ustar vampasvampas00000000000000import sys import pytest from saltfactories.utils import platform from saltfactories.utils import random_string pytestmark = pytest.mark.skipif( sys.platform.lower().startswith("win"), reason="Disabled on windows because of multiprocessing pickle spawning issues", ) @pytest.fixture(scope="module") def master(salt_factories): factory = salt_factories.salt_master_daemon(random_string("master-")) with factory.started(): yield factory @pytest.fixture(scope="module") def proxy_minion(master): factory = master.salt_proxy_minion_daemon(random_string("proxy-minion-")) with factory.started(): yield factory @pytest.fixture def salt_cli(master): return master.salt_cli() @pytest.fixture def salt_call_cli(proxy_minion): return proxy_minion.salt_call_cli() def test_proxy_minion(proxy_minion, salt_cli): assert proxy_minion.is_running() ret = salt_cli.run("test.ping", minion_tgt=proxy_minion.id) assert ret.exitcode == 0, ret assert ret.json is True def test_no_match(proxy_minion, salt_cli): assert proxy_minion.is_running() ret = salt_cli.run("test.ping", minion_tgt="proxy-minion-2") assert ret.exitcode == 2, ret assert not ret.json def test_show_jid(proxy_minion, salt_cli): if platform.is_darwin() and sys.version_info[:2] == (3, 7): pytest.skip( "This test passes on Darwin under Py3.7, it has the expected output " "and yet, it times out. Will investigate later." ) assert proxy_minion.is_running() ret = salt_cli.run("--show-jid", "test.ping", minion_tgt=proxy_minion.id) assert ret.exitcode == 0, ret assert ret.json is True def test_proxy_minion_salt_call(proxy_minion, salt_call_cli): assert proxy_minion.is_running() ret = salt_call_cli.run("test.ping") assert ret.exitcode == 0, ret assert ret.json is True # Now with --local ret = salt_call_cli.run("--proxyid={}".format(proxy_minion.id), "test.ping") assert ret.exitcode == 0, ret assert ret.json is True def test_state_tree(proxy_minion, salt_call_cli): sls_contents = """ test: test.succeed_without_changes """ with proxy_minion.state_tree.base.temp_file("foo.sls", sls_contents): ret = salt_call_cli.run("--local", "state.sls", "foo") assert ret.exitcode == 0 pytest-salt-factories-0.907.0/tests/integration/factories/daemons/proxy/test_restarts.py0000644000175000017500000000151314036250542032471 0ustar vampasvampas00000000000000import sys import pytest from saltfactories.utils import random_string pytestmark = pytest.mark.skipif( sys.platform.lower().startswith("win"), reason="Disabled on windows because of multiprocessing pickle spawning issues", ) @pytest.fixture(scope="module") def master(salt_factories): factory = salt_factories.salt_master_daemon(random_string("master-")) with factory.started(): yield factory def test_multiple_start_stops(master): factory = master.salt_proxy_minion_daemon(random_string("proxy-minion-")) assert factory.is_running() is False pid = None with factory.started(): assert factory.is_running() is True pid = factory.pid assert factory.is_running() is False with factory.started(): assert factory.is_running() is True assert factory.pid != pid pytest-salt-factories-0.907.0/tests/integration/factories/daemons/ssh/0000755000175000017500000000000014106144336026626 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/ssh/__init__.py0000644000175000017500000000000013723372562030735 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/ssh/test_salt_ssh.py0000644000175000017500000000323714062170520032057 0ustar vampasvampas00000000000000import logging import textwrap import pytest from saltfactories.utils import random_string log = logging.getLogger(__name__) @pytest.fixture(scope="module") @pytest.mark.skip_if_binaries_missing("sshd", "ssh-keygen") def sshd(salt_factories): # Set StrictModes to no because our config directory lives in /tmp and those permissions # are not acceptable by sshd strict paranoia. sshd_config_dict = {"StrictModes": "no"} factory = salt_factories.get_sshd_daemon(sshd_config_dict=sshd_config_dict) with factory.started(): yield factory @pytest.fixture(scope="module") def master(salt_factories): return salt_factories.salt_master_daemon(random_string("master-")) @pytest.fixture(scope="module") def salt_ssh_cli(sshd, salt_factories, master): roster_file_path = salt_factories.tmp_root_dir / "salt_ssh_roster" with open(str(roster_file_path), "w") as wfh: contents = textwrap.dedent( """\ localhost: host: 127.0.0.1 port: {} mine_functions: test.arg: ['itworked'] """.format( sshd.listen_port ) ) wfh.write(contents) log.debug("Wrote '%s' with contents:\n%s", roster_file_path, contents) log.warning("") try: yield master.salt_ssh_cli( roster_file=str(roster_file_path), client_key=str(sshd.client_key) ) finally: roster_file_path.unlink() @pytest.mark.skip_on_windows def test_salt_ssh(salt_ssh_cli): ret = salt_ssh_cli.run("--ignore-host-keys", "test.echo", "It Works!", minion_tgt="localhost") assert ret.exitcode == 0 assert ret.json == "It Works!" pytest-salt-factories-0.907.0/tests/integration/factories/daemons/sshd/0000755000175000017500000000000014106144336026772 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/sshd/__init__.py0000644000175000017500000000000013741766301031077 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/sshd/test_sshd.py0000644000175000017500000000225413775110164031352 0ustar vampasvampas00000000000000import subprocess import pytest @pytest.fixture(scope="module") @pytest.mark.skip_if_binaries_missing("sshd", "ssh-keygen") def sshd(salt_factories): # Set StrictModes to no because our config directory lives in /tmp and those permissions # are not acceptable by sshd strict paranoia. sshd_config_dict = {"StrictModes": "no"} factory = salt_factories.get_sshd_daemon(sshd_config_dict=sshd_config_dict) with factory.started(): yield factory @pytest.mark.skip_on_windows def test_sshd(sshd): assert sshd.is_running() @pytest.mark.skip_on_windows @pytest.mark.skip_if_binaries_missing("ssh") def test_connect(sshd): cmd = subprocess.run( [ "ssh", "-i", str(sshd.client_key), "-p", str(sshd.listen_port), "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", sshd.listen_address, "echo Foo", ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, check=False, ) assert cmd.returncode == 0, cmd assert "Foo" in cmd.stdout, cmd pytest-salt-factories-0.907.0/tests/integration/factories/daemons/syndic/0000755000175000017500000000000014106144336027322 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/syndic/__init__.py0000644000175000017500000000000013723372562031431 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/factories/daemons/syndic/test_syndic.py0000644000175000017500000001250714062623454032235 0ustar vampasvampas00000000000000import pytest pytestmark = pytest.mark.skip(reason="Skipping until we devote time to fix syndic support") @pytest.fixture(scope="module") def master_of_masters(salt_factories): """ This is the master of all masters, top of the chain """ factory = salt_factories.salt_master_daemon("master-of-masters", order_masters=True) with factory.started(): yield factory @pytest.fixture(scope="module") def minion_1(master_of_masters): """ This minion connects to the master-of-masters directly """ assert master_of_masters.is_running() factory = master_of_masters.salt_minion_daemon("minion-1") with factory.started(): yield factory @pytest.fixture(scope="module") def configure_salt_syndic(master_of_masters, minion_1): """ This syndic will run in tandem with a master and minion which share the same ID, connected to the upstream master-of-masters master. """ factory = master_of_masters.salt_syndic_daemon("syndic-1") with factory.started(): yield factory @pytest.fixture(scope="module") def syndic_master(master_of_masters, configure_salt_syndic): """ This is a second master, which will connect to master-of-masters through the syndic. We depend on the minion_1 fixture just so we get both the master-of-masters and minion-1 fixtures running when this master starts. """ factory = master_of_masters.salt_master_daemon("syndic-1") with factory.started(): yield factory @pytest.fixture(scope="module") def syndic_minion(syndic_master): """ This is a second master, which will connect to master-of-masters through the syndic. We depend on the minion_1 fixture just so we get both the master-of-masters and minion-1 fixtures running when this master starts. """ assert syndic_master.is_running() factory = syndic_master.salt_minion_daemon("syndic-1") with factory.started(): yield factory @pytest.fixture(scope="module") def minion_2(syndic_master): """ This minion will connect to the syndic-1 master """ assert syndic_master.is_running() factory = syndic_master.salt_minion_daemon("minion-2") with factory.started(): yield factory @pytest.fixture(scope="module") def master_of_masters_salt_cli(master_of_masters, minion_1): """ This is the 'salt' CLI tool, connected to master-of-masters. Should be able to ping minion-1 directly connected to it and minion-2 through the syndic """ assert master_of_masters.is_running() assert minion_1.is_running() return master_of_masters.salt_cli() @pytest.fixture(scope="module") def syndic_master_salt_cli(syndic_master, syndic_minion, minion_2): """ This is the 'salt' CLI tool, connected to master-of-masters. Should be able to ping minion-1 directly connected to it and minion-2 through the syndic """ assert syndic_master.is_running() assert syndic_minion.is_running() assert minion_2.is_running() return syndic_master.salt_cli() @pytest.fixture(scope="module") def syndic(salt_factories, master_of_masters, minion_1, syndic_master, syndic_minion, minion_2): """ This syndic will run in tandem with master-2, connected to the upstream master-of-masters master. """ assert master_of_masters.is_running() assert minion_1.is_running() assert syndic_master.is_running() assert syndic_minion.is_running() assert minion_2.is_running() return master_of_masters.salt_syndic_daemon(syndic_master.id) @pytest.fixture(scope="module") def salt_cli(master_of_masters_salt_cli, syndic_master_salt_cli, syndic): return master_of_masters_salt_cli def test_minion_1(master_of_masters_salt_cli): """ Just test that we can ping minion-1 """ ret = master_of_masters_salt_cli.run("test.ping", minion_tgt="minion-1", _timeout=60) assert ret.exitcode == 0, ret assert ret.json is True, ret def test_minion_syndic_1(syndic_master_salt_cli): """ Just test that we can ping minion-1 """ ret = syndic_master_salt_cli.run("test.ping", minion_tgt="syndic-1", _timeout=60) assert ret.exitcode == 0, ret assert ret.json is True, ret def test_minion_2(syndic_master_salt_cli): """ Just test that we can ping minion-2 """ ret = syndic_master_salt_cli.run("test.ping", minion_tgt="minion-2", _timeout=60) assert ret.exitcode == 0, ret assert ret.json is True, ret @pytest.mark.skip("Syndics are still broken. Moving on for now") def test_syndic(syndic, salt_cli): assert syndic.is_running() # Are we able to ping the minion connected to the master-of-masters ret = salt_cli.run("test.ping", minion_tgt="minion-1", _timeout=60) assert ret.exitcode == 0, ret assert ret.json is True, ret # Are we able to ping the minions connected to the syndic-master ret = salt_cli.run("test.ping", minion_tgt="syndic-1", _timeout=60) assert ret.exitcode == 0, ret assert ret.json is True, ret ret = salt_cli.run("test.ping", minion_tgt="minion-2", _timeout=60) assert ret.exitcode == 0, ret assert ret.json is True, ret # Are we able to ping all of them? ret = salt_cli.run("test.ping", minion_tgt="*", _timeout=60) assert ret.exitcode == 0, ret assert "minion-1" in ret.json assert ret.json["minion-1"] is True assert "minion-2" in ret.json assert ret.json["minion-2"] is True pytest-salt-factories-0.907.0/tests/integration/utils/0000755000175000017500000000000014106144336023564 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/utils/__init__.py0000644000175000017500000000000014062617367025675 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/utils/saltext/0000755000175000017500000000000014106144336025250 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/utils/saltext/__init__.py0000644000175000017500000000000014062653620027351 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/integration/utils/saltext/test_log_handlers.py0000644000175000017500000000205314062653621031325 0ustar vampasvampas00000000000000import logging import pytest from saltfactories.utils import random_string @pytest.fixture(scope="module") def master(salt_factories): factory = salt_factories.salt_master_daemon(random_string("master-")) with factory.started(): yield factory @pytest.fixture(scope="module") def minion(master): factory = master.salt_minion_daemon(random_string("minion-")) with factory.started(): yield factory @pytest.fixture def salt_cli(master): return master.salt_cli() def test_logs_forwarded_from_sub_processes(salt_cli, minion, caplog): assert minion.is_running() with caplog.at_level(logging.DEBUG): ret = salt_cli.run("test.ping", minion_tgt=minion.id) assert ret.exitcode == 0, ret assert ret.json is True non_main_processes_count = 0 for record in caplog.records: if record.processName != "MainProcess": non_main_processes_count += 1 # We should see at least a log record from the MWorker and ProcessPayload processes assert non_main_processes_count >= 2 pytest-salt-factories-0.907.0/tests/scenarios/0000755000175000017500000000000014106144336022067 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/scenarios/__init__.py0000644000175000017500000000000014033044065024163 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/scenarios/examples/0000755000175000017500000000000014106144336023705 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/scenarios/examples/__init__.py0000644000175000017500000000000014033044065026001 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/scenarios/examples/conftest.py0000644000175000017500000000243414062633167026115 0ustar vampasvampas00000000000000import shutil import attr import pytest from saltfactories import CODE_ROOT_DIR from saltfactories.utils.virtualenv import VirtualEnv @attr.s(kw_only=True, slots=True, frozen=True) class ExtensionVirtualEnv(VirtualEnv): extension_path = attr.ib() tmp_path = attr.ib() copy_path = attr.ib(init=False) venv = attr.ib(init=False) @copy_path.default def _default_copy_path(self): _copy_path = self.tmp_path / "code" shutil.copytree(str(self.extension_path), str(_copy_path)) return _copy_path @venv.default def _init_virtualenv(self): return VirtualEnv(self.tmp_path / ".venv", cwd=self.copy_path) def __enter__(self): self.venv.__enter__() self.venv.run("git", "init", ".") self.venv.run("git", "add", ".") self.venv.install(str(CODE_ROOT_DIR.parent.parent)) # Salt(3003) doesn't yet support pyzmq 21.0.x self.venv.install("pyzmq<21.0.0") self.venv.install(".[tests]") return self.venv def __exit__(self, *_): self.venv.__exit__(*_) @pytest.fixture def extension_venv(tmp_path): def create_extension_virtualenv(extension_path): return ExtensionVirtualEnv(extension_path=extension_path, tmp_path=tmp_path) return create_extension_virtualenv pytest-salt-factories-0.907.0/tests/scenarios/examples/test_echoext.py0000644000175000017500000000051214033044065026750 0ustar vampasvampas00000000000000from saltfactories import CODE_ROOT_DIR def test_echoext(extension_venv): extension_path = CODE_ROOT_DIR.parent.parent / "examples" / "echo-extension" with extension_venv(extension_path) as venv: ret = venv.run(venv.venv_python, "-m", "pytest", str(extension_path), check=False) assert ret.exitcode == 0 pytest-salt-factories-0.907.0/tests/unit/0000755000175000017500000000000014106144336021060 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/__init__.py0000644000175000017500000000000013720712732023161 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/factories/0000755000175000017500000000000014106144336023037 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/factories/__init__.py0000644000175000017500000000000013723372562025146 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/factories/base/0000755000175000017500000000000014106144336023751 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/factories/base/__init__.py0000644000175000017500000000000013723372562026060 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/factories/base/test_salt_cli_factory.py0000644000175000017500000005331414036250542030710 0ustar vampasvampas00000000000000import json import os import shutil import sys from collections import OrderedDict from unittest import mock import pytest from saltfactories.bases import SaltCli from saltfactories.utils.processes import ProcessResult @pytest.fixture def config_dir(pytester): _conf_dir = pytester.mkdir("conf") try: yield _conf_dir finally: shutil.rmtree(str(_conf_dir), ignore_errors=True) @pytest.fixture def minion_id(): return "test-minion-id" @pytest.fixture def config_file(config_dir, minion_id): config_file = str(config_dir / "config") with open(config_file, "w") as wfh: wfh.write("id: {}\n".format(minion_id)) return config_file @pytest.fixture def cli_script_name(pytester): py_file = pytester.makepyfile( """ print("This would be the CLI script") """ ) try: yield str(py_file) finally: py_file.unlink() def test_default_cli_flags(minion_id, config_dir, config_file, cli_script_name): config = {"conf_file": config_file, "id": "the-id"} args = ["test.ping"] kwargs = {"minion_tgt": minion_id} expected = [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--out=json", "--out-indent=0", "--log-level=critical", minion_id, "test.ping", ] proc = SaltCli(script_name=cli_script_name, config=config) cmdline = proc.cmdline(*args, **kwargs) assert cmdline == expected @pytest.mark.parametrize("flag", ["-l", "--log-level", "--log-level="]) def test_override_log_level(minion_id, config_dir, config_file, cli_script_name, flag): config = {"conf_file": config_file, "id": "the-id"} args = [] if flag.endswith("="): flag_overrides_args = [flag + "info"] else: flag_overrides_args = [flag, "info"] args.extend(flag_overrides_args) args.append("test.ping") kwargs = {"minion_tgt": minion_id} expected = ( [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--out=json", "--out-indent=0", minion_id, ] + flag_overrides_args + ["test.ping"] ) proc = SaltCli(script_name=cli_script_name, config=config) cmdline = proc.cmdline(*args, **kwargs) assert cmdline == expected @pytest.mark.parametrize("flag", ["--out", "--output", "--out=", "--output="]) def test_override_output(minion_id, config_dir, config_file, cli_script_name, flag): config = {"conf_file": config_file, "id": "the-id"} args = [] if flag.endswith("="): flag_overrides_args = [flag + "nested"] else: flag_overrides_args = [flag, "nested"] args.extend(flag_overrides_args) args.append("test.ping") kwargs = {"minion_tgt": minion_id} expected = ( [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--log-level=critical", minion_id, ] + flag_overrides_args + ["test.ping"] ) proc = SaltCli(script_name=cli_script_name, config=config) cmdline = proc.cmdline(*args, **kwargs) assert cmdline == expected @pytest.mark.parametrize( "flag", ["--out-indent", "--output-indent", "--out-indent=", "--output-indent="] ) def test_override_output_indent(minion_id, config_dir, config_file, cli_script_name, flag): config = {"conf_file": config_file, "id": "the-id"} args = [] if flag.endswith("="): flag_overrides_args = [flag + "1"] else: flag_overrides_args = [flag, "1"] args.extend(flag_overrides_args) args.append("test.ping") kwargs = {"minion_tgt": minion_id} expected = ( [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--out=json", "--log-level=critical", minion_id, ] + flag_overrides_args + ["test.ping"] ) proc = SaltCli(script_name=cli_script_name, config=config) cmdline = proc.cmdline(*args, **kwargs) assert cmdline == expected def test_cli_timeout_lesser_than_timeout_kw(minion_id, config_dir, config_file, cli_script_name): # Both --timeout and _timeout are passed. # Since --timeout is less than _timeout, the value of _timeout does not changed timeout = 10 explicit_timeout = 60 cli_timeout = 6 config = {"conf_file": config_file, "id": "the-id"} args = ["--timeout", str(cli_timeout), "test.ping"] kwargs = {"minion_tgt": minion_id, "_timeout": explicit_timeout} expected = [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--out=json", "--out-indent=0", "--log-level=critical", minion_id, "--timeout", "6", "test.ping", ] popen_mock = mock.MagicMock() popen_mock.pid = os.getpid() popen_mock.poll = mock.MagicMock(side_effect=[None, None, None, None, True]) terminate_mock = mock.MagicMock(return_value=ProcessResult(0, "", "", cmdline=())) popen_mock.terminate = terminate_mock proc = SaltCli(script_name=cli_script_name, config=config, timeout=timeout) with mock.patch.object(proc.impl, "init_terminal", popen_mock), mock.patch.object( proc, "terminate", terminate_mock ): proc.impl._terminal = popen_mock # We set __cli_timeout_supported__ to True just to test. This would be an attribute set # at the class level for Salt CLI's that support the timeout flag, like for example, salt-run proc.__cli_timeout_supported__ = True proc.run(*args, **kwargs) assert proc.impl._terminal_timeout == explicit_timeout assert popen_mock.call_args[0][0] == expected # pylint: disable=unsubscriptable-object def test_cli_timeout_matches_timeout_kw(minion_id, config_dir, config_file, cli_script_name): # Both --timeout and _timeout are passed. # Since --timeout is greater than _timeout, the value of _timeout is updated to the value of --timeout plus 5 timeout = 10 explicit_timeout = 20 cli_timeout = 20 config = {"conf_file": config_file, "id": "the-id"} args = ["--timeout", str(cli_timeout), "test.ping"] kwargs = {"minion_tgt": minion_id, "_timeout": explicit_timeout} expected = [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--out=json", "--out-indent=0", "--log-level=critical", minion_id, "--timeout", "20", "test.ping", ] popen_mock = mock.MagicMock() popen_mock.pid = os.getpid() popen_mock.poll = mock.MagicMock(side_effect=[None, None, None, None, True]) terminate_mock = mock.MagicMock(return_value=ProcessResult(0, "", "", cmdline=())) popen_mock.terminate = terminate_mock proc = SaltCli(script_name=cli_script_name, config=config, timeout=timeout) with mock.patch.object(proc.impl, "init_terminal", popen_mock), mock.patch.object( proc, "terminate", terminate_mock ): proc.impl._terminal = popen_mock # We set __cli_timeout_supported__ to True just to test. This would be an attribute set # at the class level for Salt CLI's that support the timeout flag, like for example, salt-run proc.__cli_timeout_supported__ = True ret = proc.run(*args, **kwargs) assert proc.impl._terminal_timeout == cli_timeout + 10 assert popen_mock.call_args[0][0] == expected # pylint: disable=unsubscriptable-object def test_cli_timeout_greater_than_timeout_kw(minion_id, config_dir, config_file, cli_script_name): # Both --timeout and _timeout are passed. # Since --timeout is greater than _timeout, the value of _timeout is updated to the value of --timeout plus 5 timeout = 10 explicit_timeout = 20 cli_timeout = 60 config = {"conf_file": config_file, "id": "the-id"} args = ["--timeout", str(cli_timeout), "test.ping"] kwargs = {"minion_tgt": minion_id, "_timeout": explicit_timeout} expected = [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--out=json", "--out-indent=0", "--log-level=critical", minion_id, "--timeout", "60", "test.ping", ] popen_mock = mock.MagicMock() popen_mock.pid = os.getpid() popen_mock.poll = mock.MagicMock(side_effect=[None, None, None, None, True]) terminate_mock = mock.MagicMock(return_value=ProcessResult(0, "", "", cmdline=())) popen_mock.terminate = terminate_mock proc = SaltCli(script_name=cli_script_name, config=config, timeout=timeout) with mock.patch.object(proc.impl, "init_terminal", popen_mock), mock.patch.object( proc, "terminate", terminate_mock ): proc.impl._terminal = popen_mock # We set __cli_timeout_supported__ to True just to test. This would be an attribute set # at the class level for Salt CLI's that support the timeout flag, like for example, salt-run proc.__cli_timeout_supported__ = True ret = proc.run(*args, **kwargs) assert proc.impl._terminal_timeout == cli_timeout + 10 assert popen_mock.call_args[0][0] == expected # pylint: disable=unsubscriptable-object def test_cli_timeout_updates_to_timeout_kw_plus_10( minion_id, config_dir, config_file, cli_script_name ): # _timeout is passed, the value of --timeout is _timeout, internal timeout is added 10 seconds timeout = 10 explicit_timeout = 60 config = {"conf_file": config_file, "id": "the-id"} args = ["test.ping"] kwargs = {"minion_tgt": minion_id, "_timeout": explicit_timeout} expected = [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--timeout={}".format(explicit_timeout), "--out=json", "--out-indent=0", "--log-level=critical", minion_id, "test.ping", ] popen_mock = mock.MagicMock() popen_mock.pid = os.getpid() popen_mock.poll = mock.MagicMock(side_effect=[None, None, None, None, True]) popen_mock.terminate = mock.MagicMock(return_value=ProcessResult(0, "", "", cmdline=())) terminate_mock = mock.MagicMock(return_value=ProcessResult(0, "", "")) proc = SaltCli(script_name=cli_script_name, config=config, timeout=timeout) with mock.patch.object(proc.impl, "init_terminal", popen_mock), mock.patch.object( proc, "terminate", terminate_mock ): proc.impl._terminal = popen_mock # We set __cli_timeout_supported__ to True just to test. This would be an attribute set # at the class level for Salt CLI's that support the timeout flag, like for example, salt-run proc.__cli_timeout_supported__ = True ret = proc.run(*args, **kwargs) assert proc.impl._terminal_timeout == explicit_timeout + 10 assert popen_mock.call_args[0][0] == expected # pylint: disable=unsubscriptable-object def test_cli_timeout_updates_to_default_timeout_plus_10( minion_id, config_dir, config_file, cli_script_name ): # Neither _timeout nor --timeout are passed, --timeout equals the default timeout, internal timeout is added 10 timeout = 10 config = {"conf_file": config_file, "id": "the-id"} args = ["test.ping"] kwargs = {"minion_tgt": minion_id} expected = [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--timeout={}".format(timeout), "--out=json", "--out-indent=0", "--log-level=critical", minion_id, "test.ping", ] popen_mock = mock.MagicMock() popen_mock.pid = os.getpid() popen_mock.poll = mock.MagicMock(side_effect=[None, None, None, None, True]) popen_mock.terminate = mock.MagicMock(return_value=ProcessResult(0, "", "", cmdline=())) terminate_mock = mock.MagicMock(return_value=ProcessResult(0, "", "")) proc = SaltCli(script_name=cli_script_name, config=config, timeout=timeout) with mock.patch.object(proc.impl, "init_terminal", popen_mock), mock.patch.object( proc, "terminate", terminate_mock ): proc.impl._terminal = popen_mock # We set __cli_timeout_supported__ to True just to test. This would be an attribute set # at the class level for Salt CLI's that support the timeout flag, like for example, salt-run proc.__cli_timeout_supported__ = True ret = proc.run(*args, **kwargs) assert proc.impl._terminal_timeout == timeout + 10 assert popen_mock.call_args[0][0] == expected # pylint: disable=unsubscriptable-object @pytest.mark.parametrize("flag", ["-t", "--timeout", "--timeout="]) def test_override_timeout(minion_id, config_dir, config_file, cli_script_name, flag): flag_value = 15 if flag.endswith("="): flag_overrides_args = [flag + str(flag_value)] else: flag_overrides_args = [flag, str(flag_value)] timeout = 10 config = {"conf_file": config_file, "id": "the-id"} args = flag_overrides_args + ["test.ping"] kwargs = {"minion_tgt": minion_id} expected = ( [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--out=json", "--out-indent=0", "--log-level=critical", minion_id, ] + flag_overrides_args + ["test.ping"] ) proc = SaltCli(script_name=cli_script_name, config=config, timeout=timeout) # We set __cli_timeout_supported__ to True just to test. This would be an attribute set # at the class level for Salt CLI's that support the timeout flag, like for example, salt-run proc.__cli_timeout_supported__ = True # We set the _terminal_timeout attribute just to test. This attribute would be set when calling # SaltScriptBase.run() but we don't really want to call it proc.impl._terminal_timeout = flag_value cmdline = proc.cmdline(*args, **kwargs) assert cmdline == expected # Let's also confirm that we also parsed the timeout flag value and set the SaltScriptBase # _terminal_timeout to that value plus 10 assert proc.impl._terminal_timeout == flag_value + 10 @pytest.mark.parametrize("flag", ["-t", "--timeout", "--timeout="]) def test_override_timeout_bad_value(minion_id, config_dir, config_file, cli_script_name, flag): flag_value = 15 if flag.endswith("="): flag_overrides_args = [flag + str(flag_value) + "i"] else: flag_overrides_args = [flag, str(flag_value) + "i"] timeout = 10 config = {"conf_file": config_file, "id": "the-id"} args = flag_overrides_args + ["test.ping"] kwargs = {"minion_tgt": minion_id} expected = ( [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--out=json", "--out-indent=0", "--log-level=critical", minion_id, ] + flag_overrides_args + ["test.ping"] ) proc = SaltCli(script_name=cli_script_name, config=config, timeout=timeout) # We set __cli_timeout_supported__ to True just to test. This would be an attribute set # at the class level for Salt CLI's that support the timeout flag, like for example, salt-run proc.__cli_timeout_supported__ = True # We set the _terminal_timeout attribute just to test. This attribute would be set when calling # SaltScriptBase.run() but we don't really want to call it proc.impl._terminal_timeout = timeout cmdline = proc.cmdline(*args, **kwargs) assert cmdline == expected # Let's confirm that even though we tried to parse the timeout flag value, it was a bad value and the # SaltScriptBase _terminal_timeout attribute was not update assert proc.impl._terminal_timeout == timeout @pytest.mark.parametrize("flag", ["-c", "--config-dir", "--config-dir=", None]) def test_override_config_dir(minion_id, config_dir, config_file, cli_script_name, flag): passed_config_dir = "{}.new".format(config_dir) if flag is None: flag_overrides_args = ["--config-dir={}".format(config_dir)] elif flag.endswith("="): flag_overrides_args = [flag + passed_config_dir] else: flag_overrides_args = [flag, passed_config_dir] config = {"conf_file": config_file, "id": "the-id"} args = flag_overrides_args + ["test.ping"] kwargs = {"minion_tgt": minion_id} expected = ( [ sys.executable, cli_script_name, "--out=json", "--out-indent=0", "--log-level=critical", minion_id, ] + flag_overrides_args + ["test.ping"] ) proc = SaltCli(script_name=cli_script_name, config=config) cmdline = proc.cmdline(*args, **kwargs) assert cmdline == expected def test_process_output(cli_script_name, config_file): in_stdout = '"The salt master could not be contacted. Is master running?"\n' in_stderr = "" cmdline = ["--out=json"] config = {"conf_file": config_file, "id": "the-id"} proc = SaltCli(script_name=cli_script_name, config=config) # Call proc.cmdline() so that proc.__json_output__ is properly set proc.cmdline() stdout, stderr, json_out = proc.process_output(in_stdout, in_stderr, cmdline=cmdline) assert stdout == json.loads(in_stdout) assert stderr == in_stderr assert json_out is None def test_non_string_cli_flags(minion_id, config_dir, config_file, cli_script_name): config = {"conf_file": config_file, "id": "the-id"} args = ["test.ping"] foo = ["the", "foo", "list"] kwargs = {"minion_tgt": minion_id, "foo": foo} expected = [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--out=json", "--out-indent=0", "--log-level=critical", minion_id, "test.ping", "foo={}".format(json.dumps(foo)), ] proc = SaltCli(script_name=cli_script_name, config=config) cmdline = proc.cmdline(*args, **kwargs) assert cmdline == expected def test_jsonify_kwargs(minion_id, config_dir, config_file, cli_script_name): config = {"conf_file": config_file, "id": "the-id"} args = ["test.ping"] # Strings extra_kwargs = OrderedDict((("look", "Ma"), ("no", "Hands!"))) kwargs = OrderedDict((("minion_tgt", minion_id),)) kwargs.update(extra_kwargs) expected = [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--out=json", "--out-indent=0", "--log-level=critical", minion_id, "test.ping", ] for key, value in extra_kwargs.items(): expected.append("{}={}".format(key, value)) proc = SaltCli(script_name=cli_script_name, config=config) cmdline = proc.cmdline(*args, **kwargs) # Function **kwargs are not ordered dictionaries on some python versions # let's just use sorted to make sure everything is in the output assert sorted(cmdline) == sorted(expected) # Numbers extra_kwargs = OrderedDict((("width", 1.27), ("height", 3))) kwargs = OrderedDict((("minion_tgt", minion_id),)) kwargs.update(extra_kwargs) expected = [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--out=json", "--out-indent=0", "--log-level=critical", minion_id, "test.ping", ] for key, value in extra_kwargs.items(): value = json.dumps(value) expected.append("{}={}".format(key, value)) proc = SaltCli(script_name=cli_script_name, config=config) cmdline = proc.cmdline(*args, **kwargs) # Function **kwargs are not ordered dictionaries on some python versions # let's just use sorted to make sure everything is in the output assert sorted(cmdline) == sorted(expected) # Booleans extra_kwargs = OrderedDict((("short", False), ("tall", True))) kwargs = OrderedDict((("minion_tgt", minion_id),)) kwargs.update(extra_kwargs) expected = [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--out=json", "--out-indent=0", "--log-level=critical", minion_id, "test.ping", ] for key, value in extra_kwargs.items(): value = json.dumps(value) expected.append("{}={}".format(key, value)) proc = SaltCli(script_name=cli_script_name, config=config) cmdline = proc.cmdline(*args, **kwargs) # Function **kwargs are not ordered dictionaries on some python versions # let's just use sorted to make sure everything is in the output assert sorted(cmdline) == sorted(expected) # JSon structure extra_kwargs = {"look": "Ma", "no": "Hands!"} kwargs = {"minion_tgt": minion_id, "extra": extra_kwargs} expected = [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--out=json", "--out-indent=0", "--log-level=critical", minion_id, "test.ping", "extra={}".format(json.dumps(extra_kwargs)), ] proc = SaltCli(script_name=cli_script_name, config=config) cmdline = proc.cmdline(*args, **kwargs) # Function **kwargs are not ordered dictionaries on some python versions # let's just use sorted to make sure everything is in the output assert sorted(cmdline) == sorted(expected) def test_salt_cli_factory_id_attr_comes_first_in_repr(config_file): proc = SaltCli(script_name="foo-bar", config={"id": "TheID", "conf_file": config_file}) regex = r"{}(id='TheID'".format(proc.__class__.__name__) assert repr(proc).startswith(regex) assert str(proc).startswith(regex) def test_salt_cli_display_name(config_file): factory_id = "TheID" proc = SaltCli(script_name="foo-bar", config={"id": factory_id, "conf_file": config_file}) assert proc.get_display_name() == "{}(id={!r})".format(SaltCli.__name__, factory_id) pytest-salt-factories-0.907.0/tests/unit/factories/base/test_salt_daemon_factory.py0000644000175000017500000000606514036250542031405 0ustar vampasvampas00000000000000import shutil import sys import pytest from saltfactories.bases import SaltDaemon @pytest.fixture def config_dir(pytester): _conf_dir = pytester.mkdir("conf") try: yield _conf_dir finally: shutil.rmtree(str(_conf_dir), ignore_errors=True) @pytest.fixture def master_id(): return "test-master-id" @pytest.fixture def config_file(config_dir, master_id): config_file = str(config_dir / "config") with open(config_file, "w") as wfh: wfh.write("id: {}\n".format(master_id)) return config_file @pytest.fixture def cli_script_name(pytester): py_file = pytester.makepyfile( """ print("This would be the CLI script") """ ) try: yield str(py_file) finally: py_file.unlink() def test_default_cli_flags(config_dir, config_file, cli_script_name): config = {"conf_file": config_file, "id": "the-id"} expected = [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), "--log-level=critical", ] proc = SaltDaemon(start_timeout=1, script_name=cli_script_name, config=config) cmdline = proc.cmdline() assert cmdline == expected @pytest.mark.parametrize("flag", ["-l", "--log-level", "--log-level="]) def test_override_log_level(config_dir, config_file, cli_script_name, flag): config = {"conf_file": config_file, "id": "the-id"} if flag.endswith("="): args = [flag + "info"] else: args = [flag, "info"] expected = [ sys.executable, cli_script_name, "--config-dir={}".format(config_dir), ] + args proc = SaltDaemon(start_timeout=1, script_name=cli_script_name, config=config) cmdline = proc.cmdline(*args) assert cmdline == expected @pytest.mark.parametrize("flag", ["-c", "--config-dir", "--config-dir=", None]) def test_override_config_dir(config_dir, config_file, cli_script_name, flag): passed_config_dir = "{}.new".format(config_dir) if flag is None: args = ["--config-dir={}".format(config_dir)] elif flag.endswith("="): args = [flag + passed_config_dir] else: args = [flag, passed_config_dir] config = {"conf_file": config_file, "id": "the-id"} expected = [sys.executable, cli_script_name, "--log-level=critical"] + args proc = SaltDaemon(start_timeout=1, script_name=cli_script_name, config=config) cmdline = proc.cmdline(*args) assert cmdline == expected def test_salt_daemon_factory_id_attr_comes_first_in_repr(config_file): proc = SaltDaemon( start_timeout=1, script_name="foo-bar", config={"id": "TheID", "conf_file": config_file} ) regex = r"{}(id='TheID'".format(proc.__class__.__name__) assert repr(proc).startswith(regex) assert str(proc).startswith(regex) def test_salt_cli_display_name(config_file): factory_id = "TheID" proc = SaltDaemon( start_timeout=1, script_name="foo-bar", config={"id": factory_id, "conf_file": config_file}, ) assert proc.get_display_name() == "{}(id={!r})".format(SaltDaemon.__name__, factory_id) pytest-salt-factories-0.907.0/tests/unit/factories/cli/0000755000175000017500000000000014106144336023606 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/factories/cli/__init__.py0000644000175000017500000000000013757132736025721 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/factories/cli/test_salt.py0000644000175000017500000000374214036250542026167 0ustar vampasvampas00000000000000""" tests.unit.factories.cli.test_salt ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the ``salt`` CLI functionality """ import shutil import pytest from saltfactories.cli.salt import Salt @pytest.fixture def config_dir(pytester): _conf_dir = pytester.mkdir("conf") try: yield _conf_dir finally: shutil.rmtree(str(_conf_dir), ignore_errors=True) @pytest.fixture def minion_id(): return "test-minion-id" @pytest.fixture def config_file(config_dir, minion_id): config_file = str(config_dir / "config") with open(config_file, "w") as wfh: wfh.write("id: {}\n".format(minion_id)) return config_file @pytest.fixture def cli_script_name(pytester): py_file = pytester.makepyfile( """ print("This would be the CLI script") """ ) try: yield str(py_file) finally: py_file.unlink() def test_missing_minion_id_raises_exception(minion_id, config_dir, config_file, cli_script_name): config = {"conf_file": config_file, "id": "the-id"} args = ["test.ping"] proc = Salt(script_name=cli_script_name, config=config) with pytest.raises(pytest.UsageError) as exc: proc.cmdline(*args) assert ( str(exc.value) == "The `minion_tgt` keyword argument is mandatory for the salt CLI factory" ) @pytest.mark.parametrize("flag", ["-V", "--version", "--versions-report", "--help"]) def test_missing_minion_id_does_not_raise_exception( minion_id, config_dir, config_file, cli_script_name, flag ): """ Assert that certain flag, which just output something and then exit, don't raise an exception """ config = {"conf_file": config_file, "id": "the-id"} args = ["test.ping"] + [flag] proc = Salt(script_name=cli_script_name, config=config) try: proc.cmdline(*args) except RuntimeError: pytest.fail( "The Salt class raised RuntimeError when the CLI flag '{}' was present in args".format( flag ) ) pytest-salt-factories-0.907.0/tests/unit/factories/daemons/0000755000175000017500000000000014106144336024465 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/factories/daemons/__init__.py0000644000175000017500000000000013723372562026574 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/factories/daemons/test_api.py0000644000175000017500000000210414036250542026643 0ustar vampasvampas00000000000000import pytest from saltfactories.daemons.api import SaltApi from saltfactories.utils import random_string def test_missing_api_config(salt_factories): master = salt_factories.salt_master_daemon(random_string("master-")) with pytest.raises(pytest.UsageError) as exc: master.salt_api_daemon() assert str(exc.value) == ( "The salt-master configuration for this salt-api instance does not seem to have " "any api properly configured." ) def test_configure_raises_exception(salt_factories): with pytest.raises(pytest.UsageError) as exc: SaltApi.configure(salt_factories, "api") assert str(exc.value) == ( "The salt-api daemon is not configurable. It uses the salt-master config that " "it's attached to." ) def test_load_config_raises_exception(): with pytest.raises(pytest.UsageError) as exc: SaltApi.load_config("config_file", {}) assert str(exc.value) == ( "The salt-api daemon does not have it's own config file. It uses the salt-master config that " "it's attached to." ) pytest-salt-factories-0.907.0/tests/unit/factories/daemons/test_container.py0000644000175000017500000000167414042445237030073 0ustar vampasvampas00000000000000from unittest import mock import pytest from saltfactories.daemons.container import Container def test_missing_docker_library(): with mock.patch( "saltfactories.daemons.container.HAS_DOCKER", new_callable=mock.PropertyMock(return_value=False), ): with pytest.raises(RuntimeError) as exc: Container(name="foo", image="bar") assert str(exc.value) == "The docker python library was not found installed" def test_missing_requests_library(): with mock.patch( "saltfactories.daemons.container.HAS_DOCKER", new_callable=mock.PropertyMock(return_value=True), ), mock.patch( "saltfactories.daemons.container.HAS_REQUESTS", new_callable=mock.PropertyMock(return_value=False), ): with pytest.raises(RuntimeError) as exc: Container(name="foo", image="bar") assert str(exc.value) == "The requests python library was not found installed" pytest-salt-factories-0.907.0/tests/unit/test_exceptions.py0000644000175000017500000001412313723372562024663 0ustar vampasvampas00000000000000""" tests.unit.test_exceptions ~~~~~~~~~~~~~~~~~~~~~~~~~~ Exceptions unit tests """ import textwrap import traceback import pytest import saltfactories.exceptions def test_process_failed_message(): message = "The message" with pytest.raises(saltfactories.exceptions.FactoryFailure) as exc: raise saltfactories.exceptions.FactoryFailure(message) assert str(exc.value) == message def test_process_failed_cmdline(): message = "The message" cmdline = ["python", "--version"] expected = textwrap.dedent( """\ {} Command Line: {!r} """.format( message, cmdline ) ) with pytest.raises(saltfactories.exceptions.FactoryFailure) as exc: raise saltfactories.exceptions.FactoryFailure(message, cmdline=cmdline) output = str(exc.value) assert output == expected def test_process_failed_exitcode(): message = "The message" exitcode = 1 expected = textwrap.dedent( """\ {} Exitcode: {} """.format( message, exitcode ) ) with pytest.raises(saltfactories.exceptions.FactoryFailure) as exc: raise saltfactories.exceptions.FactoryFailure(message, exitcode=exitcode) output = str(exc.value) assert output == expected def test_process_failed_stdout(): message = "The message" stdout = "This is the STDOUT" expected = textwrap.dedent( """\ {} Process Output: >>>>> STDOUT >>>>> {} <<<<< STDOUT <<<<< """.format( message, stdout ) ) with pytest.raises(saltfactories.exceptions.FactoryFailure) as exc: raise saltfactories.exceptions.FactoryFailure(message, stdout=stdout) output = str(exc.value) assert output == expected def test_process_failed_stderr(): message = "The message" stderr = "This is the STDERR" expected = textwrap.dedent( """\ {} Process Output: >>>>> STDERR >>>>> {} <<<<< STDERR <<<<< """.format( message, stderr ) ) with pytest.raises(saltfactories.exceptions.FactoryFailure) as exc: raise saltfactories.exceptions.FactoryFailure(message, stderr=stderr) output = str(exc.value) assert output == expected def test_process_failed_stdout_and_stderr(): message = "The message" stdout = "This is the STDOUT" stderr = "This is the STDERR" expected = textwrap.dedent( """\ {} Process Output: >>>>> STDOUT >>>>> {} <<<<< STDOUT <<<<< >>>>> STDERR >>>>> {} <<<<< STDERR <<<<< """.format( message, stdout, stderr ) ) with pytest.raises(saltfactories.exceptions.FactoryFailure) as exc: raise saltfactories.exceptions.FactoryFailure(message, stdout=stdout, stderr=stderr) output = str(exc.value) assert output == expected def test_process_failed_cmdline_stdout_and_stderr(): message = "The message" stdout = "This is the STDOUT" stderr = "This is the STDERR" cmdline = ["python", "--version"] expected = textwrap.dedent( """\ {} Command Line: {!r} Process Output: >>>>> STDOUT >>>>> {} <<<<< STDOUT <<<<< >>>>> STDERR >>>>> {} <<<<< STDERR <<<<< """.format( message, cmdline, stdout, stderr ) ) with pytest.raises(saltfactories.exceptions.FactoryFailure) as exc: raise saltfactories.exceptions.FactoryFailure( message, cmdline=cmdline, stdout=stdout, stderr=stderr ) output = str(exc.value) assert output == expected def test_process_failed_cmdline_stdout_stderr_and_exitcode(): message = "The message" stdout = "This is the STDOUT" stderr = "This is the STDERR" cmdline = ["python", "--version"] exitcode = 1 expected = textwrap.dedent( """\ {} Command Line: {!r} Exitcode: {} Process Output: >>>>> STDOUT >>>>> {} <<<<< STDOUT <<<<< >>>>> STDERR >>>>> {} <<<<< STDERR <<<<< """.format( message, cmdline, exitcode, stdout, stderr ) ) with pytest.raises(saltfactories.exceptions.FactoryFailure) as exc: raise saltfactories.exceptions.FactoryFailure( message, cmdline=cmdline, stdout=stdout, stderr=stderr, exitcode=exitcode ) output = str(exc.value) assert output == expected def test_process_failed_exc(): with pytest.raises(ZeroDivisionError) as exc: 1 / 0 # pylint: disable=pointless-statement excinfo = exc._excinfo formatted_exception = "".join(traceback.format_exception(*excinfo)).rstrip() message = "The message" expected = "{}\n{}\n".format(message, formatted_exception) with pytest.raises(saltfactories.exceptions.FactoryFailure) as exc: raise saltfactories.exceptions.FactoryFailure(message, exc=excinfo) output = str(exc.value) assert output == expected def test_process_failed_cmdline_stdout_stderr_and_exc(): with pytest.raises(ZeroDivisionError) as exc: 1 / 0 # pylint: disable=pointless-statement excinfo = exc._excinfo formatted_exception = "".join(traceback.format_exception(*excinfo)).rstrip() message = "The message" stdout = "This is the STDOUT" stderr = "This is the STDERR" cmdline = ["python", "--version"] expected = textwrap.dedent( """\ {} Command Line: {!r} Process Output: >>>>> STDOUT >>>>> {} <<<<< STDOUT <<<<< >>>>> STDERR >>>>> {} <<<<< STDERR <<<<< """.format( message, cmdline, stdout, stderr ) ) expected += formatted_exception + "\n" with pytest.raises(saltfactories.exceptions.FactoryFailure) as exc: raise saltfactories.exceptions.FactoryFailure( message, cmdline=cmdline, stdout=stdout, stderr=stderr, exc=excinfo ) output = str(exc.value) assert output == expected pytest-salt-factories-0.907.0/tests/unit/utils/0000755000175000017500000000000014106144336022220 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/utils/__init__.py0000644000175000017500000000000013720712732024321 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/utils/markers/0000755000175000017500000000000014106144336023664 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/utils/markers/__init__.py0000644000175000017500000000000013720712732025765 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/utils/markers/test_skip_if_binaries_missing.py0000644000175000017500000000444714042445237032342 0ustar vampasvampas00000000000000""" tests.unit.utils.markers.test_skip_if_binaries_missing ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the "skip_if_binaries_missing" marker helper """ import os import sys import pytest import saltfactories.utils.markers as markers @pytest.fixture def python_binary(): return os.path.basename(sys.executable) def test_single_existing(python_binary): assert markers.skip_if_binaries_missing([python_binary]) is None def test_multiple_existing(python_binary): assert markers.skip_if_binaries_missing([python_binary, "pip"]) is None def test_single_non_existing_with_message(): reason = markers.skip_if_binaries_missing(["python9"], reason="Dam!") assert reason is not None assert reason == "Dam!" def test_multiple_one_missing(python_binary): reason = markers.skip_if_binaries_missing([python_binary, "pip9"]) assert reason is not None assert reason == "The 'pip9' binary was not found" def test_multiple_all_missing(): reason = markers.skip_if_binaries_missing(["python9", "pip9"]) assert reason is not None assert reason == "The 'python9' binary was not found" def test_multiple_one_missing_check_all_false(python_binary): reason = markers.skip_if_binaries_missing([python_binary, "pip9"], check_all=False) # We should get no message back because the python binary is found assert reason is None, reason reason = markers.skip_if_binaries_missing(["python9", "pip"], check_all=False) # We should get no message back because the pip binary is found assert reason is None, reason def test_multiple_one_missing_check_all_false_with_message(python_binary): reason = markers.skip_if_binaries_missing( [python_binary, "pip9"], reason="Dam!", check_all=False ) # We should get no message back because the python binary is found assert reason is None def test_multiple_missing_check_all_false(): reason = markers.skip_if_binaries_missing(["python9", "pip9"], check_all=False) assert reason is not None assert reason == "None of the following binaries was found: python9, pip9" def test_multiple_missing_check_all_false_with_message(): reason = markers.skip_if_binaries_missing(["python9", "pip9"], reason="Dam!", check_all=False) assert reason is not None assert reason == "Dam!" pytest-salt-factories-0.907.0/tests/unit/utils/markers/test_skip_if_no_local_network.py0000644000175000017500000000172413763635374032363 0ustar vampasvampas00000000000000""" tests.unit.utils.markers.test_skip_if_no_local_network ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the "skip_if_no_local_network" marker helper """ from unittest import mock import saltfactories.utils.markers as markers from saltfactories.utils import ports from saltfactories.utils import socket def test_has_local_network(): assert markers.skip_if_no_local_network() is None def test_no_local_network(): mock_socket = mock.MagicMock() mock_socket.bind = mock.MagicMock(side_effect=socket.error) with mock.patch( "saltfactories.utils.ports.get_unused_localhost_port", side_effect=[ports.get_unused_localhost_port() for n in range(10)], ): with mock.patch("saltfactories.utils.markers.socket.socket", return_value=mock_socket): skip_reason = markers.skip_if_no_local_network() assert skip_reason is not None assert skip_reason == "No local network was detected" pytest-salt-factories-0.907.0/tests/unit/utils/markers/test_skip_if_no_remote_network.py0000644000175000017500000000141713763635325032557 0ustar vampasvampas00000000000000""" tests.unit.utils.markers.test_skip_if_no_remote_network ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the "skip_if_no_remote_network" marker helper """ from unittest import mock import saltfactories.utils.markers as markers from saltfactories.utils import socket def test_has_remote_network(): assert markers.skip_if_no_remote_network() is None def test_no_remote_network(): mock_socket = mock.MagicMock() mock_socket.connect = mock.MagicMock(side_effect=socket.error) with mock.patch("saltfactories.utils.markers.socket.socket", return_value=mock_socket): skip_reason = markers.skip_if_no_remote_network() assert skip_reason is not None assert skip_reason == "No internet network connection was detected" pytest-salt-factories-0.907.0/tests/unit/utils/markers/test_skip_if_not_root.py0000644000175000017500000000167313720712732030655 0ustar vampasvampas00000000000000""" tests.unit.utils.markers.test_skip_if_not_root ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test the "skip_if_not_root" marker helper """ import sys from unittest import mock import saltfactories.utils.markers def test_when_root(): if sys.platform.startswith("win"): with mock.patch("salt.utils.win_functions.is_admin", return_value=True): assert saltfactories.utils.markers.skip_if_not_root() is None else: with mock.patch("os.getuid", return_value=0): assert saltfactories.utils.markers.skip_if_not_root() is None def test_when_not_root(): if sys.platform.startswith("win"): with mock.patch("salt.utils.win_functions.is_admin", return_value=False): assert saltfactories.utils.markers.skip_if_not_root() is not None else: with mock.patch("os.getuid", return_value=1): assert saltfactories.utils.markers.skip_if_not_root() is not None pytest-salt-factories-0.907.0/tests/unit/utils/processes/0000755000175000017500000000000014106144336024226 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/utils/processes/__init__.py0000644000175000017500000000000013720712732026327 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/utils/processes/test_processresult.py0000644000175000017500000000370313723372562030567 0ustar vampasvampas00000000000000""" tests.unit.utils.processes.test_processresult ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test saltfactories.utils.processes.ProcessResult """ import textwrap import pytest from saltfactories.utils.processes import ProcessResult @pytest.mark.parametrize("exitcode", [None, 1.0, -1.0, "0"]) def test_non_int_exitcode_raises_exception(exitcode): with pytest.raises(ValueError): ProcessResult(exitcode, None, None) def test_attributes(): exitcode = 0 stdout = "STDOUT" stderr = "STDERR" cmdline = None ret = ProcessResult(exitcode, stdout, stderr) assert ret.exitcode == exitcode assert ret.stdout == stdout assert ret.stderr == stderr assert ret.cmdline == cmdline cmdline = [1, 2, 3] ret = ProcessResult(exitcode, stdout, stderr, cmdline=cmdline) assert ret.exitcode == exitcode assert ret.stdout == stdout assert ret.stderr == stderr assert ret.cmdline == cmdline def test_str_formatting(): exitcode = 0 stdout = "STDOUT" stderr = "STDERR" cmdline = None ret = ProcessResult(exitcode, stdout, stderr) expected = textwrap.dedent( """\ ProcessResult Exitcode: {} Process Output: >>>>> STDOUT >>>>> {} <<<<< STDOUT <<<<< >>>>> STDERR >>>>> {} <<<<< STDERR <<<<< """.format( exitcode, stdout, stderr ) ) assert str(ret) == expected cmdline = [1, 2, 3] ret = ProcessResult(exitcode, stdout, stderr, cmdline=cmdline) expected = textwrap.dedent( """\ ProcessResult Command Line: {!r} Exitcode: {} Process Output: >>>>> STDOUT >>>>> {} <<<<< STDOUT <<<<< >>>>> STDERR >>>>> {} <<<<< STDERR <<<<< """.format( cmdline, exitcode, stdout, stderr, ) ) assert str(ret) == expected pytest-salt-factories-0.907.0/tests/unit/utils/processes/test_shellresult.py0000644000175000017500000000562413723372562030224 0ustar vampasvampas00000000000000""" tests.unit.utils.processes.test_shellresult ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test saltfactories.utils.processes.ShellResult """ import pprint import textwrap import pytest from saltfactories.utils.processes import ShellResult @pytest.mark.parametrize("exitcode", [None, 1.0, -1.0, "0"]) def test_non_int_exitcode_raises_exception(exitcode): with pytest.raises(ValueError): ShellResult(exitcode, None, None) def test_attributes(): exitcode = 0 stdout = "STDOUT" stderr = "STDERR" json = None cmdline = None ret = ShellResult(exitcode, stdout, stderr) assert ret.exitcode == exitcode assert ret.stdout == stdout assert ret.stderr == stderr assert ret.json == json assert ret.cmdline == cmdline json = {1: 1} ret = ShellResult(exitcode, stdout, stderr, json=json) assert ret.exitcode == exitcode assert ret.stdout == stdout assert ret.stderr == stderr assert ret.json == json assert ret.cmdline == cmdline cmdline = [1, 2, 3] ret = ShellResult(exitcode, stdout, stderr, json=json, cmdline=cmdline) assert ret.exitcode == exitcode assert ret.stdout == stdout assert ret.stderr == stderr assert ret.json == json assert ret.cmdline == cmdline def test_str_formatting(): exitcode = 0 stdout = "STDOUT" stderr = "STDERR" json = None cmdline = None ret = ShellResult(exitcode, stdout, stderr) expected = textwrap.dedent( """\ ShellResult Exitcode: {} Process Output: >>>>> STDOUT >>>>> {} <<<<< STDOUT <<<<< >>>>> STDERR >>>>> {} <<<<< STDERR <<<<< """.format( exitcode, stdout, stderr ) ) assert str(ret) == expected json = {1: 1} ret = ShellResult(exitcode, stdout, stderr, json=json) expected = textwrap.dedent( """\ ShellResult Exitcode: {} Process Output: >>>>> STDOUT >>>>> {} <<<<< STDOUT <<<<< >>>>> STDERR >>>>> {} <<<<< STDERR <<<<< JSON Object: {} """.format( exitcode, stdout, stderr, "".join(" {}".format(line) for line in pprint.pformat(json)) ) ) assert str(ret) == expected cmdline = [1, 2, 3] ret = ShellResult(exitcode, stdout, stderr, json=json, cmdline=cmdline) expected = textwrap.dedent( """\ ShellResult Command Line: {!r} Exitcode: {} Process Output: >>>>> STDOUT >>>>> {} <<<<< STDOUT <<<<< >>>>> STDERR >>>>> {} <<<<< STDERR <<<<< JSON Object: {} """.format( cmdline, exitcode, stdout, stderr, "".join(" {}".format(line) for line in pprint.pformat(json)), ) ) assert str(ret) == expected pytest-salt-factories-0.907.0/tests/unit/utils/saltext/0000755000175000017500000000000014106144336023704 5ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/utils/saltext/__init__.py0000644000175000017500000000000014062653620026005 0ustar vampasvampas00000000000000pytest-salt-factories-0.907.0/tests/unit/utils/saltext/test_log_handlers.py0000644000175000017500000000123114062653621027756 0ustar vampasvampas00000000000000import pytest from saltfactories.utils.saltext.log_handlers.pytest_log_handler import ZMQHandler def test_zmqhandler_immutable_formatter_attribute(subtests): handler = ZMQHandler() formatter = handler.formatter with subtests.test("ZMQHandler.setFormatter()"): with pytest.raises(RuntimeError): handler.setFormatter("foo") with subtests.test("ZMQHandler.formatter = ..."): with pytest.raises(RuntimeError): handler.formatter = "foo" with subtests.test("del ZMQHandler.formatter"): with pytest.raises(RuntimeError): del handler.formatter assert handler.formatter is formatter pytest-salt-factories-0.907.0/tests/unit/utils/test_cli_scripts.py0000644000175000017500000005050514061662631026157 0ustar vampasvampas00000000000000""" tests.utils.test_cli_scripts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test saltfactories.utils.cli_scripts """ import os import pathlib import textwrap import pytest import saltfactories.utils.cli_scripts as cli_scripts def test_generate_script_defaults(tmpdir): """ Test defaults script generation """ script_path = cli_scripts.generate_script(tmpdir.strpath, "salt-foobar") with open(script_path) as rfh: contents = rfh.read() expected = textwrap.dedent( """\ from __future__ import absolute_import import os import sys # We really do not want buffered output os.environ[str("PYTHONUNBUFFERED")] = str("1") # Don't write .pyc files or create them in __pycache__ directories os.environ[str("PYTHONDONTWRITEBYTECODE")] = str("1") import atexit import traceback from salt.scripts import salt_foobar def main(): if sys.platform.startswith("win"): import os.path import py_compile cfile = os.path.splitext(__file__)[0] + '.pyc' if not os.path.exists(cfile): py_compile.compile(__file__, cfile) salt_foobar() if __name__ == '__main__': exitcode = 0 try: main() except SystemExit as exc: exitcode = exc.code # https://docs.python.org/3/library/exceptions.html#SystemExit if exitcode is None: exitcode = 0 if not isinstance(exitcode, int): # A string?! sys.stderr.write(exitcode) exitcode = 1 except Exception as exc: sys.stderr.write( "An un-handled exception was caught: " + str(exc) + "\\n" + traceback.format_exc() ) exitcode = 1 sys.stdout.flush() sys.stderr.flush() atexit._run_exitfuncs() os._exit(exitcode) """ ) assert contents == expected def test_generate_script_code_dir(tmpdir): """ Test code_dir inclusion in script generation """ code_dir = tmpdir.mkdir("code-dir").strpath script_path = cli_scripts.generate_script(tmpdir.strpath, "salt-foobar", code_dir=code_dir) with open(script_path) as rfh: contents = rfh.read() expected = textwrap.dedent( """\ from __future__ import absolute_import import os import sys # We really do not want buffered output os.environ[str("PYTHONUNBUFFERED")] = str("1") # Don't write .pyc files or create them in __pycache__ directories os.environ[str("PYTHONDONTWRITEBYTECODE")] = str("1") CODE_DIR = r'{}' if CODE_DIR in sys.path: sys.path.remove(CODE_DIR) sys.path.insert(0, CODE_DIR) import atexit import traceback from salt.scripts import salt_foobar def main(): if sys.platform.startswith("win"): import os.path import py_compile cfile = os.path.splitext(__file__)[0] + '.pyc' if not os.path.exists(cfile): py_compile.compile(__file__, cfile) salt_foobar() if __name__ == '__main__': exitcode = 0 try: main() except SystemExit as exc: exitcode = exc.code # https://docs.python.org/3/library/exceptions.html#SystemExit if exitcode is None: exitcode = 0 if not isinstance(exitcode, int): # A string?! sys.stderr.write(exitcode) exitcode = 1 except Exception as exc: sys.stderr.write( "An un-handled exception was caught: " + str(exc) + "\\n" + traceback.format_exc() ) exitcode = 1 sys.stdout.flush() sys.stderr.flush() atexit._run_exitfuncs() os._exit(exitcode) """.format( code_dir ) ) assert contents == expected def test_generate_script_inject_coverage(tmpdir): """ Test coverage related code included in script generation """ # If code_dir is not passed, assert that we fail with pytest.raises(pytest.UsageError): cli_scripts.generate_script(tmpdir.strpath, "salt-foobar-fail", inject_coverage=True) code_dir = tmpdir.mkdir("code-dir").strpath script_path = cli_scripts.generate_script( tmpdir.strpath, "salt-foobar", code_dir=code_dir, inject_coverage=True ) with open(script_path) as rfh: contents = rfh.read() expected = textwrap.dedent( """\ from __future__ import absolute_import import os import sys # We really do not want buffered output os.environ[str("PYTHONUNBUFFERED")] = str("1") # Don't write .pyc files or create them in __pycache__ directories os.environ[str("PYTHONDONTWRITEBYTECODE")] = str("1") CODE_DIR = r'{}' if CODE_DIR in sys.path: sys.path.remove(CODE_DIR) sys.path.insert(0, CODE_DIR) # Setup coverage environment variables COVERAGE_FILE = os.path.join(CODE_DIR, '.coverage') COVERAGE_PROCESS_START = os.path.join(CODE_DIR, '.coveragerc') os.environ[str('COVERAGE_FILE')] = str(COVERAGE_FILE) os.environ[str('COVERAGE_PROCESS_START')] = str(COVERAGE_PROCESS_START) import atexit import traceback from salt.scripts import salt_foobar def main(): if sys.platform.startswith("win"): import os.path import py_compile cfile = os.path.splitext(__file__)[0] + '.pyc' if not os.path.exists(cfile): py_compile.compile(__file__, cfile) salt_foobar() if __name__ == '__main__': exitcode = 0 try: main() except SystemExit as exc: exitcode = exc.code # https://docs.python.org/3/library/exceptions.html#SystemExit if exitcode is None: exitcode = 0 if not isinstance(exitcode, int): # A string?! sys.stderr.write(exitcode) exitcode = 1 except Exception as exc: sys.stderr.write( "An un-handled exception was caught: " + str(exc) + "\\n" + traceback.format_exc() ) exitcode = 1 sys.stdout.flush() sys.stderr.flush() atexit._run_exitfuncs() os._exit(exitcode) """.format( code_dir ) ) assert contents == expected def test_generate_script_inject_sitecustomize(tmpdir): """ Test sitecustomize injection related code included in script generation """ sitecustomize_path = pathlib.Path(cli_scripts.__file__).resolve().parent / "coverage" code_dir = tmpdir.mkdir("code-dir").strpath script_path = cli_scripts.generate_script( tmpdir.strpath, "salt-foobar", code_dir=code_dir, inject_coverage=True, inject_sitecustomize=True, ) with open(script_path) as rfh: contents = rfh.read() expected = textwrap.dedent( """\ from __future__ import absolute_import import os import sys # We really do not want buffered output os.environ[str("PYTHONUNBUFFERED")] = str("1") # Don't write .pyc files or create them in __pycache__ directories os.environ[str("PYTHONDONTWRITEBYTECODE")] = str("1") CODE_DIR = r'{}' if CODE_DIR in sys.path: sys.path.remove(CODE_DIR) sys.path.insert(0, CODE_DIR) # Setup coverage environment variables COVERAGE_FILE = os.path.join(CODE_DIR, '.coverage') COVERAGE_PROCESS_START = os.path.join(CODE_DIR, '.coveragerc') os.environ[str('COVERAGE_FILE')] = str(COVERAGE_FILE) os.environ[str('COVERAGE_PROCESS_START')] = str(COVERAGE_PROCESS_START) # Allow sitecustomize.py to be importable for test coverage purposes SITECUSTOMIZE_DIR = r'{}' PYTHONPATH = os.environ.get('PYTHONPATH') or None if PYTHONPATH is None: PYTHONPATH_ENV_VAR = SITECUSTOMIZE_DIR else: PYTHON_PATH_ENTRIES = PYTHONPATH.split(os.pathsep) if SITECUSTOMIZE_DIR in PYTHON_PATH_ENTRIES: PYTHON_PATH_ENTRIES.remove(SITECUSTOMIZE_DIR) PYTHON_PATH_ENTRIES.insert(0, SITECUSTOMIZE_DIR) PYTHONPATH_ENV_VAR = os.pathsep.join(PYTHON_PATH_ENTRIES) os.environ[str('PYTHONPATH')] = str(PYTHONPATH_ENV_VAR) if SITECUSTOMIZE_DIR in sys.path: sys.path.remove(SITECUSTOMIZE_DIR) sys.path.insert(0, SITECUSTOMIZE_DIR) import atexit import traceback from salt.scripts import salt_foobar def main(): if sys.platform.startswith("win"): import os.path import py_compile cfile = os.path.splitext(__file__)[0] + '.pyc' if not os.path.exists(cfile): py_compile.compile(__file__, cfile) salt_foobar() if __name__ == '__main__': exitcode = 0 try: main() except SystemExit as exc: exitcode = exc.code # https://docs.python.org/3/library/exceptions.html#SystemExit if exitcode is None: exitcode = 0 if not isinstance(exitcode, int): # A string?! sys.stderr.write(exitcode) exitcode = 1 except Exception as exc: sys.stderr.write( "An un-handled exception was caught: " + str(exc) + "\\n" + traceback.format_exc() ) exitcode = 1 sys.stdout.flush() sys.stderr.flush() atexit._run_exitfuncs() os._exit(exitcode) """.format( code_dir, str(sitecustomize_path) ) ) assert contents == expected script_path = cli_scripts.generate_script( tmpdir.strpath, "salt-foobar-2", inject_sitecustomize=True ) with open(script_path) as rfh: contents = rfh.read() expected = textwrap.dedent( """\ from __future__ import absolute_import import os import sys # We really do not want buffered output os.environ[str("PYTHONUNBUFFERED")] = str("1") # Don't write .pyc files or create them in __pycache__ directories os.environ[str("PYTHONDONTWRITEBYTECODE")] = str("1") # Allow sitecustomize.py to be importable for test coverage purposes SITECUSTOMIZE_DIR = r'{}' PYTHONPATH = os.environ.get('PYTHONPATH') or None if PYTHONPATH is None: PYTHONPATH_ENV_VAR = SITECUSTOMIZE_DIR else: PYTHON_PATH_ENTRIES = PYTHONPATH.split(os.pathsep) if SITECUSTOMIZE_DIR in PYTHON_PATH_ENTRIES: PYTHON_PATH_ENTRIES.remove(SITECUSTOMIZE_DIR) PYTHON_PATH_ENTRIES.insert(0, SITECUSTOMIZE_DIR) PYTHONPATH_ENV_VAR = os.pathsep.join(PYTHON_PATH_ENTRIES) os.environ[str('PYTHONPATH')] = str(PYTHONPATH_ENV_VAR) if SITECUSTOMIZE_DIR in sys.path: sys.path.remove(SITECUSTOMIZE_DIR) sys.path.insert(0, SITECUSTOMIZE_DIR) import atexit import traceback from salt.scripts import salt_foobar_2 def main(): if sys.platform.startswith("win"): import os.path import py_compile cfile = os.path.splitext(__file__)[0] + '.pyc' if not os.path.exists(cfile): py_compile.compile(__file__, cfile) salt_foobar_2() if __name__ == '__main__': exitcode = 0 try: main() except SystemExit as exc: exitcode = exc.code # https://docs.python.org/3/library/exceptions.html#SystemExit if exitcode is None: exitcode = 0 if not isinstance(exitcode, int): # A string?! sys.stderr.write(exitcode) exitcode = 1 except Exception as exc: sys.stderr.write( "An un-handled exception was caught: " + str(exc) + "\\n" + traceback.format_exc() ) exitcode = 1 sys.stdout.flush() sys.stderr.flush() atexit._run_exitfuncs() os._exit(exitcode) """.format( str(sitecustomize_path) ) ) assert contents == expected def test_generate_script_salt(tmpdir): """ Test script generation for the salt CLI script """ script_path = cli_scripts.generate_script(tmpdir.strpath, "salt") with open(script_path) as rfh: contents = rfh.read() expected = textwrap.dedent( """\ from __future__ import absolute_import import os import sys # We really do not want buffered output os.environ[str("PYTHONUNBUFFERED")] = str("1") # Don't write .pyc files or create them in __pycache__ directories os.environ[str("PYTHONDONTWRITEBYTECODE")] = str("1") import atexit import traceback from salt.scripts import salt_main if __name__ == '__main__': exitcode = 0 try: salt_main() except SystemExit as exc: exitcode = exc.code # https://docs.python.org/3/library/exceptions.html#SystemExit if exitcode is None: exitcode = 0 if not isinstance(exitcode, int): # A string?! sys.stderr.write(exitcode) exitcode = 1 except Exception as exc: sys.stderr.write( "An un-handled exception was caught: " + str(exc) + "\\n" + traceback.format_exc() ) exitcode = 1 sys.stdout.flush() sys.stderr.flush() atexit._run_exitfuncs() os._exit(exitcode) """ ) assert contents == expected def test_generate_script_salt_api(tmpdir): """ Test script generation for the salt-api CLI script """ script_path = cli_scripts.generate_script(tmpdir.strpath, "salt-api") with open(script_path) as rfh: contents = rfh.read() expected = textwrap.dedent( """\ from __future__ import absolute_import import os import sys # We really do not want buffered output os.environ[str("PYTHONUNBUFFERED")] = str("1") # Don't write .pyc files or create them in __pycache__ directories os.environ[str("PYTHONDONTWRITEBYTECODE")] = str("1") import atexit import traceback import salt.cli.api import salt.utils.process salt.utils.process.notify_systemd() def main(): sapi = salt.cli.api.SaltAPI() sapi.start() if __name__ == '__main__': exitcode = 0 try: main() except SystemExit as exc: exitcode = exc.code # https://docs.python.org/3/library/exceptions.html#SystemExit if exitcode is None: exitcode = 0 if not isinstance(exitcode, int): # A string?! sys.stderr.write(exitcode) exitcode = 1 except Exception as exc: sys.stderr.write( "An un-handled exception was caught: " + str(exc) + "\\n" + traceback.format_exc() ) exitcode = 1 sys.stdout.flush() sys.stderr.flush() atexit._run_exitfuncs() os._exit(exitcode) """ ) assert contents == expected def test_generate_script_creates_missing_bin_dir(tmpdir): """ Test defaults script generation """ script_path = cli_scripts.generate_script(tmpdir.join("blah").strpath, "salt-foobar") with open(script_path) as rfh: contents = rfh.read() expected = textwrap.dedent( """\ from __future__ import absolute_import import os import sys # We really do not want buffered output os.environ[str("PYTHONUNBUFFERED")] = str("1") # Don't write .pyc files or create them in __pycache__ directories os.environ[str("PYTHONDONTWRITEBYTECODE")] = str("1") import atexit import traceback from salt.scripts import salt_foobar def main(): if sys.platform.startswith("win"): import os.path import py_compile cfile = os.path.splitext(__file__)[0] + '.pyc' if not os.path.exists(cfile): py_compile.compile(__file__, cfile) salt_foobar() if __name__ == '__main__': exitcode = 0 try: main() except SystemExit as exc: exitcode = exc.code # https://docs.python.org/3/library/exceptions.html#SystemExit if exitcode is None: exitcode = 0 if not isinstance(exitcode, int): # A string?! sys.stderr.write(exitcode) exitcode = 1 except Exception as exc: sys.stderr.write( "An un-handled exception was caught: " + str(exc) + "\\n" + traceback.format_exc() ) exitcode = 1 sys.stdout.flush() sys.stderr.flush() atexit._run_exitfuncs() os._exit(exitcode) """ ) assert contents == expected assert os.path.isdir(tmpdir.join("blah").strpath) def test_generate_script_only_generates_once(tmpdir): """ Test defaults script generation """ script_path = cli_scripts.generate_script(tmpdir.strpath, "salt-foobar") with open(script_path) as rfh: contents = rfh.read() expected = textwrap.dedent( """\ from __future__ import absolute_import import os import sys # We really do not want buffered output os.environ[str("PYTHONUNBUFFERED")] = str("1") # Don't write .pyc files or create them in __pycache__ directories os.environ[str("PYTHONDONTWRITEBYTECODE")] = str("1") import atexit import traceback from salt.scripts import salt_foobar def main(): if sys.platform.startswith("win"): import os.path import py_compile cfile = os.path.splitext(__file__)[0] + '.pyc' if not os.path.exists(cfile): py_compile.compile(__file__, cfile) salt_foobar() if __name__ == '__main__': exitcode = 0 try: main() except SystemExit as exc: exitcode = exc.code # https://docs.python.org/3/library/exceptions.html#SystemExit if exitcode is None: exitcode = 0 if not isinstance(exitcode, int): # A string?! sys.stderr.write(exitcode) exitcode = 1 except Exception as exc: sys.stderr.write( "An un-handled exception was caught: " + str(exc) + "\\n" + traceback.format_exc() ) exitcode = 1 sys.stdout.flush() sys.stderr.flush() atexit._run_exitfuncs() os._exit(exitcode) """ ) assert contents == expected statinfo_1 = os.stat(script_path) script_path = cli_scripts.generate_script(tmpdir.strpath, "salt-foobar") with open(script_path) as rfh: contents = rfh.read() assert contents == expected statinfo_2 = os.stat(script_path) assert statinfo_1 == statinfo_2 pytest-salt-factories-0.907.0/tests/unit/utils/test_platforms.py0000644000175000017500000001013114033044065025631 0ustar vampasvampas00000000000000""" tests.unit.utils.test_platforms ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tests for saltfactories.utils.platforms """ from unittest import mock import saltfactories.utils.platform def test_is_windows(): return_value = True with mock.patch("salt.utils.platform.is_windows", return_value=return_value): assert saltfactories.utils.platform.is_windows() is return_value def test_is_not_windows(): return_value = True with mock.patch("salt.utils.platform.is_windows", return_value=return_value): assert saltfactories.utils.platform.is_windows() is return_value def test_is_linux(): return_value = True with mock.patch("salt.utils.platform.is_linux", return_value=return_value): assert saltfactories.utils.platform.is_linux() is return_value def test_is_not_linux(): return_value = True with mock.patch("salt.utils.platform.is_linux", return_value=return_value): assert saltfactories.utils.platform.is_linux() is return_value def test_is_darwin(): return_value = True with mock.patch("salt.utils.platform.is_darwin", return_value=return_value): assert saltfactories.utils.platform.is_darwin() is return_value def test_is_not_darwin(): return_value = True with mock.patch("salt.utils.platform.is_darwin", return_value=return_value): assert saltfactories.utils.platform.is_darwin() is return_value def test_is_sunos(): return_value = True with mock.patch("salt.utils.platform.is_sunos", return_value=return_value): assert saltfactories.utils.platform.is_sunos() is return_value def test_is_not_sunos(): return_value = True with mock.patch("salt.utils.platform.is_sunos", return_value=return_value): assert saltfactories.utils.platform.is_sunos() is return_value def test_is_smartos(): return_value = True with mock.patch("salt.utils.platform.is_smartos", return_value=return_value): assert saltfactories.utils.platform.is_smartos() is return_value def test_is_not_smartos(): return_value = True with mock.patch("salt.utils.platform.is_smartos", return_value=return_value): assert saltfactories.utils.platform.is_smartos() is return_value def test_is_freebsd(): return_value = True with mock.patch("salt.utils.platform.is_freebsd", return_value=return_value): assert saltfactories.utils.platform.is_freebsd() is return_value def test_is_not_freebsd(): return_value = True with mock.patch("salt.utils.platform.is_freebsd", return_value=return_value): assert saltfactories.utils.platform.is_freebsd() is return_value def test_is_netbsd(): return_value = True with mock.patch("salt.utils.platform.is_netbsd", return_value=return_value): assert saltfactories.utils.platform.is_netbsd() is return_value def test_is_not_netbsd(): return_value = True with mock.patch("salt.utils.platform.is_netbsd", return_value=return_value): assert saltfactories.utils.platform.is_netbsd() is return_value def test_is_openbsd(): return_value = True with mock.patch("salt.utils.platform.is_openbsd", return_value=return_value): assert saltfactories.utils.platform.is_openbsd() is return_value def test_is_not_openbsd(): return_value = True with mock.patch("salt.utils.platform.is_openbsd", return_value=return_value): assert saltfactories.utils.platform.is_openbsd() is return_value def test_is_aix(): return_value = True with mock.patch("salt.utils.platform.is_aix", return_value=return_value): assert saltfactories.utils.platform.is_aix() is return_value def test_is_not_aix(): return_value = True with mock.patch("salt.utils.platform.is_aix", return_value=return_value): assert saltfactories.utils.platform.is_aix() is return_value def test_is_aarch64(): return_value = True with mock.patch("sys.platform", "aarch64"): assert saltfactories.utils.platform.is_aarch64() is return_value def test_is_not_aarch64(): return_value = False with mock.patch("sys.platform", "not_aarch64"): assert saltfactories.utils.platform.is_aarch64() is return_value pytest-salt-factories-0.907.0/tests/unit/utils/test_ports.py0000644000175000017500000000532414035621147025005 0ustar vampasvampas00000000000000""" tests.utils.test_ports ~~~~~~~~~~~~~~~~~~~~~~ Test the port related utilities """ import functools from unittest import mock import pytest import saltfactories.utils.ports as ports_utils class MockedCreateSocket: """ This class just mocks the `socket.socket(...)` call so that we return the ports we want """ def __init__(self, ports): self.ports = list(ports) + list(ports) def __call__(self, *args, **kwargs): port = self.ports.pop(0) # Return a MockedSocket instance return MockedSocket(port) class MockedSocket: """ This class is used so that we can return the known port in the getsockname call """ def __init__(self, port): self.port = port def bind(self, *args, **kwargs): pass def getsockname(self): return None, self.port def close(self): pass def test_get_unused_localhost_port_cached(): """ 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( "saltfactories.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( "saltfactories.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( "saltfactories.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-salt-factories-0.907.0/tests/unit/utils/test_random_string.py0000644000175000017500000000055313720712732026504 0ustar vampasvampas00000000000000""" tests.unit.utils.test_random_string ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Unit tests for the random string generator """ import pytest from saltfactories.utils import random_string def test_raises_runtimeerror_on_bad_arguments(): with pytest.raises(RuntimeError): random_string("foo", uppercase=False, lowercase=False, digits=False) pytest-salt-factories-0.907.0/tests/unit/utils/test_time.py0000644000175000017500000000062513723372562024602 0ustar vampasvampas00000000000000import time from unittest import mock import saltfactories.utils.time def test_sleep(): start = time.time() with mock.patch("time.sleep", return_value=None): time.sleep(1) saltfactories.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