diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..54c60f98 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +tools/triage/triage binary -diff diff --git a/.tekton/pulp-deploy-and-test.yaml b/.tekton/pulp-deploy-and-test.yaml index 7c21b7d4..31cf6dad 100644 --- a/.tekton/pulp-deploy-and-test.yaml +++ b/.tekton/pulp-deploy-and-test.yaml @@ -797,31 +797,110 @@ spec: cmd_prefix bash -c "HOME=/tmp/home pip3 install -r /tmp/unittest_requirements.txt -r /tmp/functest_requirements.txt" # Because we pass the path to pytest -o cache_dir=/tmp/home/.cache/pytest_cache, pulpcore-manager must be in the same dir cmd_prefix bash -c "ln -s /usr/local/lib/pulp/bin/pulpcore-manager /tmp/home/.local/bin/pulpcore-manager || /bin/true" + + # === triage test impact analysis === + TRIAGE_OK=true + + # Check for [full-test] override in SNAPSHOT + if echo '$(params.SNAPSHOT)' | grep -qi 'full-test'; then + echo "triage: [full-test] marker found, running all tests" + TRIAGE_OK=false + fi + + if [ "$TRIAGE_OK" = "true" ]; then + cmd_prefix bash -c "mkdir -p /tmp/home/.triage && curl -fsS -o /tmp/home/.triage/openapi.json http://localhost:8000/api/pulp/api/v3/docs/api.json" || TRIAGE_OK=false + fi + + if [ "$TRIAGE_OK" = "true" ]; then + cmd_prefix bash -c "HOME=/tmp/home django-admin triage_urls --output /tmp/home/.triage/urls.json" || TRIAGE_OK=false + fi + + if [ "$TRIAGE_OK" = "true" ]; then + cmd_prefix bash -c "HOME=/tmp/home /src/tools/triage/triage discover --project-root /src --python python3" || TRIAGE_OK=false + fi + + if [ "$TRIAGE_OK" = "true" ]; then + cmd_prefix bash -c "HOME=/tmp/home /src/tools/triage/triage map --project-root /src --force" || TRIAGE_OK=false + fi + + if [ "$TRIAGE_OK" = "true" ]; then + cmd_prefix bash -c "HOME=/tmp/home /src/tools/triage/triage select --diff /src/.triage/pr.diff --json --pyargs" > /tmp/select.json || TRIAGE_OK=false + fi + + if [ "$TRIAGE_OK" = "true" ]; then + if ! python3 -c "import json; json.load(open('/tmp/select.json'))" 2>/dev/null; then + echo "triage: invalid select.json, falling back" + TRIAGE_OK=false + fi + fi + + if [ "$TRIAGE_OK" = "false" ]; then + echo "triage: running full test suite" + else + echo "triage: $(python3 -c "import json; d=json.load(open('/tmp/select.json')); print(f'selected {d[\"tests_selected\"]}/{d[\"tests_total\"]} tests, plugins: {d[\"plugins_affected\"]}')")" + fi + # === end triage === + echo "CURL OUTPUT" curl https://env-${NS}.apps.crc-eph.r9lp.p1.openshiftapps.com/api/pulp-content/default/ echo "ROUTES" oc_wrapper get route set +e # Only testing test_download_content because it is a very thorough test that tests that all the components of pulp can work - cmd_prefix bash -c "HOME=/tmp/home PYTHONPATH=/tmp/home/.local/lib/python3.11/site-packages/ XDG_CONFIG_HOME=/tmp/home/.config API_PROTOCOL=http API_HOST=pulp-api API_PORT=8000 ADMIN_USERNAME=admin ADMIN_PASSWORD=$PASSWORD /tmp/home/.local/bin/pytest -o cache_dir=/tmp/home/.cache/pytest_cache -v -r sx --color=yes --suppress-no-test-exit-code --pyargs pulp_rpm.tests.functional -m parallel -n 8 -k 'test_download_content' --junitxml=/tmp/home/junit-pulp-parallel.xml" || debug_and_fail + if [ "$TRIAGE_OK" = "false" ] || python3 -c "import json,sys; sys.exit(0 if 'pulp_rpm' in json.load(open('/tmp/select.json')).get('plugins_affected',[]) else 1)" 2>/dev/null; then + cmd_prefix bash -c "HOME=/tmp/home PYTHONPATH=/tmp/home/.local/lib/python3.11/site-packages/ XDG_CONFIG_HOME=/tmp/home/.config API_PROTOCOL=http API_HOST=pulp-api API_PORT=8000 ADMIN_USERNAME=admin ADMIN_PASSWORD=$PASSWORD /tmp/home/.local/bin/pytest -o cache_dir=/tmp/home/.cache/pytest_cache -v -r sx --color=yes --suppress-no-test-exit-code --pyargs pulp_rpm.tests.functional -m parallel -n 8 -k 'test_download_content' --junitxml=/tmp/home/junit-pulp-parallel.xml" || debug_and_fail + else + echo "triage: skipping pulp_rpm tests (plugin not affected)" + fi # Never test test_package_manager_consume because they require sudo # Do not test test_domain_create because it requires more than 2GB of RAM # Only testing test_download_policies because they are very thorough tests that test that all the components of pulp can work - cmd_prefix bash -c "HOME=/tmp/home PYTHONPATH=/tmp/home/.local/lib/python3.11/site-packages/ XDG_CONFIG_HOME=/tmp/home/.config API_PROTOCOL=http API_HOST=pulp-api API_PORT=8000 ADMIN_USERNAME=admin ADMIN_PASSWORD=$PASSWORD /tmp/home/.local/bin/pytest -o cache_dir=/tmp/home/.cache/pytest_cache -v -r sx --color=yes --pyargs pulp_rpm.tests.functional -m 'not parallel' -k 'test_download_policies' --junitxml=/tmp/home/junit-pulp-serial.xml" || debug_and_fail + if [ "$TRIAGE_OK" = "false" ] || python3 -c "import json,sys; sys.exit(0 if 'pulp_rpm' in json.load(open('/tmp/select.json')).get('plugins_affected',[]) else 1)" 2>/dev/null; then + cmd_prefix bash -c "HOME=/tmp/home PYTHONPATH=/tmp/home/.local/lib/python3.11/site-packages/ XDG_CONFIG_HOME=/tmp/home/.config API_PROTOCOL=http API_HOST=pulp-api API_PORT=8000 ADMIN_USERNAME=admin ADMIN_PASSWORD=$PASSWORD /tmp/home/.local/bin/pytest -o cache_dir=/tmp/home/.cache/pytest_cache -v -r sx --color=yes --pyargs pulp_rpm.tests.functional -m 'not parallel' -k 'test_download_policies' --junitxml=/tmp/home/junit-pulp-serial.xml" || debug_and_fail + else + echo "triage: skipping pulp_rpm tests (plugin not affected)" + fi # Run the jq header auth test - cmd_prefix bash -c "HOME=/tmp/home PYTHONPATH=/tmp/home/.local/lib/python3.11/site-packages/ XDG_CONFIG_HOME=/tmp/home/.config API_PROTOCOL=http API_HOST=pulp-api API_PORT=8000 ADMIN_USERNAME=admin ADMIN_PASSWORD=$PASSWORD /tmp/home/.local/bin/pytest -o cache_dir=/tmp/home/.cache/pytest_cache -v -r sx --color=yes --pyargs pulpcore.tests.functional -m 'parallel' -n 8 -k 'test_jq_header_remote_auth' --junitxml=/tmp/home/junit-pulp-serial.xml" || debug_and_fail + if [ "$TRIAGE_OK" = "false" ] || python3 -c "import json,sys; sys.exit(0 if 'pulpcore' in json.load(open('/tmp/select.json')).get('plugins_affected',[]) else 1)" 2>/dev/null; then + cmd_prefix bash -c "HOME=/tmp/home PYTHONPATH=/tmp/home/.local/lib/python3.11/site-packages/ XDG_CONFIG_HOME=/tmp/home/.config API_PROTOCOL=http API_HOST=pulp-api API_PORT=8000 ADMIN_USERNAME=admin ADMIN_PASSWORD=$PASSWORD /tmp/home/.local/bin/pytest -o cache_dir=/tmp/home/.cache/pytest_cache -v -r sx --color=yes --pyargs pulpcore.tests.functional -m 'parallel' -n 8 -k 'test_jq_header_remote_auth' --junitxml=/tmp/home/junit-pulp-serial.xml" || debug_and_fail + else + echo "triage: skipping pulpcore tests (plugin not affected)" + fi ### END Adapted from ./.github/workflows/scripts/script.sh # Run pulp_maven functional tests - cmd_prefix bash -c "HOME=/tmp/home PYTHONPATH=/tmp/home/.local/lib/python3.11/site-packages/ XDG_CONFIG_HOME=/tmp/home/.config API_PROTOCOL=http API_HOST=pulp-api API_PORT=8000 ADMIN_USERNAME=admin ADMIN_PASSWORD=$PASSWORD /tmp/home/.local/bin/pytest -o cache_dir=/tmp/home/.cache/pytest_cache -v -r sx --color=yes --pyargs pulp_maven.tests.functional.api.test_download_content --junitxml=/tmp/home/junit-pulp-serial.xml" || debug_and_fail + if [ "$TRIAGE_OK" = "false" ] || python3 -c "import json,sys; sys.exit(0 if 'pulp_maven' in json.load(open('/tmp/select.json')).get('plugins_affected',[]) else 1)" 2>/dev/null; then + cmd_prefix bash -c "HOME=/tmp/home PYTHONPATH=/tmp/home/.local/lib/python3.11/site-packages/ XDG_CONFIG_HOME=/tmp/home/.config API_PROTOCOL=http API_HOST=pulp-api API_PORT=8000 ADMIN_USERNAME=admin ADMIN_PASSWORD=$PASSWORD /tmp/home/.local/bin/pytest -o cache_dir=/tmp/home/.cache/pytest_cache -v -r sx --color=yes --pyargs pulp_maven.tests.functional.api.test_download_content --junitxml=/tmp/home/junit-pulp-serial.xml" || debug_and_fail + else + echo "triage: skipping pulp_maven tests (plugin not affected)" + fi # Run pulp_npm functional tests - cmd_prefix bash -c "HOME=/tmp/home PYTHONPATH=/tmp/home/.local/lib/python3.11/site-packages/ XDG_CONFIG_HOME=/tmp/home/.config API_PROTOCOL=http API_HOST=pulp-api API_PORT=8000 ADMIN_USERNAME=admin ADMIN_PASSWORD=$PASSWORD /tmp/home/.local/bin/pytest -o cache_dir=/tmp/home/.cache/pytest_cache -v -r sx --color=yes --pyargs pulp_npm.tests.functional -k 'test_pull_through_install' --junitxml=/tmp/home/junit-pulp-serial.xml" || debug_and_fail + if [ "$TRIAGE_OK" = "false" ] || python3 -c "import json,sys; sys.exit(0 if 'pulp_npm' in json.load(open('/tmp/select.json')).get('plugins_affected',[]) else 1)" 2>/dev/null; then + cmd_prefix bash -c "HOME=/tmp/home PYTHONPATH=/tmp/home/.local/lib/python3.11/site-packages/ XDG_CONFIG_HOME=/tmp/home/.config API_PROTOCOL=http API_HOST=pulp-api API_PORT=8000 ADMIN_USERNAME=admin ADMIN_PASSWORD=$PASSWORD /tmp/home/.local/bin/pytest -o cache_dir=/tmp/home/.cache/pytest_cache -v -r sx --color=yes --pyargs pulp_npm.tests.functional -k 'test_pull_through_install' --junitxml=/tmp/home/junit-pulp-serial.xml" || debug_and_fail + else + echo "triage: skipping pulp_npm tests (plugin not affected)" + fi # Run pulp_service functional tests - cmd_prefix bash -c "HOME=/tmp/home PYTHONPATH=/tmp/home/.local/lib/python3.11/site-packages/ XDG_CONFIG_HOME=/tmp/home/.config API_PROTOCOL=http API_HOST=pulp-api API_PORT=8000 ADMIN_USERNAME=admin ADMIN_PASSWORD=$PASSWORD /tmp/home/.local/bin/pytest -o cache_dir=/tmp/home/.cache/pytest_cache -v -r sx --color=yes --pyargs pulp_service.tests.functional -m 'not parallel' --junitxml=/tmp/home/junit-pulp-serial.xml" || debug_and_fail + if [ "$TRIAGE_OK" = "false" ] || python3 -c "import json,sys; sys.exit(0 if 'pulp_service' in json.load(open('/tmp/select.json')).get('plugins_affected',[]) else 1)" 2>/dev/null; then + if [ "$TRIAGE_OK" = "true" ]; then + NODEIDS=$(python3 -c "import json; d=json.load(open('/tmp/select.json')); [print(t) for t in d['tests_to_run'] if 'pulp_service' in t]" 2>/dev/null | tr '\n' ' ') + NEW_TESTS=$(python3 -c "import json; d=json.load(open('/tmp/select.json')); [print(t) for t in d.get('new_test_files',[])]" 2>/dev/null | tr '\n' ' ') + ALL_TESTS="$NODEIDS $NEW_TESTS" + if [ -n "$(echo $ALL_TESTS | tr -d ' ')" ]; then + cmd_prefix bash -c "HOME=/tmp/home PYTHONPATH=/tmp/home/.local/lib/python3.11/site-packages/ XDG_CONFIG_HOME=/tmp/home/.config API_PROTOCOL=http API_HOST=pulp-api API_PORT=8000 ADMIN_USERNAME=admin ADMIN_PASSWORD=$PASSWORD /tmp/home/.local/bin/pytest -o cache_dir=/tmp/home/.cache/pytest_cache -v -r sx --color=yes --pyargs $ALL_TESTS --junitxml=/tmp/home/junit-pulp-serial.xml" || debug_and_fail + else + echo "triage: no pulp_service tests to run" + fi + else + cmd_prefix bash -c "HOME=/tmp/home PYTHONPATH=/tmp/home/.local/lib/python3.11/site-packages/ XDG_CONFIG_HOME=/tmp/home/.config API_PROTOCOL=http API_HOST=pulp-api API_PORT=8000 ADMIN_USERNAME=admin ADMIN_PASSWORD=$PASSWORD /tmp/home/.local/bin/pytest -o cache_dir=/tmp/home/.cache/pytest_cache -v -r sx --color=yes --pyargs pulp_service.tests.functional -m 'not parallel' --junitxml=/tmp/home/junit-pulp-serial.xml" || debug_and_fail + fi + else + echo "triage: skipping pulp_service tests (plugin not affected)" + fi - name: push-api-json-files-to-pulp when: diff --git a/.tekton/pulp-pull-request.yaml b/.tekton/pulp-pull-request.yaml index fb01e577..d9dbdd60 100644 --- a/.tekton/pulp-pull-request.yaml +++ b/.tekton/pulp-pull-request.yaml @@ -172,6 +172,27 @@ spec: workspace: workspace - name: basic-auth workspace: git-auth + - name: triage-diff + runAfter: + - clone-repository + taskSpec: + steps: + - name: generate-diff + image: registry.access.redhat.com/ubi9/ubi-minimal:latest + script: | + #!/bin/bash + set -ex + cd $(workspaces.source.path)/source + git fetch origin main --depth=1 || true + mkdir -p .triage + git diff origin/main...HEAD -- . > .triage/pr.diff 2>/dev/null || echo "" > .triage/pr.diff + echo "triage: diff is $(wc -l < .triage/pr.diff) lines" + workspaces: + - name: source + workspace: workspace + workspaces: + - name: source + workspace: workspace - name: prefetch-dependencies params: - name: input @@ -225,6 +246,7 @@ spec: - name: NO_PROXY value: $(tasks.init.results.no-proxy) runAfter: + - triage-diff - prefetch-dependencies taskRef: params: diff --git a/pulp_service/pulp_service/management/__init__.py b/pulp_service/pulp_service/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pulp_service/pulp_service/management/commands/__init__.py b/pulp_service/pulp_service/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pulp_service/pulp_service/management/commands/triage_urls.py b/pulp_service/pulp_service/management/commands/triage_urls.py new file mode 100644 index 00000000..32e40391 --- /dev/null +++ b/pulp_service/pulp_service/management/commands/triage_urls.py @@ -0,0 +1,87 @@ +""" +Django management command to generate .triage/urls.json. + +Run from within a Pulp Django environment: + + django-admin triage_urls --output .triage/urls.json +""" + +import json +import sys + +from django.core.management.base import BaseCommand +from django.urls import URLPattern, URLResolver, get_resolver + + +class Command(BaseCommand): + help = "Export URL routing map for triage test impact analysis" + + def add_arguments(self, parser): + parser.add_argument( + "--output", + type=str, + default=None, + help="Output file path (default: stdout)", + ) + + def handle(self, *args, **options): + resolver = get_resolver() + routes = [] + + self._collect_routes(resolver, "", routes) + routes.sort(key=lambda route: route["pattern"]) + + output = json.dumps(routes, indent=2) + if options["output"]: + with open(options["output"], "w") as output_file: + output_file.write(output) + self.stderr.write(f"Wrote {len(routes)} routes to {options['output']}") + else: + sys.stdout.write(output) + + def _collect_routes(self, resolver, prefix, routes): + for pattern in resolver.url_patterns: + if isinstance(pattern, URLResolver): + new_prefix = prefix + str(pattern.pattern) + self._collect_routes(pattern, new_prefix, routes) + elif isinstance(pattern, URLPattern): + route = self._extract_route(pattern, prefix) + if route: + routes.append(route) + + def _extract_route(self, pattern, prefix): + full_pattern = prefix + str(pattern.pattern) + callback = pattern.callback + + viewset_class = _resolve_viewset_class(callback) + if viewset_class is None: + return None + + dotted_path = f"{viewset_class.__module__}.{viewset_class.__qualname__}" + actions = _resolve_actions(callback) + + return { + "pattern": "/" + full_pattern.lstrip("/"), + "viewset": dotted_path, + "actions": sorted(set(actions)), + } + + +def _resolve_viewset_class(callback): + if hasattr(callback, "cls"): + return callback.cls + if hasattr(callback, "view_class"): + return callback.view_class + if hasattr(callback, "initkwargs") and "cls" in callback.initkwargs: + return callback.initkwargs["cls"] + return None + + +def _resolve_actions(callback): + if hasattr(callback, "actions"): + return list(callback.actions.values()) + if hasattr(callback, "initkwargs"): + actions_map = callback.initkwargs.get("actions", {}) + if isinstance(actions_map, dict): + return list(actions_map.values()) + return [] diff --git a/tools/triage/BUILD.md b/tools/triage/BUILD.md new file mode 100644 index 00000000..f606ae1e --- /dev/null +++ b/tools/triage/BUILD.md @@ -0,0 +1,18 @@ +# triage binary + +Pre-built from: https://github.com/pulp/agent-project (triage/ directory) + +## Rebuild + +```bash +cd agent-project/triage +cargo build --release --target x86_64-unknown-linux-gnu +cp target/release/triage /path/to/pulp-service/tools/triage/triage +cd /path/to/pulp-service/tools/triage && sha256sum triage > triage.sha256 +``` + +## Verify + +```bash +cd tools/triage && sha256sum -c triage.sha256 +``` diff --git a/tools/triage/triage b/tools/triage/triage new file mode 100755 index 00000000..274c37f7 Binary files /dev/null and b/tools/triage/triage differ diff --git a/tools/triage/triage.sha256 b/tools/triage/triage.sha256 new file mode 100644 index 00000000..442a8219 --- /dev/null +++ b/tools/triage/triage.sha256 @@ -0,0 +1 @@ +26d884a4fbd20612e20f66bb17b8301aa20ce776b1ffaa8c7826533b3464cc9f triage