From 57aa70ecbce5e1820eb160c67c5e141dbe7eff11 Mon Sep 17 00:00:00 2001 From: Ivan Fernandez Calvo Date: Fri, 16 Feb 2024 18:02:35 +0100 Subject: [PATCH] feat: update build system (#15) * feat: update build system * chore: linting * feat: update CI --- .editorconfig | 24 ----- .github/workflows/cd.yml | 44 +++++++-- .github/workflows/ci.yml | 8 +- .gitignore | 3 + Makefile | 91 ++++++------------- mypy.ini | 18 ---- pyproject.toml | 131 +++++++++++++++++++++++++-- setup.cfg | 103 --------------------- src/pytest_otel/__init__.py | 51 ++++++----- tests/it/conftest.py | 4 +- tests/it/test_basic_plugin.py | 3 +- tests/it/test_failure_code_plugin.py | 3 +- tests/it/test_failure_plugin.py | 3 +- tests/it/test_skip_plugin.py | 3 +- tests/it/test_success_plugin.py | 3 +- tests/it/test_xfail_no_run_plugin.py | 3 +- tests/it/test_xfail_plugin.py | 3 +- tests/it/utils/__init__.py | 43 +++++---- tests/test_pytest_otel.py | 23 +++-- tox.ini | 24 ----- 20 files changed, 274 insertions(+), 314 deletions(-) delete mode 100644 mypy.ini delete mode 100644 setup.cfg delete mode 100644 tox.ini diff --git a/.editorconfig b/.editorconfig index 912f5aa..892edf6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,30 +12,6 @@ insert_final_newline = true [*.asciidoc] trim_trailing_whitespace = false -[Jenkinsfile] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.groovy] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.dsl] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - [{Makefile,**.mk}] # Use tabs for indentation (Makefiles require tabs) indent_style = tab diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index b71159b..b3aa038 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -24,13 +24,15 @@ jobs: - uses: actions/setup-python@v5 with: python-version-file: .python-version - cache: 'pip' - cache-dependency-path: requirements.txt + cache-dependency-path: pyproject.toml - name: Lint run: make lint - build: + test-python: runs-on: ubuntu-latest needs: lint + strategy: + matrix: + python-version: [3.8, 3.9, 3.10, 3.11, 3.12] steps: - name: Checkout repository uses: actions/checkout@v4 @@ -40,16 +42,15 @@ jobs: ref: ${{ github.sha || 'main' }} - uses: actions/setup-python@v5 with: - python-version-file: .python-version - cache: 'pip' - cache-dependency-path: requirements.txt + python-version-file: ${{ matrix.python-version }} + cache-dependency-path: pyproject.toml - name: test run: make test - name: it-test run: make it-test release: runs-on: ubuntu-latest - needs: [lint, build] + needs: [lint, test-python] permissions: # write permission is required to create a github release contents: write @@ -60,16 +61,39 @@ jobs: fetch-depth: 0 fetch-tags: false ref: ${{ github.sha || 'main' }} + - uses: actions/setup-python@v5 + with: + python-version-file: .python-version + cache-dependency-path: pyproject.toml + + - name: Check version change + uses: actions/github-script@v7 + id: check_version + with: + script: | + const fs = require('fs'); + const version = fs.readFileSync('src/pytest_otel/__init__.py', 'utf8').match(/__version__ = (.*)/)[1].trim(); + const { data: latestRelease } = await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + }) + + if (latestRelease.tag_name === version) { + core.setOutput('release', 'false' ); + } else { + core.setOutput('release', 'true' ); + core.setOutput('version', version ); + } - name: Release + if: ${{ steps.check_version.outputs.release == 'true'}} id: release - run: | - make publish - grep "version = " setup.cfg | tr -d " " >> ${GITHUB_OUTPUT} + run: make publish env: TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} REPO_URL: ${{ secrets.REPO_URL }} - uses: release-drafter/release-drafter@v6 + if: ${{ steps.check_version.outputs.release == 'true'}} with: version: ${{ steps.release.outputs.version }} publish: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32c861d..abfc64f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,11 +21,10 @@ jobs: - uses: actions/setup-python@v5 with: python-version-file: .python-version - cache: 'pip' - cache-dependency-path: requirements.txt + cache-dependency-path: pyproject.toml - name: Lint run: make lint - build: + test: runs-on: ubuntu-latest needs: lint steps: @@ -38,8 +37,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version-file: .python-version - cache: 'pip' - cache-dependency-path: requirements.txt + cache-dependency-path: pyproject.toml - name: test run: make test - name: it-test diff --git a/.gitignore b/.gitignore index e653045..2e0a3d3 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ junit-test_*.xml .mypy_cache .pytest_cache tests/tests.json +dist +.coverage.* +.coverage diff --git a/Makefile b/Makefile index c4045d9..eca0dea 100644 --- a/Makefile +++ b/Makefile @@ -6,33 +6,11 @@ OTEL_SERVICE_NAME ?= "pytest_otel_test" OTEL_EXPORTER_OTLP_INSECURE ?= True OTEL_EXPORTER_OTLP_HEADERS ?= TRACEPARENT ?= -REPO_URL ?= https://upload.pypi.org/legacy/ DEMO_DIR ?= docs/demos VENV ?= .venv PYTHON ?= python3 PIP ?= pip3 -GH_VERSION = 1.0.0 - -UNAME_S := $(shell uname -s) -ifeq ($(UNAME_S),Linux) - OS_FLAG := linux -endif -ifeq ($(UNAME_S),Darwin) - OS_FLAG := macOS -endif -UNAME_P := $(shell uname -m) -ifeq ($(UNAME_P),x86_64) - ARCH_FLAG := amd64 -endif -ifneq ($(filter %86,$(UNAME_P)),) - ARCH_FLAG := i386 -endif -GH_BINARY = gh_$(GH_VERSION)_$(OS_FLAG)_$(ARCH_FLAG) -GH = $(CURDIR)/bin/gh - -export UID=$(shell id -u) -export GID=$(shell id -g) SHELL = /bin/bash .SILENT: @@ -43,55 +21,44 @@ help: @echo "" @grep '^## @help' Makefile|cut -d ":" -f 2-3|( (sort|column -s ":" -t) || (sort|tr ":" "\t") || (tr ":" "\t")) -bin: - mkdir bin - -bin/gh: bin - curl -sSfL https://github.com/cli/cli/releases/download/v$(GH_VERSION)/$(GH_BINARY).tar.gz|tar xz - mv $(GH_BINARY)/bin/gh bin/gh - rm -fr $(GH_BINARY) - ## @help:virtualenv:Create a Python virtual environment. .PHONY: virtualenv virtualenv: $(PYTHON) --version test -d $(VENV) || $(PYTHON) -m venv $(VENV);\ - source $(VENV)/bin/activate;\ - $(PIP) install -q -r requirements.txt; - -## @help:install:Install APM CLI in a Python virtual environment. -.PHONY: install -install: virtualenv - source $(VENV)/bin/activate;\ - $(PIP) install .; + source $(VENV)/bin/activate; \ + $(PIP) install ".[test]"; ## @help:test:Run the test. .PHONY: test -test: virtualenv install +test: virtualenv source $(VENV)/bin/activate;\ - pytest --capture=no -p pytester --runpytest=subprocess \ + $(PYTHON) -m pytest --capture=no -p pytester --runpytest=subprocess \ --junitxml $(CURDIR)/junit-test_pytest_otel.xml \ tests/test_pytest_otel.py; ## @help:test:Run the test. .PHONY: test -it-test: virtualenv install +it-test: virtualenv set -e;\ source $(VENV)/bin/activate;\ for test in tests/it/test_*.py; \ do \ - pytest --capture=no -p pytester --runpytest=subprocess \ + $(PYTHON) -m pytest --capture=no -p pytester --runpytest=subprocess \ --junitxml $(CURDIR)/junit-$$(basename $${test}).xml \ $${test}; \ done; - #pytest --capture=no -p pytester --runpytest=subprocess tests/it/test_*.py; -## @help:coverage:Report coverage. -.PHONY: coverage -coverage: virtualenv +## @help:format:Format the code. +format: virtualenv + source $(VENV)/bin/activate;\ + $(PYTHON) -m black src/pytest_otel tests; + +## @help:test-coverage:Report coverage. +.PHONY: test-coverage +test-coverage: virtualenv source $(VENV)/bin/activate;\ - coverage run --source=otel -m pytest; \ - coverage report -m; + pytest --cov=pytest_otel --capture=no -p pytester --runpytest=subprocess tests/test_pytest_otel.py; ## @precomit:pre-commit:Run precommit hooks. lint: virtualenv @@ -107,13 +74,12 @@ clean: @find . -type f -name "*.py[co]" -delete @find . -type d -name "__pycache__" -delete @find . -name '*~' -delete - -@rm -fr src/pytest_otel.egg-info *.egg-info build dist $(VENV) bin .tox .mypy_cache .pytest_cache otel-traces-file-output.json test_spans.json temp junit-*.xml + -@rm -fr src/pytest_otel.egg-info *.egg-info build dist $(VENV) bin .tox .mypy_cache .pytest_cache otel-traces-file-output.json test_spans.json temp junit-*.xml .coverage* -package: virtualenv +## @help:build:Build the Python project package. +build: virtualenv source $(VENV)/bin/activate;\ - set +xe; \ - pip install wheel; \ - $(PYTHON) setup.py sdist bdist_wheel + $(PYTHON) -m build ## @help:run-otel-collector:Run OpenTelemetry collector in debug mode. .PHONY: run-otel-collector @@ -125,23 +91,20 @@ run-otel-collector: --name otelcol otel/opentelemetry-collector \ --config otel-config.yaml; \ -#https://upload.pypi.org/legacy/ -#https://test.pypi.org/legacy/ -#secret/observability-team/ci/apm-agent-python-pypi-test -#secret/observability-team/ci/apm-agent-python-pypi-prod +# https://upload.pypi.org/legacy/ +# https://test.pypi.org/legacy/ ## @help:publish REPO_URL=${REPO_URL} TWINE_USER=${TWINE_USER} TWINE_PASSWORD=${TWINE_PASSWORD}:Publish the Python project in a PyPI repository. .PHONY: publish -publish: package +publish: build set +xe; \ source $(VENV)/bin/activate;\ - $(PYTHON) -m pip install twine;\ - echo "Uploading to $${REPO_URL} with user $${TWINE_USER}";\ - python -m twine upload --username "$${TWINE_USER}" --password "$${TWINE_PASSWORD}" --skip-existing --repository-url $${REPO_URL} dist/*.tar.gz;\ - python -m twine upload --username "$${TWINE_USER}" --password "$${TWINE_PASSWORD}" --skip-existing --repository-url $${REPO_URL} dist/*.whl + echo "Uploading to $${REPO_URL}";\ + $(PYTHON) -m twine upload --username "$${TWINE_USER}" --password "$${TWINE_PASSWORD}" --skip-existing --repository-url $${REPO_URL} dist/*.tar.gz;\ + $(PYTHON) -m twine upload --username "$${TWINE_USER}" --password "$${TWINE_PASSWORD}" --skip-existing --repository-url $${REPO_URL} dist/*.whl ## @help:demo-start-DEMO_NAME:Starts the demo from the demo folder, DEMO_NAME is the name of the demo type folder in the docs/demos folder (jaeger, elastic). .PHONY: demo-start-% -demo-start-%: virtualenv install +demo-start-%: virtualenv $(MAKE) demo-stop-$* mkdir -p $(DEMO_DIR)/$*/build touch $(DEMO_DIR)/$*/build/tests.json @@ -149,7 +112,7 @@ demo-start-%: virtualenv install . $(DEMO_DIR)/$*/demo.env;\ env | grep OTEL;\ source $(VENV)/bin/activate;\ - pytest --capture=no docs/demos/test/test_demo.py || echo "Demo execution finished you can access to http://localhost:5601 to check the traces, the user is 'admin' and the password 'changeme'"; + $(PYTHON) -m pytest --capture=no docs/demos/test/test_demo.py || echo "Demo execution finished you can access to http://localhost:5601 to check the traces, the user is 'admin' and the password 'changeme'"; ## @help:demo-stop-DEMO_NAME:Stops the demo from the demo folder, DEMO_NAME is the name of the demo type folder in the docs/demos folder (jaeger, elastic). .PHONY: demo-stop-% diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index fa83284..0000000 --- a/mypy.ini +++ /dev/null @@ -1,18 +0,0 @@ -[mypy] -python_version = 3.8 -allow_untyped_defs = True -allow_untyped_calls = True -disallow_any_generics = True -disallow_subclassing_any = True -disallow_incomplete_defs = True -disallow_untyped_decorators = True -show_error_codes = True -no_implicit_optional = True -warn_redundant_casts = True -warn_unused_ignores = False -warn_no_return = True -warn_return_any = True -implicit_reexport = False -strict_equality = True -warn_unused_configs = True -pretty = True diff --git a/pyproject.toml b/pyproject.toml index db26f99..95af0cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,92 @@ +[build-system] +requires = ["setuptools >= 61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "pytest_otel" +dynamic = ["version"] +description = "OpenTelemetry plugin for Pytest" +readme = {file = "README.md", content-type = "text/markdown"} +authors = [ + {name = "Ivan Fernandez Calvo (kuisathaverat)"} +] dependencies = [ - "opentelemetry-api==1.15.0", - "opentelemetry-exporter-otlp==1.15.0", - "opentelemetry-sdk==1.15.0", - "pytest==7.1.3", + "opentelemetry-api==1.15.0", + "opentelemetry-exporter-otlp==1.15.0", + "opentelemetry-sdk==1.15.0", + "pytest==7.1.3", +] +requires-python = ">= 3.8" +license = {file = "LICENSE"} +keywords = ["pytest", "otel", "opentelemetry", "debug"] +classifiers = [ + "Environment :: Plugins", + "Framework :: Pytest", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Testing", + "Topic :: Utilities", ] +[project_urls] +Homepage = "https://github.com/kuisathaverat/pytest_otel" +Documentation = "https://github.com/kuisathaverat/pytest_otel/blob/main/README.md" +Repository = "https://github.com/kuisathaverat/pytest_otel.git" +Issues = "https://github.com/kuisathaverat/pytest_otel/issues" +Changelog = "https://github.com/kuisathaverat/pytest_otel/releases" -[build-system] -requires = ["setuptools >= 44.0.0", "wheel >= 0.30"] -build-backend = "setuptools.build_meta" +[project.optional-dependencies] +test = [ + "pytest-cov==4.1.0", + "pre-commit==3.6.1", + "pytest-docker==3.1.1", + "mypy==1.8.0", + "black==24.2.0", + "flake8==7.0.0", + "build==1.0.3", + "twine==5.0.0", +] + +[tool.setuptools.dynamic] +version = {attr = "pytest_otel.__version__"} + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +pytest_otel = ["py.typed"] + +[project.entry-points.pytest11] +pytest_otel = "pytest_otel" + +[tool.pytest] +addopts = "-ra --showlocals -vv" +testpaths = "tests" +xfail_strict = true +junit_family = "xunit2" + +[tool.distutils.bdist_wheel] +universal = true + +[tool.flake8] +max-line-length = "120" +ignore = [ + "F401", "H301", "E203", "SC200", "SC100", "W503", +] +exclude = [ + ".venv", + ".git", + "__pycache__", + ".tox", + ".mypy_cache", +] [tool.black] line-length = 120 @@ -24,3 +103,41 @@ exclude = ''' | tests/data )/ ''' + +[tool.coverage.run] +source = [ "src", "tests" ] +parallel = true +branch = true +dynamic_context = "test_function" + +[tool.coverage.report] +fail_under = "100" +skip_covered = true +show_missing = true + +[tool.coverage.html] +show_contexts = true +skip_covered = false +skip_empty = false + +[tool.coverage.paths] +source = [ "src" ] + +[tool.mypy] +python_version = "3.8" +allow_untyped_defs = true +allow_untyped_calls = true +disallow_any_generics = true +disallow_subclassing_any = true +disallow_incomplete_defs = true +disallow_untyped_decorators = true +show_error_codes = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = false +warn_no_return = true +warn_return_any = true +implicit_reexport = false +strict_equality = true +warn_unused_configs = true +pretty = true diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index c05c08c..0000000 --- a/setup.cfg +++ /dev/null @@ -1,103 +0,0 @@ -[metadata] -name = pytest_otel -description = pytest-otel report OpenTelemetry traces about test executed -long_description = file: README.md -long_description_content_type = text/markdown -url = https://github.com/kuisathaverat/pytest_otel -maintainer = Ivan Fernandez Calvo -version = 1.4.2 -license = Apache-2.0 -license_file = LICENSE.txt -platforms = any -classifiers = - Environment :: Plugins - Framework :: Pytest - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Operating System :: OS Independent - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Topic :: Software Development :: Libraries - Topic :: Software Development :: Testing - Topic :: Utilities -keywords = pytest, otel, opentelemetry, debug -project_urls = - Source=https://github.com/kuisathaverat/pytest_otel - Tracker=https://github.com/kuisathaverat/pytest_otel/issues - -[options] -packages = find: -install_requires = - opentelemetry-api==1.15.0 - opentelemetry-exporter-otlp==1.15.0 - opentelemetry-sdk==1.15.0 - pytest==7.2.1 -python_requires = >=3.6 -include_package_data = True -package_dir = - =src -zip_safe = True - -[options.packages.find] -where = src - -[options.entry_points] -pytest11 = otel = pytest_otel - -[options.extras_require] -test = - coverage>=5 - -[options.package_data] -pytest_otel = py.typed - -[sdist] -formats = gztar - -[bdist_wheel] -universal = true - -[flake8] -max-line-length = 120 -ignore = F401, H301, E203, SC200, SC100, W503 -exclude = .venv,.git,__pycache__,.tox,.mypy_cache - -[coverage:run] -source = - ${_COVERAGE_SRC} - ${_COVERAGE_TEST} -parallel = True -branch = True -dynamic_context = test_function - -[coverage:report] -fail_under = 100 -skip_covered = true -show_missing = true -omit = - tests/example.py - -[coverage:html] -show_contexts = True -skip_covered = False -skip_empty = False - -[coverage:paths] -source = - src - .tox*/*/lib/python*/site-packages - .tox*/pypy*/site-packages - .tox*\*\Lib\site-packages\ - */src - *\src - -[tool:pytest] -addopts = -ra --showlocals -vv -testpaths = tests -xfail_strict = True -junit_family = xunit2 diff --git a/src/pytest_otel/__init__.py b/src/pytest_otel/__init__.py index 77db6ae..53fee7d 100644 --- a/src/pytest_otel/__init__.py +++ b/src/pytest_otel/__init__.py @@ -24,6 +24,8 @@ # from opentelemetry.ext.otcollector.metrics_exporter import CollectorMetricsExporter # from opentelemetry.sdk.metrics import Counter, MeterProvider +__version__ = "1.4.3" + LOGGER = logging.getLogger("pytest_otel") service_name = None traceparent = None @@ -42,6 +44,7 @@ # total_counter = None # controller = None + def pytest_addoption(parser): """Init command line arguments""" group = parser.getgroup("pytest-otel", "report OpenTelemetry traces for tests executed.") @@ -92,6 +95,7 @@ def pytest_addoption(parser): help="", ) + def init_otel(): """Init the OpenTelemetry settings""" global tracer, session_name, service_name, insecure, otel_exporter, errors_counter, failed_counter, skipped_counter, total_counter, controller # noqa: E501 @@ -147,8 +151,7 @@ def start_span(span_name, context=None, kind=None): """Starts a span with the name, context, and kind passed as parameters""" global tracer, spans spans[span_name] = tracer.start_span( - span_name, context=context, record_exception=True, set_status_on_exception=True, - kind=kind + span_name, context=context, record_exception=True, set_status_on_exception=True, kind=kind ) LOGGER.debug("The {} transaction start_span.".format(span_name)) return spans[span_name] @@ -169,16 +172,18 @@ def convertOutcome(outcome): """Convert from pytest outcome to OpenTelemetry status code""" if outcome == "passed": return Status(status_code=StatusCode.OK) - elif (outcome == "failed" - or outcome == "interrupted" - or outcome == "internal_error" - or outcome == "usage_error" - or outcome == "no_tests_collected" - ): + elif ( + outcome == "failed" + or outcome == "interrupted" + or outcome == "internal_error" + or outcome == "usage_error" + or outcome == "no_tests_collected" + ): return Status(status_code=StatusCode.ERROR) else: return Status(status_code=StatusCode.UNSET) + # def update_metrics(outcome): # """Update the metrics with the test result""" # if (outcome == "interrupted" @@ -210,6 +215,7 @@ def exitCodeToOutcome(exit_code): else: return "failed" + def traceparent_context(traceparent): """Extracts the trace context from the TRACEPARENT passed""" carrier = {} @@ -237,7 +243,7 @@ def pytest_sessionstart(session): if service_name is not None: os.environ["OTEL_SERVICE_NAME"] = service_name if insecure: - os.environ["OTEL_EXPORTER_OTLP_INSECURE"] = f'{insecure}' + os.environ["OTEL_EXPORTER_OTLP_INSECURE"] = f"{insecure}" if traceparent is None: traceparent = os.getenv("TRACEPARENT", None) if len(os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "")) == 0: @@ -277,7 +283,7 @@ def pytest_sessionfinish(session, exitstatus): # noqa: U100 json += "," json += span_list[i].to_json() json += "\n]\n" - with open(otel_span_file_output, 'w', encoding='utf-8') as output: + with open(otel_span_file_output, "w", encoding="utf-8") as output: output.write(json) print(json) @@ -291,7 +297,7 @@ def pytest_runtest_call(item): record_exception=True, set_status_on_exception=True, ) as span: - #total_counter.add(1) + # total_counter.add(1) LOGGER.debug("Test {} starts - {}".format(item.name, span.get_span_context())) span.set_attribute("tests.name", item.name) info = yield @@ -300,14 +306,14 @@ def pytest_runtest_call(item): if hasattr(info, "_excinfo"): if info._excinfo: (info_class, info_msg, info_trace) = info._excinfo - if info_class.__name__ == 'Failed': + if info_class.__name__ == "Failed": outcome = "failed" span.set_attribute("tests.message", "{}".format(info_msg)) if hasattr(sys, "last_value") and hasattr(sys, "last_traceback") and hasattr(sys, "last_type"): longrepr = "" - last_value = getattr(sys, 'last_value') - last_traceback = getattr(sys, 'last_traceback') - last_type = getattr(sys, 'last_type') + last_value = getattr(sys, "last_value") + last_traceback = getattr(sys, "last_traceback") + last_type = getattr(sys, "last_type") if not isinstance(last_value, _pytest._code.ExceptionInfo): outcome = "failed" @@ -323,9 +329,8 @@ def pytest_runtest_call(item): stack_trace = repr(traceback.format_exception(last_type, last_value, last_traceback)) span.set_attribute("tests.error", "{}".format(stack_trace)) - if hasattr(last_value, "args") and len(getattr(last_value, 'args', [])) > 0: - span.set_attribute("tests.message", "{}" - .format(last_value.args[0])) + if hasattr(last_value, "args") and len(getattr(last_value, "args", [])) > 0: + span.set_attribute("tests.message", "{}".format(last_value.args[0])) if longrepr: span.set_attribute("tests.message", "{}".format(longrepr)) @@ -334,15 +339,15 @@ def pytest_runtest_call(item): elif last_type: span.set_attribute("tests.message", "{}".format(last_type)) - skipping = getattr(_pytest, 'skipping', None) + skipping = getattr(_pytest, "skipping", None) if skipping: - key = getattr(skipping, 'xfailed_key', None) + key = getattr(skipping, "xfailed_key", None) xfailed = item._store.get(key, None) - reason = getattr(xfailed, 'reason', None) - if reason : + reason = getattr(xfailed, "reason", None) + if reason: span.set_attribute("tests.message", "{}".format(reason)) - #update_metrics(outcome) + # update_metrics(outcome) status = convertOutcome(outcome) span.set_status(status) span.set_attribute("tests.status", "{}".format(outcome)) diff --git a/tests/it/conftest.py b/tests/it/conftest.py index 42ae34e..1361e6a 100644 --- a/tests/it/conftest.py +++ b/tests/it/conftest.py @@ -8,7 +8,5 @@ def otel_service(docker_ip, docker_services): # `port_for` takes a container port and returns the corresponding host port port = docker_services.port_for("otel-collector", 4317) - docker_services.wait_until_responsive( - timeout=30.0, pause=5, check=lambda: is_portListening(docker_ip, port) - ) + docker_services.wait_until_responsive(timeout=30.0, pause=5, check=lambda: is_portListening(docker_ip, port)) return True diff --git a/tests/it/test_basic_plugin.py b/tests/it/test_basic_plugin.py index 9320191..5380a6d 100644 --- a/tests/it/test_basic_plugin.py +++ b/tests/it/test_basic_plugin.py @@ -23,5 +23,6 @@ def test_basic_plugin(pytester, otel_service): def test_basic(): time.sleep(5) pass -""") +""" + ) assertTest(pytester, "test_basic", "passed", STATUS_CODE_OK, "passed", STATUS_CODE_OK) diff --git a/tests/it/test_failure_code_plugin.py b/tests/it/test_failure_code_plugin.py index 7262078..cea9ea0 100644 --- a/tests/it/test_failure_code_plugin.py +++ b/tests/it/test_failure_code_plugin.py @@ -23,5 +23,6 @@ def test_failure_code_plugin(pytester, otel_service): def test_failure_code(): d = 1/0 pass -""") +""" + ) assertTest(pytester, "test_failure_code", "failed", STATUS_CODE_ERROR, "failed", STATUS_CODE_ERROR) diff --git a/tests/it/test_failure_plugin.py b/tests/it/test_failure_plugin.py index 2acfd51..89da815 100644 --- a/tests/it/test_failure_plugin.py +++ b/tests/it/test_failure_plugin.py @@ -22,5 +22,6 @@ def test_failure_plugin(pytester, otel_service): + """ def test_failure(): assert 1 < 0 -""") +""" + ) assertTest(pytester, "test_failure", "failed", STATUS_CODE_ERROR, "failed", STATUS_CODE_ERROR) diff --git a/tests/it/test_skip_plugin.py b/tests/it/test_skip_plugin.py index ead0967..402c6a4 100644 --- a/tests/it/test_skip_plugin.py +++ b/tests/it/test_skip_plugin.py @@ -23,5 +23,6 @@ def test_skip_plugin(pytester, otel_service): @pytest.mark.skip def test_skip(): assert True -""") +""" + ) assertTest(pytester, None, "passed", STATUS_CODE_OK, None, None) diff --git a/tests/it/test_success_plugin.py b/tests/it/test_success_plugin.py index 047feda..09d1588 100644 --- a/tests/it/test_success_plugin.py +++ b/tests/it/test_success_plugin.py @@ -22,5 +22,6 @@ def test_success_plugin(pytester, otel_service): + """ def test_success(): assert True -""") +""" + ) assertTest(pytester, "test_success", "passed", STATUS_CODE_OK, "passed", STATUS_CODE_OK) diff --git a/tests/it/test_xfail_no_run_plugin.py b/tests/it/test_xfail_no_run_plugin.py index 3151261..b5b5344 100644 --- a/tests/it/test_xfail_no_run_plugin.py +++ b/tests/it/test_xfail_no_run_plugin.py @@ -23,5 +23,6 @@ def test_xfail_no_run_plugin(pytester, otel_service): @pytest.mark.xfail(run=False) def test_xfail_no_run(): assert False -""") +""" + ) assertTest(pytester, None, "passed", STATUS_CODE_OK, None, None) diff --git a/tests/it/test_xfail_plugin.py b/tests/it/test_xfail_plugin.py index 223a980..41076d6 100644 --- a/tests/it/test_xfail_plugin.py +++ b/tests/it/test_xfail_plugin.py @@ -23,5 +23,6 @@ def test_xfail_plugin(pytester, otel_service): @pytest.mark.xfail(reason="foo bug") def test_xfail(): assert False -""") +""" + ) assertTest(pytester, None, "passed", STATUS_CODE_OK, None, None) diff --git a/tests/it/utils/__init__.py b/tests/it/utils/__init__.py index ed1f4de..05fb0c0 100644 --- a/tests/it/utils/__init__.py +++ b/tests/it/utils/__init__.py @@ -36,19 +36,22 @@ def waitForFileContent(filename): """wait for a file has content""" while getSize(filename) < 1: time.sleep(5) - subprocess.check_output(f"docker cp $(docker ps -q --filter expose=4317):/tmp/tests.json {filename}", - stderr=subprocess.STDOUT, shell=True) - with open(filename, encoding='utf-8') as input: + subprocess.check_output( + f"docker cp $(docker ps -q --filter expose=4317):/tmp/tests.json {filename}", + stderr=subprocess.STDOUT, + shell=True, + ) + with open(filename, encoding="utf-8") as input: print(input.read()) def assertAttrKeyValue(attributes, key, value): """check the value of a key in attributes""" - realValue = '' + realValue = "" for attr in attributes: - if attr['key'] == key: + if attr["key"] == key: realValue = attr["value"]["stringValue"] - assert realValue == value, f'attribute {key} is not {value}: {realValue}' + assert realValue == value, f"attribute {key} is not {value}: {realValue}" def assertTestSuit(span, outcome, status): @@ -56,7 +59,7 @@ def assertTestSuit(span, outcome, status): assert span["kind"] == SPAN_KIND_SERVER, f'span kind is not server: {span["kind"]}' assert span["status"]["code"] == status, f'status code is not {status}: {span["status"]["code"]}' if outcome is not None: - assertAttrKeyValue(span["attributes"], 'tests.status', outcome) + assertAttrKeyValue(span["attributes"], "tests.status", outcome) assert len(span["parentSpanId"]) == 0, f'parent span id is not empty: {span["parentSpanId"]}' return True @@ -65,33 +68,37 @@ def assertSpan(span, name, outcome, status): """check attributes of a span""" assert span["kind"] == SPAN_KIND_INTERNAL, f'span kind is not internal: {span["kind"]}' assert span["status"]["code"] == status, f'status code is not {status}: {span["status"]["code"]}' - assertAttrKeyValue(span["attributes"], 'tests.name', name) + assertAttrKeyValue(span["attributes"], "tests.name", name) if outcome is not None: - assertAttrKeyValue(span["attributes"], 'tests.status', outcome) + assertAttrKeyValue(span["attributes"], "tests.status", outcome) assert len(span["parentSpanId"]) > 0, f'parent span id is empty: {span["parentSpanId"]}' return True def assertTest(pytester, name, ts_outcome, ts_status, outcome, status): """check a test results are correct""" - pytester.runpytest("--otel-endpoint=http://127.0.0.1:4317", "--otel-service-name=pytest_otel", "--otel-debug=True", "-rsx") + pytester.runpytest( + "--otel-endpoint=http://127.0.0.1:4317", "--otel-service-name=pytest_otel", "--otel-debug=True", "-rsx" + ) filename = "./tests.json" waitForFileContent(filename) foundTest = False foundTestSuit = False - with open(filename, encoding='utf-8') as input: + with open(filename, encoding="utf-8") as input: spans_output = json.loads(input.readline()) - print(f""" + print( + f""" spans_output {spans_output} resourceSpans {spans_output['resourceSpans']} - """) - for resourceSpan in spans_output['resourceSpans']: - for instrumentationLibrarySpan in resourceSpan['scopeSpans']: - for span in instrumentationLibrarySpan['spans']: + """ + ) + for resourceSpan in spans_output["resourceSpans"]: + for instrumentationLibrarySpan in resourceSpan["scopeSpans"]: + for span in instrumentationLibrarySpan["spans"]: if span["name"] == f"Running {name}": foundTest = assertSpan(span, name, outcome, status) if span["name"] == "Test Suite": foundTestSuit = assertTestSuit(span, ts_outcome, ts_status) - assert foundTest or name is None, f'test {name} not found' - assert foundTestSuit, 'test suit not found' + assert foundTest or name is None, f"test {name} not found" + assert foundTestSuit, "test suit not found" os.remove(filename) diff --git a/tests/test_pytest_otel.py b/tests/test_pytest_otel.py index 394fd10..17196c0 100644 --- a/tests/test_pytest_otel.py +++ b/tests/test_pytest_otel.py @@ -36,7 +36,7 @@ def assertSpan(span, name, outcome, status): def assertTest(pytester, name, ts_outcome, ts_status, outcome, status): pytester.runpytest("--otel-span-file-output=./test_spans.json", "--otel-debug=True", "-rsx") span_list = None - with open("test_spans.json", encoding='utf-8') as input: + with open("test_spans.json", encoding="utf-8") as input: span_list = json.loads(input.read()) foundTest = False foundTestSuit = False @@ -57,7 +57,8 @@ def test_basic_plugin(pytester): def test_basic(): time.sleep(5) pass -""") +""" + ) assertTest(pytester, "test_basic", "passed", "OK", "passed", "OK") @@ -68,7 +69,8 @@ def test_success_plugin(pytester): + """ def test_success(): assert True -""") +""" + ) assertTest(pytester, "test_success", "passed", "OK", "passed", "OK") @@ -79,7 +81,8 @@ def test_failure_plugin(pytester): + """ def test_failure(): assert 1 < 0 -""") +""" + ) assertTest(pytester, "test_failure", "failed", "ERROR", "failed", "ERROR") @@ -91,7 +94,8 @@ def test_failure_code_plugin(pytester): def test_failure_code(): d = 1/0 pass -""") +""" + ) assertTest(pytester, "test_failure_code", "failed", "ERROR", "failed", "ERROR") @@ -103,7 +107,8 @@ def test_skip_plugin(pytester): @pytest.mark.skip def test_skip(): assert True -""") +""" + ) assertTest(pytester, None, "passed", "OK", None, None) @@ -115,7 +120,8 @@ def test_xfail_plugin(pytester): @pytest.mark.xfail(reason="foo bug") def test_xfail(): assert False -""") +""" + ) assertTest(pytester, None, "passed", "OK", None, None) @@ -127,5 +133,6 @@ def test_xfail_no_run_plugin(pytester): @pytest.mark.xfail(run=False) def test_xfail_no_run(): assert False -""") +""" + ) assertTest(pytester, None, "passed", "OK", None, None) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index af44248..0000000 --- a/tox.ini +++ /dev/null @@ -1,24 +0,0 @@ -[tox] -envlist = - py311 - py310 - py39 - py38 - -[testenv] -deps = - pytest==7.1.3 - pytest-docker==1.0.1 -commands = - pytest {tty:--color=yes} --capture=no \ - -p pytester --runpytest=subprocess \ - --junitxml {toxworkdir}{/}junit-{envname}.xml \ - tests/test_pytest_otel.py - -[testenv:linting] -basepython = python3 -skip_install = true -deps = - pre-commit==2.20.0 -commands = - pre-commit run