From c003b611f6f8234e1af6b979f55441ae50f2c931 Mon Sep 17 00:00:00 2001 From: llbbl Date: Thu, 4 Sep 2025 18:33:51 +0000 Subject: [PATCH] feat: Set up comprehensive Python testing infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Poetry package manager with pyproject.toml configuration - Migrate existing dependencies from requirements.txt to Poetry - Configure pytest with coverage reporting and custom markers (unit, integration, slow) - Create structured testing directories (tests/unit/, tests/integration/) - Add comprehensive shared fixtures in conftest.py for common testing scenarios - Set up .gitignore with testing-related exclusions - Include validation tests to verify infrastructure setup - Configure 80% test coverage threshold with HTML/XML reporting 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .gitignore | 167 +++++++++++++++++++++++ .venv_test/bin/Activate.ps1 | 247 ++++++++++++++++++++++++++++++++++ .venv_test/bin/activate | 69 ++++++++++ .venv_test/bin/activate.csh | 26 ++++ .venv_test/bin/activate.fish | 69 ++++++++++ .venv_test/bin/coverage | 8 ++ .venv_test/bin/coverage-3.11 | 8 ++ .venv_test/bin/coverage3 | 8 ++ .venv_test/bin/pip | 8 ++ .venv_test/bin/pip3 | 8 ++ .venv_test/bin/pip3.11 | 8 ++ .venv_test/bin/py.test | 8 ++ .venv_test/bin/pytest | 8 ++ .venv_test/bin/python | 1 + .venv_test/bin/python3 | 1 + .venv_test/bin/python3.11 | 1 + .venv_test/lib64 | 1 + .venv_test/pyvenv.cfg | 5 + pyproject.toml | 145 ++++++++++++++++++++ tests/__init__.py | 1 + tests/conftest.py | 218 ++++++++++++++++++++++++++++++ tests/integration/__init__.py | 1 + tests/test_infrastructure.py | 109 +++++++++++++++ tests/unit/__init__.py | 1 + 24 files changed, 1126 insertions(+) create mode 100644 .gitignore create mode 100644 .venv_test/bin/Activate.ps1 create mode 100644 .venv_test/bin/activate create mode 100644 .venv_test/bin/activate.csh create mode 100644 .venv_test/bin/activate.fish create mode 100755 .venv_test/bin/coverage create mode 100755 .venv_test/bin/coverage-3.11 create mode 100755 .venv_test/bin/coverage3 create mode 100755 .venv_test/bin/pip create mode 100755 .venv_test/bin/pip3 create mode 100755 .venv_test/bin/pip3.11 create mode 100755 .venv_test/bin/py.test create mode 100755 .venv_test/bin/pytest create mode 120000 .venv_test/bin/python create mode 120000 .venv_test/bin/python3 create mode 120000 .venv_test/bin/python3.11 create mode 120000 .venv_test/lib64 create mode 100644 .venv_test/pyvenv.cfg create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/test_infrastructure.py create mode 100644 tests/unit/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d70954 --- /dev/null +++ b/.gitignore @@ -0,0 +1,167 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +coverage.xml +.tox/ +.nox/ + +# Claude Code settings +.claude/ + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Project-specific +*.log +*.tmp +*.temp +work_dir/ +workzone/ +results/ +output/ +*.qcow2 +*.img \ No newline at end of file diff --git a/.venv_test/bin/Activate.ps1 b/.venv_test/bin/Activate.ps1 new file mode 100644 index 0000000..eeea358 --- /dev/null +++ b/.venv_test/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 virtual 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/.venv_test/bin/activate b/.venv_test/bin/activate new file mode 100644 index 0000000..e7c0797 --- /dev/null +++ b/.venv_test/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=/workspace/.venv_test +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='(.venv_test) '"${PS1:-}" + export PS1 + VIRTUAL_ENV_PROMPT='(.venv_test) ' + 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/.venv_test/bin/activate.csh b/.venv_test/bin/activate.csh new file mode 100644 index 0000000..c8c38e1 --- /dev/null +++ b/.venv_test/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 /workspace/.venv_test + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/"bin":$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = '(.venv_test) '"$prompt" + setenv VIRTUAL_ENV_PROMPT '(.venv_test) ' +endif + +alias pydoc python -m pydoc + +rehash diff --git a/.venv_test/bin/activate.fish b/.venv_test/bin/activate.fish new file mode 100644 index 0000000..c4ee84d --- /dev/null +++ b/.venv_test/bin/activate.fish @@ -0,0 +1,69 @@ +# 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" + set -e _OLD_FISH_PROMPT_OVERRIDE + # prevents error when using nested fish instances (Issue #93858) + if functions -q _old_fish_prompt + functions -e fish_prompt + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + 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 /workspace/.venv_test + +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) '(.venv_test) ' (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 '(.venv_test) ' +end diff --git a/.venv_test/bin/coverage b/.venv_test/bin/coverage new file mode 100755 index 0000000..258c88c --- /dev/null +++ b/.venv_test/bin/coverage @@ -0,0 +1,8 @@ +#!/workspace/.venv_test/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/.venv_test/bin/coverage-3.11 b/.venv_test/bin/coverage-3.11 new file mode 100755 index 0000000..258c88c --- /dev/null +++ b/.venv_test/bin/coverage-3.11 @@ -0,0 +1,8 @@ +#!/workspace/.venv_test/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/.venv_test/bin/coverage3 b/.venv_test/bin/coverage3 new file mode 100755 index 0000000..258c88c --- /dev/null +++ b/.venv_test/bin/coverage3 @@ -0,0 +1,8 @@ +#!/workspace/.venv_test/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/.venv_test/bin/pip b/.venv_test/bin/pip new file mode 100755 index 0000000..ebdab8e --- /dev/null +++ b/.venv_test/bin/pip @@ -0,0 +1,8 @@ +#!/workspace/.venv_test/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/.venv_test/bin/pip3 b/.venv_test/bin/pip3 new file mode 100755 index 0000000..ebdab8e --- /dev/null +++ b/.venv_test/bin/pip3 @@ -0,0 +1,8 @@ +#!/workspace/.venv_test/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/.venv_test/bin/pip3.11 b/.venv_test/bin/pip3.11 new file mode 100755 index 0000000..ebdab8e --- /dev/null +++ b/.venv_test/bin/pip3.11 @@ -0,0 +1,8 @@ +#!/workspace/.venv_test/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/.venv_test/bin/py.test b/.venv_test/bin/py.test new file mode 100755 index 0000000..0d933c4 --- /dev/null +++ b/.venv_test/bin/py.test @@ -0,0 +1,8 @@ +#!/workspace/.venv_test/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/.venv_test/bin/pytest b/.venv_test/bin/pytest new file mode 100755 index 0000000..0d933c4 --- /dev/null +++ b/.venv_test/bin/pytest @@ -0,0 +1,8 @@ +#!/workspace/.venv_test/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/.venv_test/bin/python b/.venv_test/bin/python new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/.venv_test/bin/python @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/.venv_test/bin/python3 b/.venv_test/bin/python3 new file mode 120000 index 0000000..ae65fda --- /dev/null +++ b/.venv_test/bin/python3 @@ -0,0 +1 @@ +/usr/bin/python3 \ No newline at end of file diff --git a/.venv_test/bin/python3.11 b/.venv_test/bin/python3.11 new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/.venv_test/bin/python3.11 @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/.venv_test/lib64 b/.venv_test/lib64 new file mode 120000 index 0000000..7951405 --- /dev/null +++ b/.venv_test/lib64 @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/.venv_test/pyvenv.cfg b/.venv_test/pyvenv.cfg new file mode 100644 index 0000000..9476cdd --- /dev/null +++ b/.venv_test/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.11.2 +executable = /usr/bin/python3.11 +command = /usr/bin/python3 -m venv /workspace/.venv_test diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1ba222f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,145 @@ +[tool.poetry] +name = "syzscope" +version = "0.1.0" +description = "A comprehensive binary analysis framework for vulnerability research" +authors = ["SyzScope Team"] +readme = "README.md" +packages = [{include = "syzscope"}] + +[tool.poetry.dependencies] +python = "^3.8" +ailment = "9.0.5405" +angr = "9.0.5405" +archinfo = "9.0.5405" +bcrypt = "^3.2.0" +beautifulsoup4 = "^4.9.1" +bitstring = "^3.1.7" +cachetools = "^4.2.1" +capstone = "^4.0.2" +certifi = "2020.4.5.1" +cffi = "1.14.4" +chardet = "3.0.4" +claripy = "9.0.5405" +cle = "9.0.5405" +CppHeaderParser = "2.7.4" +cryptography = "^3.3.1" +DateTime = "4.3" +decorator = "4.4.2" +dpkt = "1.9.4" +future = "0.18.2" +gitdb = "4.0.5" +GitPython = "3.1.12" +idna = "2.9" +intervaltree = "3.1.0" +itanium-demangler = "1.0" +Mako = "1.1.4" +MarkupSafe = "1.1.1" +mulpyplexer = "0.9" +networkx = "^2.5" +numpy = "^1.18.4" +packaging = "20.8" +paramiko = "2.7.2" +pefile = "2019.4.18" +pexpect = "4.8.0" +plumbum = "1.6.9" +ply = "3.11" +progressbar2 = "3.53.1" +protobuf = "3.14.0" +psutil = "5.8.0" +ptyprocess = "0.7.0" +pycparser = "2.20" +pyelftools = "0.27" +Pygments = "2.7.4" +PyNaCl = "1.4.0" +pyparsing = "2.4.7" +pyserial = "3.5" +PySMT = "0.9.0" +PySocks = "1.7.1" +python-dateutil = "2.8.1" +python-utils = "2.5.5" +pytz = "2020.1" +pyvex = "9.0.5405" +requests = "^2.23.0" +ROPGadget = "6.5" +rpyc = "5.0.1" +six = "1.15.0" +smmap = "3.0.5" +sortedcontainers = "2.3.0" +soupsieve = "^2.0.1" +unicorn = "1.0.2rc4" +urllib3 = "1.25.9" +wllvm = "1.2.8" +z3-solver = "4.8.10.0" +"zope.interface" = "5.1.0" +pwntools = "4.10.0" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.0" +pytest-cov = "^4.0" +pytest-mock = "^3.10" + +[tool.poetry.scripts] +test = "pytest" +tests = "pytest" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "--strict-markers", + "--strict-config", + "--verbose", + "--cov=syzscope", + "--cov-report=term-missing", + "--cov-report=html:htmlcov", + "--cov-report=xml:coverage.xml", + "--cov-fail-under=80", +] +markers = [ + "unit: marks tests as unit tests (fast, isolated)", + "integration: marks tests as integration tests (slower, may require external resources)", + "slow: marks tests as slow (may take a long time to run)", +] +filterwarnings = [ + "ignore::DeprecationWarning", + "ignore::PendingDeprecationWarning", +] + +[tool.coverage.run] +source = ["syzscope"] +omit = [ + "*/tests/*", + "*/test_*", + "*/__pycache__/*", + "*/.*", + "setup.py", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", +] +show_missing = true +skip_covered = false +precision = 2 + +[tool.coverage.html] +directory = "htmlcov" + +[tool.coverage.xml] +output = "coverage.xml" \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..2b5ed94 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Testing package for syzscope \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..6e3cbbf --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,218 @@ +"""Shared pytest fixtures for syzscope testing.""" + +import os +import tempfile +import shutil +from pathlib import Path +from typing import Dict, Any, Generator +from unittest.mock import Mock, MagicMock + +import pytest + + +@pytest.fixture +def temp_dir() -> Generator[Path, None, None]: + """Provide a temporary directory for testing.""" + temp_dir = tempfile.mkdtemp() + try: + yield Path(temp_dir) + finally: + shutil.rmtree(temp_dir, ignore_errors=True) + + +@pytest.fixture +def temp_file(temp_dir: Path) -> Generator[Path, None, None]: + """Provide a temporary file for testing.""" + temp_file = temp_dir / "test_file.txt" + temp_file.touch() + yield temp_file + + +@pytest.fixture +def mock_config() -> Dict[str, Any]: + """Provide mock configuration for testing.""" + return { + "debug": True, + "timeout": 30, + "max_retries": 3, + "log_level": "INFO", + "work_dir": "/tmp/syzscope_test", + "vm_config": { + "memory": "2G", + "cpu_count": 2, + "disk_size": "10G", + }, + "analysis_config": { + "max_depth": 100, + "enable_caching": True, + "timeout": 300, + } + } + + +@pytest.fixture +def mock_vm_instance(): + """Mock VM instance for testing.""" + vm_mock = Mock() + vm_mock.start.return_value = True + vm_mock.stop.return_value = True + vm_mock.is_running.return_value = True + vm_mock.execute_command.return_value = ("output", "", 0) + vm_mock.upload_file.return_value = True + vm_mock.download_file.return_value = True + return vm_mock + + +@pytest.fixture +def mock_kernel(): + """Mock kernel for testing.""" + kernel_mock = Mock() + kernel_mock.version = "5.4.0" + kernel_mock.arch = "x86_64" + kernel_mock.config = {"CONFIG_KASAN": True, "CONFIG_KCOV": True} + kernel_mock.build.return_value = True + kernel_mock.get_symbols.return_value = {"func1": 0x12345, "func2": 0x67890} + return kernel_mock + + +@pytest.fixture +def mock_crash_case(): + """Mock crash case for testing.""" + crash_mock = Mock() + crash_mock.title = "Test crash case" + crash_mock.reproducer = "test_reproducer.c" + crash_mock.syzkaller_prog = "test_prog" + crash_mock.kernel_config = "test_config" + crash_mock.crash_log = "test crash log" + crash_mock.manager = "test_manager" + crash_mock.time = "2023-01-01 12:00:00" + return crash_mock + + +@pytest.fixture +def mock_static_analyzer(): + """Mock static analyzer for testing.""" + analyzer_mock = Mock() + analyzer_mock.analyze.return_value = { + "vulnerabilities": [], + "call_graph": {}, + "data_flow": {}, + "taint_analysis": {} + } + analyzer_mock.get_function_info.return_value = { + "name": "test_func", + "file": "test.c", + "line": 100, + "type": "function" + } + return analyzer_mock + + +@pytest.fixture +def mock_symbolic_executor(): + """Mock symbolic executor for testing.""" + sym_exec_mock = Mock() + sym_exec_mock.run.return_value = { + "states": [], + "constraints": [], + "memory_map": {}, + "registers": {} + } + sym_exec_mock.add_constraint.return_value = True + sym_exec_mock.solve.return_value = {"solution": True, "model": {}} + return sym_exec_mock + + +@pytest.fixture +def mock_gdb_session(): + """Mock GDB session for testing.""" + gdb_mock = Mock() + gdb_mock.attach.return_value = True + gdb_mock.detach.return_value = True + gdb_mock.set_breakpoint.return_value = 1 + gdb_mock.continue_execution.return_value = True + gdb_mock.get_registers.return_value = {"rax": 0x123, "rbx": 0x456} + gdb_mock.get_memory.return_value = b"\x00\x01\x02\x03" + gdb_mock.execute_command.return_value = "gdb output" + return gdb_mock + + +@pytest.fixture +def sample_binary_path(temp_dir: Path) -> Path: + """Provide path to a sample binary for testing.""" + binary_path = temp_dir / "sample_binary" + # Create a minimal ELF file header for testing + with open(binary_path, "wb") as f: + f.write(b"\x7fELF\x02\x01\x01\x00" + b"\x00" * 56) # Minimal ELF header + return binary_path + + +@pytest.fixture +def sample_c_file(temp_dir: Path) -> Path: + """Provide a sample C file for testing.""" + c_file = temp_dir / "sample.c" + c_file.write_text(""" +#include +#include + +int vulnerable_function(char *input) { + char buffer[128]; + strcpy(buffer, input); // Potential buffer overflow + return strlen(buffer); +} + +int main(int argc, char *argv[]) { + if (argc > 1) { + return vulnerable_function(argv[1]); + } + return 0; +} +""") + return c_file + + +@pytest.fixture +def mock_environment_vars(monkeypatch): + """Mock environment variables for testing.""" + test_env = { + "SYZSCOPE_DEBUG": "1", + "SYZSCOPE_WORKDIR": "/tmp/syzscope_test", + "SYZSCOPE_TIMEOUT": "300", + "QEMU_PATH": "/usr/bin/qemu-system-x86_64", + } + for key, value in test_env.items(): + monkeypatch.setenv(key, value) + return test_env + + +@pytest.fixture +def mock_file_system(monkeypatch, temp_dir: Path): + """Mock file system operations for testing.""" + def mock_exists(path): + return True if "existing" in str(path) else False + + def mock_isfile(path): + return True if "file" in str(path) else False + + def mock_isdir(path): + return True if "dir" in str(path) else False + + monkeypatch.setattr(os.path, "exists", mock_exists) + monkeypatch.setattr(os.path, "isfile", mock_isfile) + monkeypatch.setattr(os.path, "isdir", mock_isdir) + + +@pytest.fixture +def capture_logs(caplog): + """Fixture to capture and provide access to logs.""" + return caplog + + +@pytest.fixture(scope="session") +def test_data_dir() -> Path: + """Provide path to test data directory.""" + return Path(__file__).parent / "data" + + +# Pytest configuration and custom markers +pytest_plugins = ["pytest_mock"] \ No newline at end of file diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e27cd7a --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1 @@ +# Integration tests package \ No newline at end of file diff --git a/tests/test_infrastructure.py b/tests/test_infrastructure.py new file mode 100644 index 0000000..b04f0b1 --- /dev/null +++ b/tests/test_infrastructure.py @@ -0,0 +1,109 @@ +"""Test infrastructure validation tests.""" + +import pytest +import sys +from pathlib import Path + +# Mark this module to be excluded from coverage +# pragma: no cover + + +def test_python_version(): + """Test that Python version meets requirements.""" + assert sys.version_info >= (3, 8), "Python 3.8+ is required" + + +def test_syzscope_package_importable(): + """Test that syzscope package is importable.""" + try: + import syzscope + assert hasattr(syzscope, '__init__') + except ImportError: + pytest.fail("syzscope package should be importable") + + +def test_project_structure(): + """Test that expected project directories exist.""" + project_root = Path(__file__).parent.parent + + expected_dirs = [ + "syzscope", + "tests", + "tests/unit", + "tests/integration" + ] + + for dir_name in expected_dirs: + dir_path = project_root / dir_name + assert dir_path.exists(), f"Directory {dir_name} should exist" + assert dir_path.is_dir(), f"{dir_name} should be a directory" + + +def test_required_files_exist(): + """Test that required configuration files exist.""" + project_root = Path(__file__).parent.parent + + required_files = [ + "pyproject.toml", + "requirements.txt", + "README.md", + ] + + for file_name in required_files: + file_path = project_root / file_name + assert file_path.exists(), f"File {file_name} should exist" + assert file_path.is_file(), f"{file_name} should be a file" + + +def test_fixtures_available(temp_dir, mock_config, mock_vm_instance): + """Test that pytest fixtures are working correctly.""" + # Test temp_dir fixture + assert temp_dir.exists() + assert temp_dir.is_dir() + + # Test mock_config fixture + assert isinstance(mock_config, dict) + assert "debug" in mock_config + assert "timeout" in mock_config + + # Test mock_vm_instance fixture + assert hasattr(mock_vm_instance, 'start') + assert hasattr(mock_vm_instance, 'stop') + assert hasattr(mock_vm_instance, 'is_running') + + +@pytest.mark.unit +def test_unit_test_marker(): + """Test that unit test marker works.""" + assert True + + +@pytest.mark.integration +def test_integration_test_marker(): + """Test that integration test marker works.""" + assert True + + +@pytest.mark.slow +def test_slow_test_marker(): + """Test that slow test marker works.""" + assert True + + +def test_pytest_configuration(): + """Test that pytest is configured correctly.""" + # This test will only pass if pytest is running with proper configuration + # The fact that it runs at all means pytest found the configuration + import pytest + + # Check that pytest-cov is available + try: + import pytest_cov + except ImportError: + pytest.fail("pytest-cov should be available") + + # Check that pytest-mock is available + try: + import pytest_mock + except ImportError: + pytest.fail("pytest-mock should be available") \ No newline at end of file diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..07c9273 --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1 @@ +# Unit tests package \ No newline at end of file