diff --git a/.config/requirements.in b/.config/requirements.in index 24870d52..ab5930fa 100644 --- a/.config/requirements.in +++ b/.config/requirements.in @@ -1,2 +1,3 @@ coverage pytest>=6,<8.0.0 + diff --git a/.config/requirements.txt b/.config/requirements.txt index 4ba635f7..ff4cc535 100644 --- a/.config/requirements.txt +++ b/.config/requirements.txt @@ -1,9 +1,10 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --extra=docs --extra=test --no-annotate --output-file=.config/requirements.txt --resolver=backtracking --strip-extras --unsafe-package=ruamel-yaml-clib pyproject.toml # + ansible-core==2.15.1 attrs==22.2.0 cffi==1.15.1 diff --git a/.gitignore b/.gitignore index b5d6705b..7cf8834d 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,7 @@ venv/ ENV/ env.bak/ venv.bak/ +newenv # Spyder project settings .spyderproject @@ -166,4 +167,4 @@ cython_debug/ # and should all have detailed explanations # Version created and populated by setuptools_scm -/src/pytest_ansible/_version.py \ No newline at end of file +/src/pytest_ansible/_version.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 8da526c2..dcdc9d23 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,12 +2,13 @@ "[python]": { "editor.codeActionsOnSave": { "source.organizeImports": true - } + }, + "editor.defaultFormatter": "ms-python.black-formatter" }, "editor.formatOnSave": true, "isort.check": false, "prettier.enable": false, - "python.formatting.provider": "black", + "python.formatting.provider": "none", "python.linting.flake8Enabled": true, "python.linting.mypyEnabled": true, "python.linting.pylintEnabled": true, diff --git a/molecule/default/INSTALL.rst b/molecule/default/INSTALL.rst new file mode 100644 index 00000000..1b38d098 --- /dev/null +++ b/molecule/default/INSTALL.rst @@ -0,0 +1,15 @@ +*********************************** +Delegated driver installation guide +*********************************** + +Requirements +============ + +This driver is delegated to the developer. Up to the developer to implement +requirements. + +Install +======= + +This driver is delegated to the developer. Up to the developer to implement +requirements. diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml new file mode 100644 index 00000000..26c3c78b --- /dev/null +++ b/molecule/default/converge.yml @@ -0,0 +1,8 @@ +--- +- name: Converge + hosts: all + gather_facts: false + tasks: + - name: "Include pytest-ansible" + ansible.builtin.include_role: + name: "pytest-ansible" diff --git a/molecule/default/create.yml b/molecule/default/create.yml new file mode 100644 index 00000000..0d14ffc6 --- /dev/null +++ b/molecule/default/create.yml @@ -0,0 +1,36 @@ +--- +- name: Create + hosts: localhost + connection: local + gather_facts: false + no_log: "{{ molecule_no_log }}" + tasks: + # TODO: Developer must implement and populate 'server' variable + + - when: server.changed | default(false) | bool + block: + - name: Populate instance config dict + ansible.builtin.set_fact: + instance_conf_dict: + { + "instance": "{{ }}", + "address": "{{ }}", + "user": "{{ }}", + "port": "{{ }}", + "identity_file": "{{ }}", + } + with_items: "{{ server.results }}" + register: instance_config_dict + + - name: Convert instance config dict to a list + ansible.builtin.set_fact: + instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" + + - name: Dump instance config + ansible.builtin.copy: + content: | + # Molecule managed + + {{ instance_conf | to_json | from_json | to_yaml }} + dest: "{{ molecule_instance_config }}" + mode: 0600 diff --git a/molecule/default/destroy.yml b/molecule/default/destroy.yml new file mode 100644 index 00000000..dd6e2203 --- /dev/null +++ b/molecule/default/destroy.yml @@ -0,0 +1,24 @@ +--- +- name: Destroy + hosts: localhost + connection: local + gather_facts: false + no_log: "{{ molecule_no_log }}" + tasks: + # Developer must implement. + + # Mandatory configuration for Molecule to function. + + - name: Populate instance config + ansible.builtin.set_fact: + instance_conf: {} + + - name: Dump instance config + ansible.builtin.copy: + content: | + # Molecule managed + + {{ instance_conf | to_json | from_json | to_yaml }} + dest: "{{ molecule_instance_config }}" + mode: 0600 + when: server.changed | default(false) | bool diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml new file mode 100644 index 00000000..ba225d60 --- /dev/null +++ b/molecule/default/molecule.yml @@ -0,0 +1,31 @@ +--- +dependency: + name: galaxy + state: present + +driver: + name: delegated + +platforms: + - name: instance + +verifier: + name: ansible + +provisioner: + name: ansible + log: true + config_options: + defaults: + interpreter_python: auto + verbosity: 1 + +scenario: + name: default + test_sequence: + - dependency + - syntax + - create + - prepare + - converge + - verify diff --git a/molecule/default/verify.yml b/molecule/default/verify.yml new file mode 100644 index 00000000..a5cfa75e --- /dev/null +++ b/molecule/default/verify.yml @@ -0,0 +1,10 @@ +--- +# This is an example playbook to execute Ansible tests. + +- name: Verify + hosts: all + gather_facts: false + tasks: + - name: Example assertion + ansible.builtin.assert: + that: true diff --git a/newenv/bin/Activate.ps1 b/newenv/bin/Activate.ps1 new file mode 100644 index 00000000..bab2ca64 --- /dev/null +++ b/newenv/bin/Activate.ps1 @@ -0,0 +1,247 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove VIRTUAL_ENV_PROMPT altogether. + if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { + Remove-Item -Path env:VIRTUAL_ENV_PROMPT + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virutal environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } + $env:VIRTUAL_ENV_PROMPT = $Prompt +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/newenv/bin/activate b/newenv/bin/activate new file mode 100644 index 00000000..59a85935 --- /dev/null +++ b/newenv/bin/activate @@ -0,0 +1,69 @@ +# This file must be used with "source bin/activate" *from bash* +# you cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # This should detect bash and zsh, which have a hash command that must + # be called to get it to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +VIRTUAL_ENV="/home/ruchi/pytest-ansible/newenv" +export VIRTUAL_ENV + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/bin:$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1="(newenv) ${PS1:-}" + export PS1 + VIRTUAL_ENV_PROMPT="(newenv) " + export VIRTUAL_ENV_PROMPT +fi + +# This should detect bash and zsh, which have a hash command that must +# be called to get it to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null +fi diff --git a/newenv/bin/activate.csh b/newenv/bin/activate.csh new file mode 100644 index 00000000..78f865b7 --- /dev/null +++ b/newenv/bin/activate.csh @@ -0,0 +1,26 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV "/home/ruchi/pytest-ansible/newenv" + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/bin:$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = "(newenv) $prompt" + setenv VIRTUAL_ENV_PROMPT "(newenv) " +endif + +alias pydoc python -m pydoc + +rehash diff --git a/newenv/bin/activate.fish b/newenv/bin/activate.fish new file mode 100644 index 00000000..cacfb50e --- /dev/null +++ b/newenv/bin/activate.fish @@ -0,0 +1,66 @@ +# This file must be used with "source /bin/activate.fish" *from fish* +# (https://fishshell.com/); you cannot run it directly. + +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + functions -e fish_prompt + set -e _OLD_FISH_PROMPT_OVERRIDE + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + + set -e VIRTUAL_ENV + set -e VIRTUAL_ENV_PROMPT + if test "$argv[1]" != "nondestructive" + # Self-destruct! + functions -e deactivate + end +end + +# Unset irrelevant variables. +deactivate nondestructive + +set -gx VIRTUAL_ENV "/home/ruchi/pytest-ansible/newenv" + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/bin" $PATH + +# Unset PYTHONHOME if set. +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # Save the current fish_prompt function as the function _old_fish_prompt. + functions -c fish_prompt _old_fish_prompt + + # With the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command. + set -l old_status $status + + # Output the venv prompt; color taken from the blue of the Python logo. + printf "%s%s%s" (set_color 4B8BBE) "(newenv) " (set_color normal) + + # Restore the return status of the previous command. + echo "exit $old_status" | . + # Output the original/"old" prompt. + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" + set -gx VIRTUAL_ENV_PROMPT "(newenv) " +end diff --git a/newenv/bin/ansible b/newenv/bin/ansible new file mode 100755 index 00000000..819eeaa6 --- /dev/null +++ b/newenv/bin/ansible @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from ansible.cli.adhoc import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/ansible-config b/newenv/bin/ansible-config new file mode 100755 index 00000000..126d68e5 --- /dev/null +++ b/newenv/bin/ansible-config @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from ansible.cli.config import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/ansible-connection b/newenv/bin/ansible-connection new file mode 100755 index 00000000..6c203cde --- /dev/null +++ b/newenv/bin/ansible-connection @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from ansible.cli.scripts.ansible_connection_cli_stub import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/ansible-console b/newenv/bin/ansible-console new file mode 100755 index 00000000..82b192bf --- /dev/null +++ b/newenv/bin/ansible-console @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from ansible.cli.console import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/ansible-doc b/newenv/bin/ansible-doc new file mode 100755 index 00000000..d82cafe3 --- /dev/null +++ b/newenv/bin/ansible-doc @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from ansible.cli.doc import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/ansible-galaxy b/newenv/bin/ansible-galaxy new file mode 100755 index 00000000..6a33a2ab --- /dev/null +++ b/newenv/bin/ansible-galaxy @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from ansible.cli.galaxy import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/ansible-inventory b/newenv/bin/ansible-inventory new file mode 100755 index 00000000..13cc6217 --- /dev/null +++ b/newenv/bin/ansible-inventory @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from ansible.cli.inventory import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/ansible-playbook b/newenv/bin/ansible-playbook new file mode 100755 index 00000000..c9652dac --- /dev/null +++ b/newenv/bin/ansible-playbook @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from ansible.cli.playbook import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/ansible-pull b/newenv/bin/ansible-pull new file mode 100755 index 00000000..1a2a3334 --- /dev/null +++ b/newenv/bin/ansible-pull @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from ansible.cli.pull import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/ansible-test b/newenv/bin/ansible-test new file mode 100755 index 00000000..33c176e7 --- /dev/null +++ b/newenv/bin/ansible-test @@ -0,0 +1,45 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# PYTHON_ARGCOMPLETE_OK +"""Command line entry point for ansible-test.""" + +# NOTE: This file resides in the _util/target directory to ensure compatibility with all supported Python versions. + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import sys + + +def main(args=None): + """Main program entry point.""" + ansible_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + source_root = os.path.join(ansible_root, 'test', 'lib') + + if os.path.exists(os.path.join(source_root, 'ansible_test', '_internal', '__init__.py')): + # running from source, use that version of ansible-test instead of any version that may already be installed + sys.path.insert(0, source_root) + + # noinspection PyProtectedMember + from ansible_test._util.target.common.constants import CONTROLLER_PYTHON_VERSIONS + + if version_to_str(sys.version_info[:2]) not in CONTROLLER_PYTHON_VERSIONS: + raise SystemExit('This version of ansible-test cannot be executed with Python version %s. Supported Python versions are: %s' % ( + version_to_str(sys.version_info[:3]), ', '.join(CONTROLLER_PYTHON_VERSIONS))) + + if any(not os.get_blocking(handle.fileno()) for handle in (sys.stdin, sys.stdout, sys.stderr)): + raise SystemExit('Standard input, output and error file handles must be blocking to run ansible-test.') + + # noinspection PyProtectedMember + from ansible_test._internal import main as cli_main + + cli_main(args) + + +def version_to_str(version): + """Return a version string from a version tuple.""" + return '.'.join(str(n) for n in version) + + +if __name__ == '__main__': + main() diff --git a/newenv/bin/ansible-vault b/newenv/bin/ansible-vault new file mode 100755 index 00000000..838b3f21 --- /dev/null +++ b/newenv/bin/ansible-vault @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from ansible.cli.vault import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/chardetect b/newenv/bin/chardetect new file mode 100755 index 00000000..d458316b --- /dev/null +++ b/newenv/bin/chardetect @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from chardet.cli.chardetect import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/cookiecutter b/newenv/bin/cookiecutter new file mode 100755 index 00000000..8ca922a6 --- /dev/null +++ b/newenv/bin/cookiecutter @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from cookiecutter.__main__ import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/coverage b/newenv/bin/coverage new file mode 100755 index 00000000..4ec53436 --- /dev/null +++ b/newenv/bin/coverage @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from coverage.cmdline import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/coverage-3.10 b/newenv/bin/coverage-3.10 new file mode 100755 index 00000000..4ec53436 --- /dev/null +++ b/newenv/bin/coverage-3.10 @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from coverage.cmdline import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/coverage3 b/newenv/bin/coverage3 new file mode 100755 index 00000000..4ec53436 --- /dev/null +++ b/newenv/bin/coverage3 @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from coverage.cmdline import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/dmypy b/newenv/bin/dmypy new file mode 100755 index 00000000..6741a0e8 --- /dev/null +++ b/newenv/bin/dmypy @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3.10 +# -*- coding: utf-8 -*- +import re +import sys +from mypy.dmypy.client import console_entry +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(console_entry()) diff --git a/newenv/bin/jsonschema b/newenv/bin/jsonschema new file mode 100755 index 00000000..ae891898 --- /dev/null +++ b/newenv/bin/jsonschema @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from jsonschema.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/markdown-it b/newenv/bin/markdown-it new file mode 100755 index 00000000..8e9b99c5 --- /dev/null +++ b/newenv/bin/markdown-it @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from markdown_it.cli.parse import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/molecule b/newenv/bin/molecule new file mode 100755 index 00000000..4dc2114e --- /dev/null +++ b/newenv/bin/molecule @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from molecule.__main__ import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/mypy b/newenv/bin/mypy new file mode 100755 index 00000000..21621b67 --- /dev/null +++ b/newenv/bin/mypy @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3.10 +# -*- coding: utf-8 -*- +import re +import sys +from mypy.__main__ import console_entry +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(console_entry()) diff --git a/newenv/bin/mypyc b/newenv/bin/mypyc new file mode 100755 index 00000000..da0d9dbe --- /dev/null +++ b/newenv/bin/mypyc @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3.10 +# -*- coding: utf-8 -*- +import re +import sys +from mypyc.__main__ import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/normalizer b/newenv/bin/normalizer new file mode 100755 index 00000000..ab56efd5 --- /dev/null +++ b/newenv/bin/normalizer @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from charset_normalizer.cli.normalizer import cli_detect +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli_detect()) diff --git a/newenv/bin/pip b/newenv/bin/pip new file mode 100755 index 00000000..7ced9df5 --- /dev/null +++ b/newenv/bin/pip @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/pip3 b/newenv/bin/pip3 new file mode 100755 index 00000000..7ced9df5 --- /dev/null +++ b/newenv/bin/pip3 @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/pip3.10 b/newenv/bin/pip3.10 new file mode 100755 index 00000000..7ced9df5 --- /dev/null +++ b/newenv/bin/pip3.10 @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/pip3.11 b/newenv/bin/pip3.11 new file mode 100755 index 00000000..7ced9df5 --- /dev/null +++ b/newenv/bin/pip3.11 @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/py.test b/newenv/bin/py.test new file mode 100755 index 00000000..3d37f29f --- /dev/null +++ b/newenv/bin/py.test @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pytest import console_main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(console_main()) diff --git a/newenv/bin/pygmentize b/newenv/bin/pygmentize new file mode 100755 index 00000000..d853dfb1 --- /dev/null +++ b/newenv/bin/pygmentize @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pygments.cmdline import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/pytest b/newenv/bin/pytest new file mode 100755 index 00000000..3d37f29f --- /dev/null +++ b/newenv/bin/pytest @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pytest import console_main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(console_main()) diff --git a/newenv/bin/python b/newenv/bin/python new file mode 120000 index 00000000..b8a0adbb --- /dev/null +++ b/newenv/bin/python @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/newenv/bin/python3 b/newenv/bin/python3 new file mode 120000 index 00000000..b3b8ebf6 --- /dev/null +++ b/newenv/bin/python3 @@ -0,0 +1 @@ +/home/ruchi/venv2/develans/bin/python3 \ No newline at end of file diff --git a/newenv/bin/python3.10 b/newenv/bin/python3.10 new file mode 120000 index 00000000..b8a0adbb --- /dev/null +++ b/newenv/bin/python3.10 @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/newenv/bin/slugify b/newenv/bin/slugify new file mode 100755 index 00000000..432aaef9 --- /dev/null +++ b/newenv/bin/slugify @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from slugify.__main__ import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/stubgen b/newenv/bin/stubgen new file mode 100755 index 00000000..2cb85387 --- /dev/null +++ b/newenv/bin/stubgen @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3.10 +# -*- coding: utf-8 -*- +import re +import sys +from mypy.stubgen import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/bin/stubtest b/newenv/bin/stubtest new file mode 100755 index 00000000..8369b7b5 --- /dev/null +++ b/newenv/bin/stubtest @@ -0,0 +1,8 @@ +#!/home/ruchi/pytest-ansible/newenv/bin/python3.10 +# -*- coding: utf-8 -*- +import re +import sys +from mypy.stubtest import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/newenv/lib64 b/newenv/lib64 new file mode 120000 index 00000000..7951405f --- /dev/null +++ b/newenv/lib64 @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/newenv/pyvenv.cfg b/newenv/pyvenv.cfg new file mode 100644 index 00000000..3661203f --- /dev/null +++ b/newenv/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /home/ruchi/venv2/develans/bin +include-system-site-packages = false +version = 3.10.0 diff --git a/pyproject.toml b/pyproject.toml index 1897c643..80d58096 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,9 +90,11 @@ disable = [ "too-many-branches", "too-many-locals", "too-many-statements", + "too-few-public-methods", "unexpected-keyword-arg", "unused-argument", "invalid-name", + "unused-import", ] [tool.pytest.ini_options] diff --git a/src/pytest_ansible/molecule.py b/src/pytest_ansible/molecule.py new file mode 100644 index 00000000..f84a52a7 --- /dev/null +++ b/src/pytest_ansible/molecule.py @@ -0,0 +1,260 @@ +"""pytest-molecule plugin implementation.""" +# pylint: disable=protected-access +from __future__ import annotations + +import logging +import os +import shlex +import subprocess +import sys +import warnings +from shlex import quote + +import pkg_resources +import pytest +import yaml + +try: + from molecule.api import drivers +except ImportError: + drivers = None + + +try: + molecule_config = importlib.import_module("molecule.config") + ansible_version = molecule_config.ansible_version +except ImportError: + ansible_version = None + + :param start_path: The path to the root of the collection + :returns: A tuple of the namespace and name + """ + info_file = start_path / "galaxy.yml" + logger.info("Looking for collection info in %s", info_file) + +logger = logging.getLogger(__name__) + + +def pytest_collection_modifyitems(config, items): + if not config.getoption("--molecule"): + skip_molecule = pytest.mark.skip(reason="Molecule tests are disabled") + for item in items: + if "molecule" in item.keywords: + item.add_marker(skip_molecule) + + +def molecule_pytest_configure(config): + """Pytest hook for loading our specific configuration.""" + interesting_env_vars = [ + "ANSIBLE", + "MOLECULE", + "DOCKER", + "PODMAN", + "VAGRANT", + "VIRSH", + "ZUUL", + ] + + # Add extra information that may be key for debugging failures + if hasattr(config, "_metadata"): + for package in ["molecule"]: + config._metadata["Packages"][package] = pkg_resources.get_distribution( + package, + ).version + + if "Tools" not in config._metadata: + config._metadata["Tools"] = {} + config._metadata["Tools"]["ansible"] = str(ansible_version()) + + # Adds interesting env vars + env = "" + for key, value in sorted(os.environ.items()): + for var_name in interesting_env_vars: + if key.startswith(var_name): + env += f"{key}={value} " + config._metadata["env"] = env + + # We hide DeprecationWarnings thrown by driver loading because these are + # outside our control and worse: they are displayed even on projects that + # have no molecule tests at all as pytest_configure() is called during + # collection, causing spam. + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + + config.option.molecule = {} + for driver in map(str, drivers()): + config.addinivalue_line( + "markers", + f"{driver}: mark test to run only when {driver} is available", + ) + config.option.molecule[driver] = {"available": True} + + config.addinivalue_line( + "markers", + "no_driver: mark used for scenarios that do not contain driver info", + ) + + config.addinivalue_line( + "markers", + "molecule: mark used by all molecule scenarios", + ) + + # validate selinux availability + if sys.platform == "linux" and os.path.isfile("/etc/selinux/config"): + try: + import selinux # noqa pylint: disable=unused-import,import-error,import-outside-toplevel + except ImportError: + logging.error( + "It appears that you are trying to use " + "molecule with a Python interpreter that does not have the " + "libselinux python bindings installed. These can only be " + "installed using your distro package manager and are specific " + "to each python version. Common package names: " + "libselinux-python python2-libselinux python3-libselinux", + ) + # we do not re-raise this exception because missing or broken + # selinux bindings are not guaranteed to fail molecule execution. + + +class MoleculeFile(pytest.File): + """Wrapper class for molecule files.""" + + def collect(self): + """Test generator.""" + if hasattr(MoleculeItem, "from_parent"): + yield MoleculeItem.from_parent(name="test", parent=self) + else: + yield MoleculeItem("test", self) + + def __str__(self): + """Return test name string representation.""" + return str(self.path.relative_to(os.getcwd())) + + +class MoleculeItem(pytest.Item): + """A molecule test. + + Pytest supports multiple tests per file, molecule only one "test". + """ + + def __init__(self, name, parent): + """Construct MoleculeItem.""" + self.funcargs = {} + super().__init__(name, parent) + moleculeyml = self.path + with open(str(moleculeyml), encoding="utf-8") as stream: + # If the molecule.yml file is empty, YAML loader returns None. To + # simplify things down the road, we replace None with an empty + # dict. + data = yaml.load(stream, Loader=yaml.SafeLoader) or {} + + # we add the driver as mark + self.molecule_driver = data.get("driver", {}).get("name", "no_driver") + self.add_marker(self.molecule_driver) + + # check for known markers and add them + markers = data.get("markers", []) + if "xfail" in markers: + self.add_marker( + pytest.mark.xfail( + reason="Marked as broken by scenario configuration.", + ), + ) + if "skip" in markers: + self.add_marker( + pytest.mark.skip(reason="Disabled by scenario configuration."), + ) + + # we also add platforms as marks + for platform in data.get("platforms", []): + platform_name = platform["name"] + self.config.addinivalue_line( + "markers", + f"{platform_name}: molecule platform name is {platform_name}", + ) + self.add_marker(platform_name) + self.add_marker("molecule") + if ( + self.config.option.molecule_unavailable_driver + and not self.config.option.molecule[self.molecule_driver]["available"] + ): + self.add_marker(self.config.option.molecule_unavailable_driver) + + def runtest(self): + """Perform effective test run.""" + folder = self.path.parent + folders = folder.parts + cwd = os.path.abspath(os.path.join(folder, "../..")) + scenario = folders[-1] + + cmd = [sys.executable, "-m", "molecule"] + if self.config.option.molecule_base_config: + cmd.extend(("--base-config", self.config.option.molecule_base_config)) + if self.config.option.skip_no_git_change: + try: + with subprocess.Popen( + ["git", "diff", self.config.option.skip_no_git_change, "--", "./"], + cwd=cwd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + ) as proc: + proc.wait() + if len(proc.stdout.readlines()) == 0: + pytest.skip("No change in role") + except subprocess.CalledProcessError as exc: + pytest.fail( + "Error checking git diff. Error code was: " + + str(exc.returncode) + + "\nError output was: " + + exc.output, + ) + + cmd.extend((self.name, "-s", scenario)) + # We append the additional options to molecule call, allowing user to + # control how molecule is called by pytest-molecule + opts = os.environ.get("MOLECULE_OPTS") + if opts: + cmd.extend(shlex.split(opts)) + + print(f"running: {' '.join(quote(arg) for arg in cmd)} (from {cwd})") + if self.config.getoption("--molecule"): # Check if --molecule option is enabled + try: + # Workaround for STDOUT/STDERR line ordering issue: + # https://github.com/pytest-dev/pytest/issues/5449 + with subprocess.Popen( + cmd, + cwd=cwd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + ) as proc: + for line in proc.stdout: + print(line, end="") + proc.wait() + if proc.returncode != 0: + pytest.fail( + f"Error code {proc.returncode} returned by: {' '.join(cmd)}", + pytrace=False, + ) + except subprocess.CalledProcessError as exc: + pytest.fail( + f"Exception {exc} returned by: {' '.join(cmd)}", + pytrace=False, + ) + else: + pytest.skip( + "Molecule tests are disabled", + ) # Skip the test if --molecule option is not enabled + + def reportinfo(self): + """Return representation of test location when in verbose mode.""" + return self.fspath, 0, f"usecase: {self.name}" + + def __str__(self): + """Return name of the test.""" + return f"{self.name}[{self.molecule_driver}]" + + +class MoleculeExceptionError(Exception): + """Custom exception for error reporting.""" diff --git a/src/pytest_ansible/plugin.py b/src/pytest_ansible/plugin.py index de704958..e1f8a877 100644 --- a/src/pytest_ansible/plugin.py +++ b/src/pytest_ansible/plugin.py @@ -1,6 +1,9 @@ """PyTest Ansible Plugin.""" +from __future__ import annotations import logging +from pathlib import Path +from typing import TYPE_CHECKING import ansible import ansible.constants @@ -17,8 +20,12 @@ ) from pytest_ansible.host_manager import get_host_manager +from .molecule import MoleculeFile from .units import inject, inject_only +if TYPE_CHECKING: + from _pytest.nodes import Node + logger = logging.getLogger(__name__) # Silence linters for imported fixtures @@ -144,7 +151,45 @@ def pytest_addoption(parser): "--ansible-unit-inject-only", action="store_true", default=False, - help="Enable support for ansible collection unit tests by only injecting exisiting ANSIBLE_COLLECTIONS_PATHS.", + help="Enable support for ansible collection unit tests by only injecting exisiting ANSIBLE_COLLECTIONS_PATH.", + ) + + group.addoption( + "--molecule_unavailable_driver", + action="store", + default=None, + help="What marker to add to molecule scenarios when driver is ", + ) + group.addoption( + "--molecule_base_config", + action="store", + default=None, + help="Path to the molecule base config file. The value of this option is ", + ) + group.addoption( + "--skip_no_git_change", + action="store", + default=None, + help="Commit to use as a reference for this test. If the role wasn't", + ) + + group.addoption( + "--molecule_unavailable_driver", + action="store", + default=None, + help="What marker to add to molecule scenarios when driver is ", + ) + group.addoption( + "--molecule_base_config", + action="store", + default=None, + help="Path to the molecule base config file. The value of this option is ", + ) + group.addoption( + "--skip_no_git_change", + action="store", + default=None, + help="Commit to use as a reference for this test. If the role wasn't", ) # Add github marker to --help parser.addini("ansible", "Ansible integration", "args") @@ -174,6 +219,31 @@ def pytest_configure(config): start_path = config.invocation_params.dir inject(start_path) + if config.option.molecule: + inject(start_path) + + +def pytest_collect_file( + file_path: Path | None, + parent: pytest.Collector, +) -> Node | None: + """Transform each found molecule.yml into a pytest test.""" + has_molecule = False + try: + from .molecule import Molecule # noqa: F401 + + has_molecule = True + except ImportError: + pass + + if not has_molecule: + return None + if file_path and file_path.is_symlink(): + return None + if file_path and file_path.name == "molecule.yml": + return MoleculeFile.from_parent(path=file_path, parent=parent) + return None + def pytest_generate_tests(metafunc): """Generate tests when specific `ansible_*` fixtures are used by tests.""" diff --git a/src/pytest_ansible/units.py b/src/pytest_ansible/units.py index 2f5f0d1d..258a333b 100644 --- a/src/pytest_ansible/units.py +++ b/src/pytest_ansible/units.py @@ -102,7 +102,7 @@ def inject(start_path: Path) -> None: logger.info("Collections dir: %s", collections_dir) - # TODO: Make this a configuration option, check COLLECTIONS_PATHS + # TODO: Make this a configuration option, check COLLECTIONS_PATH # Add the user location for any dependencies paths = [str(collections_dir), "~/.ansible/collections"] logger.info("Paths: %s", paths) @@ -119,14 +119,17 @@ def inject(start_path: Path) -> None: # e.g. ansible-galaxy collection install etc # Set the environment variable as courtesy for integration tests + + envvar_name = determine_envvar() env_paths = os.pathsep.join(paths) - logger.info("Setting ANSIBLE_COLLECTIONS_PATH to %s", env_paths) - os.environ["ANSIBLE_COLLECTIONS_PATHS"] = env_paths + logger.info("Setting %s to %s", envvar_name, env_paths) + os.environ[envvar_name] = env_paths def inject_only() -> None: - """Inject the current ANSIBLE_COLLECTIONS_PATHS.""" - env_paths = os.environ.get("ANSIBLE_COLLECTIONS_PATHS", "") + """Inject the current ANSIBLE_COLLECTIONS_PATH(S).""" + envvar_name = determine_envvar() + env_paths = os.environ.get(envvar_name, "") path_list = env_paths.split(os.pathsep) for path in path_list: if path: @@ -148,3 +151,17 @@ def acf_inject(paths: list[str]) -> None: logger.debug("_ACF configured paths: %s", acf._n_configured_paths) else: logger.debug("_ACF not available") + + +def determine_envvar() -> str: + """Use the existence of the AnsibleCollectionFinder to determine + the ansible version. + + ansible 2.9 did not have AnsibleCollectionFinder and did not support ANSIBLE_COLLECTIONS_PATH + later versions do. + + :returns: The appropriate environment variable to use + """ + if not HAS_COLLECTION_FINDER: + return "ANSIBLE_COLLECTIONS_PATHS" + return "ANSIBLE_COLLECTIONS_PATH" diff --git a/tests/conftest.py b/tests/conftest.py index 686e0567..5893de71 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import pytest + from pytest_ansible.has_version import has_ansible_v1, has_ansible_v24 try: @@ -149,3 +150,33 @@ def hosts(): from pytest_ansible.host_manager import get_host_manager return get_host_manager(inventory=",".join(ALL_HOSTS), connection="local") + + +@pytest.fixture(scope="session") +def molecule_ansible(ansible_playbook): + """ + Fixture that returns the `ansible_playbook` fixture. + + :param ansible_playbook: The `ansible_playbook` fixture + :return: The `ansible_playbook` fixture + """ + return ansible_playbook + + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item, call): + """ + Pytest hook that modifies the test item by setting the `molecule_ansible` fixture + for specific fixtures ("ansible_module", "ansible_inventory") when the report + matches the call. + + :param item: Pytest test item + :param call: Test execution call (setup, call, teardown) + """ + outcome = yield + rep = outcome.get_result() + if rep.when == call: + for fixture in ("ansible_module", "ansible_inventory"): + if fixture in item.fixturenames: + setattr(item, fixture, molecule_ansible) + break diff --git a/tests/mol/__init__.py b/tests/mol/__init__.py new file mode 100644 index 00000000..f7289741 --- /dev/null +++ b/tests/mol/__init__.py @@ -0,0 +1 @@ +"""Molecule tests.""" diff --git a/tests/mol/test_mol.py b/tests/mol/test_mol.py new file mode 100644 index 00000000..bda64c81 --- /dev/null +++ b/tests/mol/test_mol.py @@ -0,0 +1,83 @@ +import os +import subprocess +from unittest.mock import Mock + +import pytest + +try: + from pytest_ansible.molecule import MoleculeFile, MoleculeItem +except ModuleNotFoundError: + MoleculeItem = None + MoleculeFile = None + + +class TestMoleculeItem: + "Perform run test" + + @pytest.fixture() + def test_run(self, mocker): # noqa: PT004 + mocker.patch("MoleculeItem.path", Mock()) + mocker.patch("MoleculeItem.config.option.molecule_base_config", None) + mocker.patch("MoleculeItem.config.option.skip_no_git_change", None) + mocker.patch("subprocess.Popen") + mocker.patch("MoleculeItem.config.getoption", lambda x: True) # noqa: PT008 + + molecule_item = MoleculeItem("test", None) + molecule_item.runtest() + proc = subprocess.run( + "pytest pytest-ansible/molecule/default/molecule.yml -v", + shell=True, + check=True, + cwd=os.path.abspath(os.path.join(molecule_item.path.parent, "../..")), + capture_output=True, + ) + assert proc.returncode == 0 + output = proc.stdout.decode("utf-8") + assert "collected 1 item" in output + assert "pytest-ansible/molecule/default/molecule.yml::test" in output + assert "1 passed" in output + + +class TestMoleculeFile: + "Test Generator to collect the test" + + @pytest.fixture() + def test_collect(self, mocker): # noqa: PT004 + mocker.patch("MoleculeItem.from_parent") + mocker.patch("MoleculeItem.__init__") + mocker.patch("MoleculeItem.__str__") + mocker.patch("MoleculeFile.path", Mock()) + + # Test when MoleculeItem.from_parent exists + MoleculeItem.from_parent.return_value = "mocked_item" + molecule_file = MoleculeFile() + items = list(molecule_file.collect()) + assert items == ["mocked_item"] + + # Test when MoleculeItem.from_parent does not exist + MoleculeItem.from_parent.side_effect = AttributeError + MoleculeItem.return_value = "mocked_item" + molecule_file = MoleculeFile() + items = list(molecule_file.collect()) + assert items == ["mocked_item"] + + proc = subprocess.run( + "pytest --collect-only", + shell=True, + check=True, + capture_output=True, + ) + assert proc.returncode == 0 + output = proc.stdout.decode("utf-8") + assert "1 test collected" in output + assert "test[delegated]" in output + + @pytest.fixture() + def _test_str(self, mocker): + mocker.patch("MoleculeFile.path", Mock(return_value="mock/path")) + mocker.patch("os.getcwd", Mock(return_value="mock/cwd")) + molecule_file = MoleculeFile() + assert str(molecule_file) == "mock/path" + assert str(molecule_file) == "mock/path" + assert str(molecule_file) == "mock/path" + assert str(molecule_file) == "mock/path" diff --git a/tests/moleculetest/__init__.py b/tests/moleculetest/__init__.py new file mode 100644 index 00000000..f36dd28b --- /dev/null +++ b/tests/moleculetest/__init__.py @@ -0,0 +1 @@ +"""Molecule Tests""" diff --git a/tests/moleculetest/test_molecule.py b/tests/moleculetest/test_molecule.py new file mode 100644 index 00000000..57b8db93 --- /dev/null +++ b/tests/moleculetest/test_molecule.py @@ -0,0 +1,115 @@ +"""Tests specific to the molecule plugin functionality.""" + +from __future__ import annotations + +import logging +import os +import subprocess +import sys +from pathlib import Path +from unittest.mock import MagicMock + +import pytest +from pytest_ansible.molecule import MoleculeFile, MoleculeItem + + +def test_molecule_collect( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test collecting a molecule.yml file. + + :param monkeypatch: The pytest monkeypatch fixture + :param tmp_path: The pytest tmp_path fixture + :param caplog: The pytest caplog fixture + """ + caplog.set_level(logging.DEBUG) + + # Create a temporary molecule file for testing + molecule_file_path = ( + tmp_path / "pytest-ansible" / "molecule" / "default" / "molecule.yml" + ) + molecule_file_path.parent.mkdir(parents=True, exist_ok=True) + molecule_file_path.write_text("test content") + + # Create the MoleculeFile object for the temporary file + molecule_file = MoleculeFile(molecule_file_path, parent=None) + + # Mock MoleculeItem object for testing + mocked_item = MagicMock() + monkeypatch.setattr( + MoleculeItem, + "from_parent", + MagicMock(return_value=mocked_item), + ) + + # Call the collect() method and convert the generator into a list to check its content + collected_items = list(molecule_file.collect()) + + # Clean up the temporary file and directory + molecule_file_path.unlink() + molecule_file_path.parent.rmdir() + + try: + proc = subprocess.run( + "pytest --collect-only", + capture_output=True, + shell=True, + check=True, + text=True, + ) + except subprocess.CalledProcessError as exc: + print(exc.stdout) + print(exc.stderr) + pytest.fail(exc.stderr) + + assert proc.returncode == 0 + output = proc.stdout.decode("utf-8") + assert "1 test collected" in output + assert "test[delegated]" in output + assert len(collected_items) == 1 + assert collected_items[0] == mocked_item + + +def test_molecule_runtest( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.LogCaptureFixture, +) -> None: + """Test running the file. + + :param monkeypatch: The pytest monkeypatch fixture + :param tmp_path: The pytest tmp_path fixture + :param capsys: The pytest capsys fixture + """ + # Test runtest() function in MoleculeItem + # Mock necessary attributes and environment variables + monkeypatch.setattr(subprocess, "Popen", MagicMock()) + monkeypatch.setenv("MOLECULE_OPTS", "--driver-name mock_driver") + monkeypatch.setattr(sys, "executable", "/path/to/python") + monkeypatch.setattr(os, "getcwd", MagicMock(return_value="/path/to")) + + molecule_item = MoleculeItem("test", parent=None) + + # Run the runtest() function + molecule_item.runtest() + + proc = subprocess.run( + f"{sys.executable} -m pytest molecule.yml -v", + shell=True, + check=True, + cwd="/home/ruchi/pytest-ansible/molecule/default/", + capture_output=True, + ) + assert proc.returncode == 0 + output = proc.stdout.decode("utf-8") + assert "collected 1 item" in output + assert "pytest-ansible/molecule/default/molecule.yml::test" in output + assert "1 passed" in output + + # Capture the printed output and check if the command is correctly formed + captured = capsys.readouterr() + assert ( + captured.out.strip() + == "running: python -m molecule test -s scenario --driver-name mock_driver" + ) diff --git a/tests/unit/test_unit.py b/tests/unit/test_unit.py index 3f712a6e..3501fb42 100644 --- a/tests/unit/test_unit.py +++ b/tests/unit/test_unit.py @@ -64,7 +64,7 @@ def test_inject_only( :param caplog: The pytest caplog fixture """ caplog.set_level(logging.DEBUG) - monkeypatch.setenv("ANSIBLE_COLLECTIONS_PATHS", str(tmp_path / "collections")) + monkeypatch.setenv("ANSIBLE_COLLECTIONS_PATH", str(tmp_path / "collections")) (tmp_path / "collections" / "ansible_collections").mkdir(parents=True)