Skip to content

Commit

Permalink
Migrate test command (#15762)
Browse files Browse the repository at this point in the history
* Migrate test command

* address
  • Loading branch information
ofek authored Sep 6, 2023
1 parent aafc898 commit 8ca9fa2
Show file tree
Hide file tree
Showing 16 changed files with 1,346 additions and 87 deletions.
2 changes: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ omit =
../datadog_checks_dev/datadog_checks/dev/tooling/utils.py

[report]
show_missing = ${DDEV_COV_MISSING}
show_missing = True
ignore_errors = True

exclude_lines =
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/test-target.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ jobs:

env:
PYTHON_VERSION: "${{ inputs.python-version || '3.9' }}"
PYTHON_FILTER: "${{ (inputs.test-py2 && !inputs.test-py3) && '2.7' || (!inputs.test-py2 && inputs.test-py3) && '3.9' || '' }}"
SKIP_ENV_NAME: "${{ (inputs.test-py2 && !inputs.test-py3) && 'py3.*' || (!inputs.test-py2 && inputs.test-py3) && 'py2.*' || '' }}"
# Windows E2E requires Windows containers
DDEV_E2E_AGENT: "${{ inputs.platform == 'windows' && (inputs.agent-image-windows || 'datadog/agent-dev:master-py3-win-servercore') || inputs.agent-image }}"
Expand Down Expand Up @@ -164,6 +165,10 @@ jobs:
ddev config set repos.${{ inputs.repo }} .
ddev config set repo ${{ inputs.repo }}
- name: Lint
# TODO: use the more descriptive `--lint` variant when ddev is released
run: ddev test -s ${{ inputs.target }}

- name: Prepare for testing
env: >-
${{ fromJson(inputs.setup-env-vars || format(
Expand Down
4 changes: 4 additions & 0 deletions ddev/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

* Remove `release agent requirements` subcommand ([#15621](https://github.com/DataDog/integrations-core/pull/15621))

***Added***:

* Migrate test command ([#15762](https://github.com/DataDog/integrations-core/pull/15762))

***Fixed***:

* Bump datadog-checks-dev version to ~=24.0 ([#15683](https://github.com/DataDog/integrations-core/pull/15683))
Expand Down
1 change: 1 addition & 0 deletions ddev/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ classifiers = [
]
dependencies = [
"click~=8.1.6",
"coverage",
"datadog-checks-dev[cli]~=24.0",
"hatch>=1.6.3",
"httpx",
Expand Down
2 changes: 1 addition & 1 deletion ddev/src/ddev/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from datadog_checks.dev.tooling.commands.create import create
from datadog_checks.dev.tooling.commands.dep import dep
from datadog_checks.dev.tooling.commands.run import run
from datadog_checks.dev.tooling.commands.test import test

from ddev._version import __version__
from ddev.cli.application import Application
Expand All @@ -20,6 +19,7 @@
from ddev.cli.meta import meta
from ddev.cli.release import release
from ddev.cli.status import status
from ddev.cli.test import test
from ddev.cli.validate import validate
from ddev.config.constants import AppEnvVars, ConfigEnvVars
from ddev.plugin import specs
Expand Down
234 changes: 234 additions & 0 deletions ddev/src/ddev/cli/test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
# (C) Datadog, Inc. 2023-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
from __future__ import annotations

from typing import TYPE_CHECKING

import click

if TYPE_CHECKING:
from ddev.cli.application import Application
from ddev.integration.core import Integration
from ddev.utils.fs import Path


def fix_coverage_report(report_file: Path):
target_dir = report_file.parent.name
report = report_file.read_bytes()

# Make every target's `tests` directory path unique so they don't get combined in UI
report = report.replace(b'"tests/', f'"{target_dir}/tests/'.encode('utf-8'))

report_file.write_bytes(report)


@click.command(short_help='Run tests')
@click.argument('target_spec', required=False)
@click.argument('args', nargs=-1)
@click.option('--lint', '-s', is_flag=True, help='Run only lint & style checks')
@click.option('--fmt', '-fs', is_flag=True, help='Run only the code formatter')
@click.option('--bench', '-b', is_flag=True, help='Run only benchmarks')
@click.option('--latest', is_flag=True, help='Only verify support of new product versions')
@click.option('--cov', '-c', 'coverage', is_flag=True, help='Measure code coverage')
@click.option('--compat', is_flag=True, help='Check compatibility with the minimum allowed Agent version')
@click.option('--ddtrace', is_flag=True, envvar='DDEV_TEST_ENABLE_TRACING', help='Enable tracing during test execution')
@click.option('--memray', is_flag=True, help='Measure memory usage during test execution')
@click.option('--recreate', '-r', is_flag=True, help='Recreate environments from scratch')
@click.option('--list', '-l', 'list_envs', is_flag=True, help='Show available test environments')
@click.option('--python-filter', envvar='PYTHON_FILTER', hidden=True)
@click.option('--junit', is_flag=True, hidden=True)
@click.option('--e2e', is_flag=True, hidden=True)
@click.pass_obj
def test(
app: Application,
target_spec: str | None,
args: tuple[str, ...],
lint: bool,
fmt: bool,
bench: bool,
latest: bool,
coverage: bool,
compat: bool,
ddtrace: bool,
memray: bool,
recreate: bool,
list_envs: bool,
python_filter: str | None,
junit: bool,
e2e: bool,
):
"""
Run tests.
"""
import json
import os
import sys

from ddev.repo.constants import PYTHON_VERSION
from ddev.testing.constants import EndToEndEnvVars, TestEnvVars
from ddev.utils.ci import running_in_ci

if target_spec is None:
target_spec = 'changed'

target_name, _, environments = target_spec.partition(':')

# target name -> target
targets: dict[str, Integration] = {}
if target_name == 'changed':
for integration in app.repo.integrations.iter_changed():
if integration.is_testable:
targets[integration.name] = integration
else:
try:
integration = app.repo.integrations.get(target_name)
except OSError:
app.abort(f'Unknown target: {target_name}')

if integration.is_testable:
targets[integration.name] = integration

if not targets:
app.abort('No testable targets found')

if list_envs:
multiple_targets = len(targets) > 1
for target in targets.values():
with target.path.as_cwd():
if multiple_targets:
app.display_header(target.display_name)

app.platform.check_command([sys.executable, '-m', 'hatch', 'env', 'show'])

return

global_env_vars: dict[str, str] = {}

hatch_verbosity = app.verbosity + 1
if hatch_verbosity > 0:
global_env_vars['HATCH_VERBOSE'] = str(hatch_verbosity)
elif hatch_verbosity < 0:
global_env_vars['HATCH_QUIET'] = str(abs(hatch_verbosity))

api_key = app.config.org.config.get('api_key')
if api_key and not (lint or fmt):
global_env_vars['DD_API_KEY'] = api_key

# Only enable certain functionality when running standard tests
standard_tests = not (lint or fmt or bench or latest)

# Keep track of environments so that they can first be removed if requested
chosen_environments = []

base_command = [sys.executable, '-m', 'hatch', 'env', 'run']
if environments and not standard_tests:
app.abort('Cannot specify environments when using specific functionality like linting')
elif lint:
chosen_environments.append('lint')
base_command.extend(('--env', 'lint', '--', 'all'))
elif fmt:
chosen_environments.append('lint')
base_command.extend(('--env', 'lint', '--', 'fmt'))
elif bench:
filter_data = json.dumps({'benchmark-env': True})
base_command.extend(('--filter', filter_data, '--', 'benchmark'))
elif latest:
filter_data = json.dumps({'latest-env': True})
base_command.extend(('--filter', filter_data, '--', 'test', '--run-latest-metrics'))
else:
if environments:
for env_name in environments.split(','):
chosen_environments.append(env_name)
base_command.extend(('--env', env_name))
else:
chosen_environments.append('default')
base_command.append('--ignore-compat')

if python_filter:
filter_data = json.dumps({'python': python_filter})
base_command.extend(('--filter', filter_data))

base_command.extend(('--', 'test-cov' if coverage else 'test'))

if app.verbosity <= 0:
base_command.extend(('--tb', 'short'))

if memray:
if app.platform.windows:
app.abort('Memory profiling with `memray` is not supported on Windows')

base_command.append('--memray')

if e2e:
base_command.extend(('-m', 'e2e'))
global_env_vars[EndToEndEnvVars.PARENT_PYTHON] = sys.executable

app.display_debug(f'Targets: {", ".join(targets)}')
for target in targets.values():
app.display_header(target.display_name)

command = base_command.copy()
env_vars = global_env_vars.copy()

if standard_tests:
if ddtrace and (target.is_integration or target.name == 'datadog_checks_base'):
# TODO: remove this once we drop Python 2
if app.platform.windows and (
(python_filter and python_filter != PYTHON_VERSION)
or not all(env_name.startswith('py3') for env_name in chosen_environments)
):
app.display_warning('Tracing is only supported on Python 3 on Windows')
else:
command.append('--ddtrace')
env_vars['DDEV_TRACE_ENABLED'] = 'true'
env_vars['DD_PROFILING_ENABLED'] = 'true'
env_vars['DD_SERVICE'] = os.environ.get('DD_SERVICE', 'ddev-integrations')
env_vars['DD_ENV'] = os.environ.get('DD_ENV', 'ddev-integrations')

if junit:
# In order to handle multiple environments the report files must contain the environment name.
# Hatch injects the `HATCH_ENV_ACTIVE` environment variable, see:
# https://hatch.pypa.io/latest/plugins/environment/reference/#hatch.env.plugin.interface.EnvironmentInterface.get_env_vars
command.extend(('--junit-xml', f'.junit/test-{"e2e" if e2e else "unit"}-$HATCH_ENV_ACTIVE.xml'))
# Test results class prefix
command.extend(('--junit-prefix', target.name))

if (
compat
and target.is_package
and target.is_integration
and target.minimum_base_package_version is not None
):
env_vars[TestEnvVars.BASE_PACKAGE_VERSION] = target.minimum_base_package_version

command.extend(args)

with target.path.as_cwd(env_vars=env_vars):
app.display_debug(f'Command: {command}')

if recreate:
if bench or latest:
variable = 'benchmark-env' if bench else 'latest-env'
env_data = json.loads(
app.platform.check_command_output([sys.executable, '-m', 'hatch', 'env', 'show', '--json'])
)
for env_name, env_config in env_data.items():
if env_config.get(variable, False):
app.platform.check_command([sys.executable, '-m', 'hatch', 'env', 'remove', env_name])
else:
for env_name in chosen_environments:
app.platform.check_command([sys.executable, '-m', 'hatch', 'env', 'remove', env_name])

app.platform.check_command(command)
if standard_tests and coverage:
app.display_header('Coverage report')
app.platform.check_command([sys.executable, '-m', 'coverage', 'report', '--rcfile=../.coveragerc'])

if running_in_ci():
app.platform.check_command(
[sys.executable, '-m', 'coverage', 'xml', '-i', '--rcfile=../.coveragerc']
)
fix_coverage_report(target.path / 'coverage.xml')
else:
(target.path / '.coverage').remove()
32 changes: 31 additions & 1 deletion ddev/src/ddev/integration/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ def normalized_display_name(self) -> str:
normalized_integration = normalized_integration.strip("_")
return normalized_integration.lower()

@cached_property
def project_file(self) -> Path:
return self.path / 'pyproject.toml'

@cached_property
def metrics_file(self) -> Path:
relative_path = self.manifest.get('/assets/integration/metrics/metadata_path', 'metadata.csv')
Expand All @@ -109,6 +113,32 @@ def config_spec(self) -> Path:
relative_path = self.manifest.get('/assets/integration/configuration/spec', 'assets/configuration/spec.yaml')
return self.path / relative_path

@cached_property
def minimum_base_package_version(self) -> str | None:
from packaging.requirements import Requirement
from packaging.utils import canonicalize_name

from ddev.utils.toml import load_toml_data

data = load_toml_data(self.project_file.read_text())
for entry in data['project'].get('dependencies', []):
dep = Requirement(entry)
if canonicalize_name(dep.name) == 'datadog-checks-base':
if dep.specifier:
specifier = str(sorted(dep.specifier, key=str)[-1])

version_index = 0
for i, c in enumerate(specifier):
if c.isdigit():
version_index = i
break

return specifier[version_index:]
else:
return None

return None

@cached_property
def is_valid(self) -> bool:
return self.is_integration or self.is_package
Expand All @@ -123,7 +153,7 @@ def has_metrics(self) -> bool:

@cached_property
def is_package(self) -> bool:
return (self.path / 'pyproject.toml').is_file()
return self.project_file.is_file()

@cached_property
def is_tile(self) -> bool:
Expand Down
Loading

0 comments on commit 8ca9fa2

Please sign in to comment.